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

`Lender` could stop operating when `feeReceiver` is `address(0)` and lending or collateral token revert on transfer to `address(0)`

Summary

The current implementation of Lender allows setting the feeReceiver to address(0) and the codebase always tries to send protocol fees to feeReceiver even when the variable is equal to address(0).

Some ERC20 implementations, like the OpenZeppelin ERC20 one, will revert if from or to are equal to address(0).

If the collateral token or the lending token use an implementation that reverts on transfer from/to address(0) and the feeReceiver is equal to address(0) the following operations will revert breaking the contract flow:

  • borrow

  • repay

  • giveLoan

  • buyLoan

  • seizeLoan

  • refinance

Vulnerability Details

Impact

The following functions will revert, breaking the protocol flow:

  • borrow

  • repay

  • giveLoan

  • buyLoan

  • seizeLoan

  • refinance

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)
)
);
}
}

RevertOnTransferAddressZeroTest.t.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "./BaseLender.sol";
contract ERC20RevertTransferAddressZero 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) {
require(to != address(0));
return super.transfer(to, amount);
}
function transferFrom(address from, address to, uint256 amount) public override returns (bool) {
require(from != address(0));
require(to != address(0));
return super.transferFrom(from, to, amount);
}
}
contract RevertOnTransferAddressZeroTest is BaseLender {
function setUp() override public {
super.setUp();
}
function testERC20WithFee() public {
// Admin of the Lender contract set the feeReceiver to `address(0)`
vm.prank(address(this));
lender.setFeeReceiver(address(0));
ERC20RevertTransferAddressZero newLoanToken = new ERC20RevertTransferAddressZero();
ERC20RevertTransferAddressZero newCollateralToken = new ERC20RevertTransferAddressZero();
vm.startPrank(lender1);
newLoanToken.mint(address(lender1), 1000 ether);
newLoanToken.approve(address(lender), 1000 ether);
newCollateralToken.mint(address(lender1), 1000 ether);
newCollateralToken.approve(address(lender), 1000 ether);
vm.stopPrank();
vm.startPrank(borrower);
newLoanToken.mint(address(borrower), 1000 ether);
newLoanToken.approve(address(lender), 1000 ether);
newCollateralToken.mint(address(borrower), 1000 ether);
newCollateralToken.approve(address(lender), 1000 ether);
vm.stopPrank();
// create the lending pool for Lender1
vm.prank(lender1);
bytes32 poolId1 = lender.setPool(Pool({
lender: lender1,
loanToken: address(newLoanToken),
collateralToken: address(newCollateralToken),
minLoanSize: 10 ether,
poolBalance: 1000 ether,
maxLoanRatio: 2 ether,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
}));
// borrower borrow something and protocol fees are triggered
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = Borrow({
poolId: poolId1,
debt: 100 ether,
collateral: 100 ether
});
// This operation will revert because the ERC20 do not allows transferring tokenes to `address(0)`
vm.prank(borrower);
vm.expectRevert();
lender.borrow(borrows);
}
}

Recommendations

If the client wants to enable the protocol to not take any fee on borrowing/supplying by setting feeReceiver equal to address(0) they must modify the code of all those functions to deduct and send the fee to feeReceiver only when feeReceiver is not equal to address(0).

If otherwise the feeReceiver cannot be equal to zero (the protocol always wants to take fees on the operations) the setFeeReceiver function should revert when the input parameter _feeReceiver is equal to address(0)

Support

FAQs

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

Give us feedback!