Platform: Code4rena
Start Date: 08/11/2022
Pot Size: $60,500 USDC
Total HM: 6
Participants: 72
Period: 5 days
Judge: Picodes
Total Solo HM: 2
Id: 178
League: ETH
Rank: 38/72
Findings: 1
Award: $77.22
🌟 Selected for report: 0
🚀 Solo Findings: 0
77.2215 USDC - $77.22
Native funds on the aggregator contract balance is a free grabLooksRareAggregator's execute() returns the native balance of the contract to the caller even when nothing was provided with the call.
This happens when LooksRareAggregator's execute() is called directly by a user with no ERC20 transfers, i.e. when tokenTransfersLength == 0
.
Bob the attacker can setup a bot that tracks aggregator's native balance and calls execute() with isAtomic == false
and some bogus trade to be reverted on the marketplace level, just to invoke _returnETHIfAny(Bob)
, which will transfer LooksRareAggregator's native balance to Bob.
This way whenever any user mistakenly sends directly any native funds to the contract, an operational mistake that happens a lot with any router-like contracts and inexperienced users, these funds will be immediately stolen by Bob's bot.
Net impact here is fund loss, which is conditional on user's mistake, so setting the severity to medium.
execute() can be called directly and set originator = msg.sender
, then proceeds with _returnETHIfAny(originator)
:
function execute( TokenTransfer[] calldata tokenTransfers, TradeData[] calldata tradeData, address originator, address recipient, bool isAtomic ) external payable nonReentrant { if (recipient == address(0)) revert ZeroAddress(); uint256 tradeDataLength = tradeData.length; if (tradeDataLength == 0) revert InvalidOrderLength(); uint256 tokenTransfersLength = tokenTransfers.length; if (tokenTransfersLength == 0) { originator = msg.sender; } else if (msg.sender != erc20EnabledLooksRareAggregator) { revert UseERC20EnabledLooksRareAggregator(); } for (uint256 i; i < tradeDataLength; ) { ... } if (tokenTransfersLength > 0) _returnERC20TokensIfAny(tokenTransfers, originator); _returnETHIfAny(originator); emit Sweep(originator); }
_returnETHIfAny() sends the whole native balance to the recipient
:
/** * @notice Return ETH back to the designated sender if any ETH is left in the payable call. */ function _returnETHIfAny(address recipient) internal { assembly { if gt(selfbalance(), 0) { let status := call(gas(), recipient, selfbalance(), 0, 0, 0, 0) } } }
Funds can be send directly to the contract by any user:
receive() external payable {}
As _returnETHIfAny() is to refund the native funds provided by the caller, consider recording msg.value
of the call and send back min(msg.value, balance)
.
#0 - c4-judge
2022-11-20T10:25:50Z
Picodes marked the issue as duplicate of #277
#1 - c4-judge
2022-12-16T14:02:49Z
Picodes marked the issue as satisfactory