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

The refinance function subtracts the debt from the pool balance twice

Summary

In the refinance function the debt is subtracted twice from the balance of the new pool. Therefore every time this function is called the chosen lender pays twice the debt without receiving it back.

Vulnerability Details

The following POC code shows the problem, it can be implemented inside the current test folder of the repo.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../../src/Lender.sol";
import {ERC20} from "solady/src/tokens/ERC20.sol";
contract TestERC20 is ERC20 {
function name() public pure override returns (string memory) {
return "Test ERC20";
}
function symbol() public pure override returns (string memory) {
return "TERC20";
}
function mint(address _to, uint256 _amount) public {
_mint(_to, _amount);
}
}
contract RefinanceUpdatesPoolBalanceTwiceTest is Test {
Lender public lender;
TestERC20 public loanToken;
TestERC20 public collateralToken;
address public user1 = vm.addr(1);
address public user2 = vm.addr(2);
address public user3 = vm.addr(2);
function setUp() public {
lender = new Lender();
loanToken = new TestERC20();
collateralToken = new TestERC20();
loanToken.mint(user1, 1000 * 10 ** 18);
loanToken.mint(user2, 1000 * 10 ** 18);
collateralToken.mint(user3, 1000 * 10 ** 18);
}
function test_refinance_updates_pool_balance_twice() public {
vm.startPrank(user1);
loanToken.approve(address(lender), 1000*10**18);
lender.setPool(
Pool({
lender: user1,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100*10**18,
poolBalance: 1000*10**18,
maxLoanRatio: 2*10**18,
auctionLength: 1 days,
interestRate: 100,
outstandingLoans: 0
})
);
vm.stopPrank();
vm.startPrank(user2);
loanToken.approve(address(lender), 1000*10**18);
lender.setPool(
Pool({
lender: user2,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100*10**18,
poolBalance: 1000*10**18,
maxLoanRatio: 2*10**18,
auctionLength: 1 days,
interestRate: 100,
outstandingLoans: 0
})
);
vm.stopPrank();
bytes32 pool1Id = lender.getPoolId(user1, address(loanToken), address(collateralToken));
bytes32 pool2Id = lender.getPoolId(user2, address(loanToken), address(collateralToken));
(,,,,uint256 poolBalance1,,,,) = lender.pools(pool1Id);
assertEq(poolBalance1, 1000 * 10**18); // pool balance untouched
vm.startPrank(user3);
// user takes a loan
collateralToken.approve(address(lender), 100*10**18);
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = Borrow({
poolId: pool1Id,
debt: 200*10**18,
collateral: 100*10**18
});
lender.borrow(borrows);
(,,,,uint256 poolBalance2,,,,) = lender.pools(pool1Id);
assertEq(poolBalance2, 800 * 10**18); // pool balance - debt
// user calls refinance to change the pool of the loan
Refinance[] memory refinances = new Refinance[](1);
refinances[0] = Refinance({
loanId: 0,
poolId: pool2Id,
debt: 200*10**18,
collateral: 100*10**18
});
lender.refinance(refinances);
vm.stopPrank();
(,,,,uint256 poolBalance3,,,,) = lender.pools(pool2Id);
assertEq(poolBalance3, 600 * 10**18); // pool balance - 2xdebt
}
}

Impact

Lenders lose funds, and they are stuck inside the contract. Malicious users could even use this burn funds from other users.

Tools Used

Manual Review, Foundry, VSCode

Recommendations

Subtract the debt only once.

Support

FAQs

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

Give us feedback!