Open code423n4 opened 1 year ago
0xSorryNotSorry marked the issue as high quality report
0xSorryNotSorry marked the issue as primary issue
price refers to the immediate mid price, not the execution price. the issue is still valid because of:
If a token with more then 36 decimals appears, function will revert
but imo is an informational rather than mid issue
outdoteth marked the issue as disagree with severity
outdoteth marked the issue as sponsor acknowledged
Agree with the Sponsor because in almost 3 years I have never seen a 36 decimals token
GalloDaSballo changed the severity to QA (Quality Assurance)
L + 3 for effort
Lines of code
https://github.com/code-423n4/2023-04-caviar/blob/main/src/PrivatePool.sol#L742-L746
Vulnerability details
Impact
PrivatePool
has aprice
function that is used to determine what the actual price of any NFT would be. This is needed and used for determining slippage. We can also see it being used in theEthRouter
, where it operates with ETH only.The formula used to determine price is incorrect, leading to a radically different price. This leads to a faulty slippage mechanism, that for any protocol integrator which wraps the PrivatePool and uses price for slippage validation will have distorted cases, ultimately leading to user attempting buys when the price is actually different.
There are 5 issues with the function that are detailed bellow.
xy=k
invariantIssues details
The price calculation is as follows:
https://github.com/code-423n4/2023-04-caviar/blob/main/src/PrivatePool.sol#L740-L746
For 1, 2 and 3:
Calculating actual buying price, while keeping
xy=k
invariant is shown inbuyQuote
inPrivatePool
https://github.com/code-423n4/2023-04-caviar/blob/main/src/PrivatePool.sol#L699-L701
with outputAmount being the number of NFTs to be bought, multiplied by 1e18 and rounded up.
Since
price()
determines the price of a pool NFT, scaled to 18 decimals, a partial implementation would actually be similar tobuyQuote
:skipping the roundup and decimal adjustments for clarity we see that the founction sould be:
price = 1e18 * virtualBaseTokenReserves / (virtualNftReserves - 1e18)
instead of(virtualBaseTokenReserves * 10 ** exponent) / virtualNftReserves;
This calculation of price will not reflect an actual price, as the buy will use
buyQuote
that maintains the xy=k invariant, while this does not.It is also a very bad practice to not include fees in the price, users are misslead that the NFTs will cost less.
For point 4, the end division by
virtualNftReserves
(which is 1e18 multiplied) results in a lack of precision.For point 5, because of the following subtraction, any token with more then 36 decimals will revert when price will be requested
https://github.com/code-423n4/2023-04-caviar/blob/main/src/PrivatePool.sol#L744
The protocol also clearly indicates to not call functions directly, but to use wrappers that use slippage (which is calculated using price()) in order to avoid losing funds on buy and change:
https://github.com/code-423n4/2023-04-caviar/blob/main/src/PrivatePool.sol#L204-L211 https://github.com/code-423n4/2023-04-caviar/blob/main/src/PrivatePool.sol#L293-L301
Proof of Concept
Add the following test in
Quotes.t.sol
Tools Used
Manual analysis
Recommended Mitigation Steps
Resolve the indicated issues, it would be simplest to modify the price function to work similar to
buyQuote
. Regardless of implementation it must include fees, expecially since it is used for slippage control.