Caviar Private Pools - Kek'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: 62/120

Findings: 2

Award: $40.02

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

Labels

bug
2 (Med Risk)
high quality report
satisfactory
duplicate-873

Awards

34.044 USDC - $34.04

External Links

Lines of code

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

Vulnerability details

Note that EthRouter.change() can take an array of changes: Change[].

The comment above the function also notes that this function should be able to accept multiple changes.

However, msg.value remains constant throughout each iteration of the loop, which means after the first loop (first change) is done executing, the second loop/change is left with no more msg.value to send.

The offending line of code: https://github.com/code-423n4/2023-04-caviar/blob/cd8a92667bcb6657f70657183769c244d04c015c/src/EthRouter.sol#L273

After the first loop, even if the excess is refunded through PrivatePool.sol#L436 the second call will fail because it no longer has the original msg.value balance causing an Out of Funds Revert.

POC

For this test I split the original test to 2 separate changes in the Change[] array. I have avoided minting extra tokens to ensure the weights and Merkle Proof remain the same.

Test file: /test/EthRouter/Change.t.sol

Run test:

forge test --ffi -m test_CallsChangeWithData_2ElementsInChangeArray -vvvv
diff --git a/Change.t.sol b/Change.t_updated.sol
index dbb69ca..d992151 100644
--- a/Change.t.sol
+++ b/Change.t_updated.sol
@@ -137,18 +137,23 @@ contract ChangeTest is Fixture {
         }
     }
 
-    function test_CallsChangeWithData() public {
-        uint256[] memory inputTokenIds = new uint256[](5);
+    function test_CallsChangeWithData_2ElementsInChangeArray() public {
+        uint256[] memory inputTokenIds = new uint256[](2);
         uint256[] memory inputTokenWeights = new uint256[](0);
-        uint256[] memory outputTokenIds = new uint256[](5);
+        uint256[] memory outputTokenIds = new uint256[](2);
         uint256[] memory outputTokenWeights = new uint256[](0);
 
-        for (uint256 i = 0; i < 5; i++) {
+        uint256[] memory inputTokenIds2 = new uint256[](3);
+        uint256[] memory inputTokenWeights2 = new uint256[](0);
+        uint256[] memory outputTokenIds2 = new uint256[](3);
+        uint256[] memory outputTokenWeights2 = new uint256[](0);
+
+        for (uint256 i = 0; i < 2; i++) {
             inputTokenIds[i] = i + 5;
             outputTokenIds[i] = i;
         }
 
-        EthRouter.Change[] memory changes = new EthRouter.Change[](1);
+        EthRouter.Change[] memory changes = new EthRouter.Change[](2);
         changes[0] = EthRouter.Change({
             pool: payable(address(privatePool)),
             nft: address(milady),
@@ -167,8 +172,31 @@ contract ChangeTest is Fixture {
             )
         });
 
+        for (uint256 i = 2; i < 5; i++) {
+            inputTokenIds2[i - 2] = i + 5;
+            outputTokenIds2[i - 2] = i;
+        }
+
+        changes[1] = EthRouter.Change({
+            pool: payable(address(privatePool)),
+            nft: address(milady),
+            inputTokenIds: inputTokenIds2,
+            inputTokenWeights: inputTokenWeights2,
+            inputProof: PrivatePool.MerkleMultiProof(
+                new bytes32[](0),
+                new bool[](0)
+            ),
+            stolenNftProofs: new IStolenNftOracle.Message[](0),
+            outputTokenIds: outputTokenIds2,
+            outputTokenWeights: outputTokenWeights2,
+            outputProof: PrivatePool.MerkleMultiProof(
+                new bytes32[](0),
+                new bool[](0)
+            )
+        });
+
         (uint256 changeFee, ) = privatePool.changeFeeQuote(
-            inputTokenIds.length * 1e18
+            inputTokenIds.length * 1e18 + inputTokenIds2.length * 1e18
         );
 
         // act

Note how the following test fails to execute with an OutOfFund exception.

...
... // first call to change() successful ...
...
    │   ├─ [76892] PrivatePool::change{value: 25000000000000000000}([5, 6], [], ([], []), [], [0, 1], [], ([], [])) 
    │   │   ├─ [5764] StolenNftOracle::validateTokensAreNotStolen(Milady: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], [5, 6], []) 
    │   │   │   └─ ← ()
    │   │   ├─ [419] Factory::protocolFeeRate() [staticcall]
    │   │   │   └─ ← 0
    │   │   ├─ [55] EthRouter::receive{value: 15000000000000000000}() 
    │   │   │   └─ ← ()
...
... // second call to change() failed
...
    │   ├─ [0] PrivatePool::change{value: 25000000000000000000}([7, 8, 9], [], ([], []), [], [2, 3, 4], [], ([], [])) 
    │   │   └─ ← "EvmError: OutOfFund"
    │   └─ ← "EvmError: Revert"
    └─ ← "EvmError: Revert"

Test result: FAILED. 0 passed; 1 failed; finished in 3.20ms

Failing tests:
Encountered 1 failing test in test/EthRouter/Change.t_updated.sol:ChangeTest
[FAIL. Reason: EvmError: Revert] test_CallsChangeWithData_2ElementsInChangeArray() (gas: 336274)

Note that both calls to change() send a value of 25000000000000000000, however, the second call no longer has that amount of Eth to send due to the fee from executing the first change() call.

Remediation

msg.value/fees should be tracked and decremented for each iteration of the loop so proper fees are passed upon each call to PrivatePool.change().

#0 - c4-pre-sort

2023-04-19T22:28:06Z

0xSorryNotSorry marked the issue as high quality report

#1 - c4-pre-sort

2023-04-20T16:55:24Z

0xSorryNotSorry marked the issue as duplicate of #873

#2 - c4-judge

2023-05-01T07:12:08Z

GalloDaSballo marked the issue as satisfactory

Awards

5.9827 USDC - $5.98

Labels

bug
2 (Med Risk)
low quality report
satisfactory
duplicate-858

External Links

Lines of code

https://github.com/code-423n4/2023-04-caviar/blob/cd8a92667bcb6657f70657183769c244d04c015c/src/PrivatePool.sol#L733

Vulnerability details

Impact

PrivatePool.changeFeeQuote() function will revert with GUSD which only has 2 decimals.

Will revert @ line: PrivatePool.sol#L733 w/ underflow of uint256.

This will cause all calls to PrivatePool.change() to revert @ this line PrivatePool.sol#L416

Consider changing the PrivatePool.changeFeeQuote() algorithm or restricting tokens based on # of decimals upon initialization.

#0 - c4-pre-sort

2023-04-19T22:27:12Z

0xSorryNotSorry marked the issue as low quality report

#1 - c4-judge

2023-05-01T11:06:48Z

GalloDaSballo marked the issue as duplicate of #858

#2 - GalloDaSballo

2023-05-01T11:06:54Z

I believe the finding to be fully valid in spite of how short it is

#3 - c4-judge

2023-05-01T11:06:58Z

GalloDaSballo marked the issue as satisfactory

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