Caviar Private Pools - BradMoon's results

A fully on-chain NFT AMM that allows you to trade every NFT in a collection.

General Information

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

Caviar

Findings Distribution

Researcher Performance

Rank: 107/120

Findings: 1

Award: $8.51

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: minhtrng

Also found by: 0x4db5362c, 0xRobocop, BradMoon, ChrisTina, Kek, Rappie, Ruhum, Voyvoda, adriro, bin2chen, chaduke, ladboy233, ych18

Labels

bug
2 (Med Risk)
downgraded by judge
partial-25
satisfactory
sponsor confirmed
duplicate-873

Awards

8.511 USDC - $8.51

External Links

Lines of code

https://github.com/code-423n4/2023-04-caviar/blob/cd8a92667bcb6657f70657183769c244d04c015c/src/EthRouter.sol#L254-#L293

Vulnerability details

Impact

Fund loss

Proof of Concept

Lines 254 to 293 have a vulnerability called "Dangerous Usage of msg.value inside a Loop". The function change accepts an array of Change struct as input and executes them one by one in a loop. Inside the loop, there is a call to PrivatePool.change function at line 273 which transfers ETH to the PrivatePool contract. The amount of ETH transferred is equal to msg.value which is set to the value of the value field in the transaction that calls the change function. Since the change function is called in a loop, the value of msg.value can be different for each iteration of the loop. This can lead to unexpected behavior and result in a loss of funds.

Tools Used

https://app.metatrust.io/

To fix this vulnerability, we should move the transfer of ETH outside the loop and calculate the total amount of ETH to be transferred before calling the PrivatePool.change function. We can use a variable to keep track of the total amount of ETH to be transferred and update it inside the loop. Then, we can transfer the total amount of ETH to the PrivatePool contract after the loop.

Here is an example of the fixed code:

function change(Change[] calldata changes, uint256 deadline) public payable { // check deadline has not passed (if any) if (block.timestamp > deadline && deadline != 0) { revert DeadlinePassed(); }

uint256 totalValue = 0; // loop through and execute the changes for (uint256 i = 0; i < changes.length; i++) { Change memory _change = changes[i]; // transfer NFTs from caller for (uint256 j = 0; j < changes[i].inputTokenIds.length; j++) { ERC721(_change.nft).safeTransferFrom(msg.sender, address(this), _change.inputTokenIds[j]); } // approve pair to transfer NFTs from router ERC721(_change.nft).setApprovalForAll(_change.pool, true); // calculate the value to be transferred uint256 changeValue = calculateChangeValue(_change); // add the value to the total value totalValue += changeValue; // execute change PrivatePool(_change.pool).change{value: changeValue}( _change.inputTokenIds, _change.inputTokenWeights, _change.inputProof, _change.stolenNftProofs, _change.outputTokenIds, _change.outputTokenWeights, _change.outputProof ); // transfer NFTs to caller for (uint256 j = 0; j < changes[i].outputTokenIds.length; j++) { ERC721(_change.nft).safeTransferFrom(address(this), msg.sender, _change.outputTokenIds[j]); } } // transfer the total value to the PrivatePool contract if (totalValue > 0) { (bool success, ) = address(PrivatePool).call{value: totalValue}(""); require(success, "Transfer failed"); } // refund any surplus ETH to the caller if (address(this).balance > 0) { msg.sender.safeTransferETH(address(this).balance); }

}

function calculateChangeValue(Change memory _change) internal view returns (uint256) { // calculate the value to be transferred uint256 inputValue = 0; for (uint256 i = 0; i < _change.inputTokenIds.length; i++) { inputValue += getTokenValue(_change.inputTokenIds[i], _change.inputTokenWeights[i]); }

uint256 outputValue = 0; for (uint256 i = 0; i < _change.outputTokenIds.length; i++) { outputValue += getTokenValue(_change.outputTokenIds[i], _change.outputTokenWeights[i]); } return inputValue > outputValue ? inputValue - outputValue : 0;

}

function getTokenValue(uint256 tokenId, uint256 tokenWeight) internal view returns (uint256) { // calculate the value of the token // ... }

#0 - c4-pre-sort

2023-04-20T19:31:03Z

0xSorryNotSorry marked the issue as primary issue

#1 - c4-sponsor

2023-04-22T09:47:25Z

outdoteth marked the issue as sponsor confirmed

#2 - outdoteth

2023-04-22T16:07:50Z

#3 - c4-judge

2023-04-30T15:36:15Z

GalloDaSballo changed the severity to 2 (Med Risk)

#4 - c4-judge

2023-04-30T15:36:28Z

GalloDaSballo marked the issue as duplicate of #873

#5 - GalloDaSballo

2023-04-30T15:37:40Z

The finding is valid and correctly flagged

The finding itself seems to be generated via a tool and it's unclear that the Warden understood the impact

Because the cause is correct, I'll award the finding

Because the impact is incorrect, I'm awarding 25%

The reality is that tx will revert if change uses any value, which was not explained here

#6 - c4-judge

2023-04-30T15:37:45Z

GalloDaSballo marked the issue as partial-50

#7 - c4-judge

2023-05-01T07:12:56Z

GalloDaSballo marked the issue as partial-25

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter