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

`Lender` does not handle correctly rebasing, inflationary, deflationary tokens and tokens with fee on transfer

Summary

The current implementation of the Lender contract does not handle these kinds of tokens:

  • Rebasing tokens

  • Inflationary tokens

  • Deflationary tokens

  • Tokens with fee-on-transfer

The accounting variables on the Loan and Pool structs will store an incorrect value that could lead to reverts or further accounting errors.

Vulnerability Details

The current implementation of the Lender contract does not handle these kinds of tokens:

  • Rebasing tokens

  • Inflationary tokens

  • Deflationary tokens

  • Tokens with fee-on-transfer

The accounting variables on the Loan and Pool structs will store an incorrect value that could lead to reverts or further accounting errors.

Impact

All the following accounting variables will store the incorrect amount of tokens:

  • loan.debt

  • loan.collateral

  • pool.poolBalance

  • pool.outstandingLoans

The accounting variables on the Loan and Pool structs will store an incorrect value that could lead to reverts or further accounting errors.

Tools Used

Manual + foundry test

BaseLender.sol

// 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 TERC20 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 WrappedLender is Lender {
function getLoanDebtDetail(uint256 loanId) external view returns (uint256 fullDebt, uint256 interest, uint256 fees) {
Loan memory loan = loans[loanId];
// calculate the accrued interest
(interest, fees) = _calculateInterest(loan);
fullDebt = loan.debt + interest + fees;
}
function getPoolInfo(bytes32 poolId) external view returns (Pool memory) {
return pools[poolId];
}
function getLoanInfo(uint256 loanId) external view returns (Loan memory) {
return loans[loanId];
}
}
contract BaseLender is Test {
WrappedLender public lender;
TERC20 public loanToken;
TERC20 public collateralToken;
address public lender1 = address(0x1);
address public lender2 = address(0x2);
address public borrower = address(0x3);
address public fees = address(0x4);
function setUp() public virtual {
lender = new WrappedLender();
loanToken = new TERC20();
collateralToken = new TERC20();
loanToken.mint(address(lender1), 100000 ether);
loanToken.mint(address(lender2), 100000 ether);
collateralToken.mint(address(borrower), 100000 ether);
vm.startPrank(lender1);
loanToken.approve(address(lender), 1000000 ether);
collateralToken.approve(address(lender), 1000000 ether);
vm.startPrank(lender2);
loanToken.approve(address(lender), 1000000 ether);
collateralToken.approve(address(lender), 1000000 ether);
vm.startPrank(borrower);
loanToken.approve(address(lender), 1000000 ether);
collateralToken.approve(address(lender), 1000000 ether);
}
function borrow(address _borrower, bytes32 poolId) public {
vm.startPrank(_borrower);
Borrow memory b = Borrow({
poolId: poolId,
debt: 100 ether,
collateral: 100 ether
});
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = b;
lender.borrow(borrows);
vm.stopPrank();
}
function createPool(address _lender) public returns (bytes32){
vm.startPrank(_lender);
Pool memory p = Pool({
lender: _lender,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100 ether,
poolBalance: 1000 ether,
maxLoanRatio: 2 ether,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
bytes32 poolId = lender.setPool(p);
vm.stopPrank();
return poolId;
}
function startAuction(address _lender, uint256 loanId) public {
uint256[] memory loans = new uint256[](1);
loans[0] = loanId;
vm.prank(_lender);
lender.startAuction(loans);
}
function seizeLoan(uint256 loanId) public {
uint256[] memory loans = new uint256[](1);
loans[0] = loanId;
lender.seizeLoan(loans);
}
function getPoolId(address _lender) public returns (bytes32) {
return keccak256(
abi.encode(
address(_lender),
address(loanToken),
address(collateralToken)
)
);
}
}

ERC20FeeTest.t.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "./BaseLender.sol";
contract ERC20WithFee is ERC20 {
uint256 public constant FEE_BPS = 50; // 0.5%
uint256 public feeAccumulated;
function name() public pure override returns (string memory) {
return "Test ERC20WithFee";
}
function symbol() public pure override returns (string memory) {
return "ERC20WithFee";
}
function mint(address _to, uint256 _amount) public {
_mint(_to, _amount);
}
function transfer(address to, uint256 amount) public override returns (bool) {
uint256 protocolFee = amount * FEE_BPS / 10_000;
feeAccumulated += protocolFee;
return super.transfer(to, amount - protocolFee);
}
function transferFrom(address from, address to, uint256 amount) public override returns (bool) {
uint256 protocolFee = amount * FEE_BPS / 10_000;
feeAccumulated += protocolFee;
return super.transferFrom(from, to, amount - protocolFee);
}
}
contract ERC20FeeTest is BaseLender {
function setUp() override public {
super.setUp();
}
function testERC20WithFee() public {
ERC20WithFee loanTokenWithFee = new ERC20WithFee();
uint256 loanTokenForPool = 10 ether;
vm.startPrank(lender1);
loanTokenWithFee.mint(address(lender1), loanTokenForPool);
loanTokenWithFee.approve(address(lender), loanTokenForPool);
vm.stopPrank();
// create the lending pool for Lender1
vm.prank(lender1);
bytes32 poolId1 = lender.setPool(Pool({
lender: lender1,
loanToken: address(loanTokenWithFee),
collateralToken: address(collateralToken),
minLoanSize: 10 ether,
poolBalance: loanTokenForPool,
maxLoanRatio: 2 ether,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
}));
// Assert that the lender contract has only received (loanTokenForPool - ERC20_FEE) but that has accounted the whole loanTokenForPool in the poolBalance
assertLt(loanTokenWithFee.balanceOf(address(lender)), loanTokenForPool);
assertEq(loanTokenWithFee.balanceOf(address(lender)), loanTokenForPool - (loanTokenForPool * loanTokenWithFee.FEE_BPS() / 10_000));
assertEq(lender.getPoolInfo(poolId1).poolBalance, loanTokenForPool);
}
}

Recommendations

The protocol should choose one of the following options:

  1. Have a list of whitelisted collateral and lending tokens that can be used

  2. Correctly account the real amount that has been deposited to the Lender contract or from the Lender contract after the transfer has happened

Support

FAQs

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

Give us feedback!