Platform: Code4rena
Start Date: 21/08/2023
Pot Size: $36,500 USDC
Total HM: 1
Participants: 43
Period: 7 days
Judge: Dravee
Id: 277
League: ETH
Rank: 2/43
Findings: 3
Award: $2,307.51
🌟 Selected for report: 1
🚀 Solo Findings: 0
1933.5938 USDC - $1,933.59
https://github.com/code-423n4/2023-08-shell/blob/main/src/proteus/EvolvingProteus.sol#L563-L595 https://github.com/code-423n4/2023-08-shell/blob/main/src/proteus/EvolvingProteus.sol#L809-L814 https://github.com/code-423n4/2023-08-shell/blob/main/src/proteus/EvolvingProteus.sol#L549 https://github.com/code-423n4/2023-08-shell/blob/main/src/proteus/EvolvingProteus.sol#L552 https://github.com/code-423n4/2023-08-shell/blob/main/src/proteus/EvolvingProteus.sol#L641 https://github.com/code-423n4/2023-08-shell/blob/main/src/proteus/EvolvingProteus.sol#L644
The EvolvingProteus.depositGivenInputAmount
function is used to calculate the output amount of LP tokens given an input amount of reserve tokens.
The EvolvingProteus.withdrawGivenOutputAmount
function is used to calculate the amount of LP tokens that must be burned given an output amount of reserve token.
Both above functions will calculate the changed LP token supply
proportionately
to the change in the utility of the pool
.
In the function execution of the above two functions (depositGivenInputAmount
and withdrawGivenOutputAmount
) the _reserveTokenSpecified
internal function is called.
The _reserveTokenSpecified
function is used to compute the changed amount
of the LP token supply
based on the proportional change of the utility
of the pool.
The issue here is that after the LP token total supply change calculation, there is no check performed on the token balance reserves and balance reserve ratio. Hence this function does not verify whether the reserve balances are within the valid boundaries once the transaction is performed.
Both the EvolvingProteus._swap
function and the EvolvingProteus._lpTokenSpecified
functions ensure that reserve balances are within valid boundaries once the respective transactions are completed by calling the EvolvingProteus._checkBalances
private function. But the _reserveTokenSpecified
function does not call on the EvolvingProteus._checkBalances
function to verify whether the reserve balances are within the valid boundaries once the transaction is completed.
The EvolvingProteus.depositGivenInputAmount
transaction will deposit an amount of specified token amount, which could decrease the y.divi(x)
(here y is the non-specified token amount and x is the specified token amount deposited) below the MIN_M
value (since x is increasing as its getting deposited). Hence the transaction should revert since the reserve token balance ratio is invalid. But the transaction will execute successfully because there is no check performed to identify this issue.
Similarly when calling the EvolvingProteus.withdrawGivenOutputAmount
transaction, an amount of specified token will be withdrawn from the pool. This will decrease the specified token balance of the pool and could increase the y.divi(x)
ratio (since x is the specified token and it is getting decreased thus increasing the ratio) beyond the MAX_M
value. Hence the transaction should revert since the reserve token balance ratio is invalid. But the transaction will execute successfully because there is no check performed to identify this issue.
Hence it is clear lack of reserve balance checks in the _reserveTokenSpecified
function breaks the key invariants related to the reserve token balances and reserve token balance ratio during deposit
and withdraw
transactions. Hence this breaks key behaviour of the protocol.
function _reserveTokenSpecified( SpecifiedToken specifiedToken, int256 specifiedAmount, bool feeDirection, int256 si, int256 xi, int256 yi ) internal view returns (int256 computedAmount) { int256 xf; int256 yf; int256 ui; int256 uf; { // calculating the final price points considering the fee if (specifiedToken == SpecifiedToken.X) { xf = xi + _applyFeeByRounding(specifiedAmount, feeDirection); yf = yi; } else { yf = yi + _applyFeeByRounding(specifiedAmount, feeDirection); xf = xi; } } ui = _getUtility(xi, yi); uf = _getUtility(xf, yf); uint256 result = Math.mulDiv(uint256(uf), uint256(si), uint256(ui)); require(result < INT_MAX); int256 sf = int256(result); // apply fee to the computed amount computedAmount = _applyFeeByRounding(sf - si, feeDirection); }
https://github.com/code-423n4/2023-08-shell/blob/main/src/proteus/EvolvingProteus.sol#L563-L595
function _checkBalances(int256 x, int256 y) private pure { if (x < MIN_BALANCE || y < MIN_BALANCE) revert BalanceError(x,y); int128 finalBalanceRatio = y.divi(x); if (finalBalanceRatio < MIN_M) revert BoundaryError(x,y); else if (MAX_M <= finalBalanceRatio) revert BoundaryError(x,y); }
https://github.com/code-423n4/2023-08-shell/blob/main/src/proteus/EvolvingProteus.sol#L809-L814
_checkBalances(xi + specifiedAmount, yi + computedAmount);
https://github.com/code-423n4/2023-08-shell/blob/main/src/proteus/EvolvingProteus.sol#L549
_checkBalances(xi + computedAmount, yi + specifiedAmount);
https://github.com/code-423n4/2023-08-shell/blob/main/src/proteus/EvolvingProteus.sol#L552
computedAmount = _applyFeeByRounding(xf - xi, feeDirection);
https://github.com/code-423n4/2023-08-shell/blob/main/src/proteus/EvolvingProteus.sol#L641
computedAmount = _applyFeeByRounding(yf - yi, feeDirection);
https://github.com/code-423n4/2023-08-shell/blob/main/src/proteus/EvolvingProteus.sol#L644
VSCode and Manual Review
It is recommended to call the EvolvingProteus._checkBalances
function with the relevant parameters at the end of the _reserveTokenSpecified
function, to verify once the deposit or withdraw transaction is over the reserve balances and reserve balance ratio are within the valid boundaries of the protocol.
Other
#0 - c4-pre-sort
2023-08-29T01:43:43Z
0xRobocop marked the issue as duplicate of #268
#1 - c4-pre-sort
2023-08-29T01:43:52Z
0xRobocop marked the issue as sufficient quality report
#2 - c4-judge
2023-09-11T19:26:36Z
JustDravee marked the issue as satisfactory
🌟 Selected for report: Udsen
Also found by: 0xSmartContract, 0xmystery, 0xprinc, Fulum, JP_Courses, MatricksDeCoder, Mirror, MohammedRizwan, MrPotatoMagic, Rolezn, Shubham, Testerbot, ast3ros, chainsnake, lanrebayode77, lsaudit, nisedo, plainshift, pontifex, prapandey031
126.4234 USDC - $126.42
duration
VARIABLE IN THE constructor
In the EvolvingProteus.constructor
the duration
is passed in as an input parameter. duration
denotes the total duration of the curve's evolution. Hence the duration
variable is a critical variable of the protocol. If duration is set to zero by mistake it will prompt a redeployment of the contract which could be wastage of gas and additional work load. Hence an input validity check should be performed for the duration
variable inside the constructor
as follows:
require(duration != 0, `Duration can not be zero`);
https://github.com/code-423n4/2023-08-shell/blob/main/src/proteus/EvolvingProteus.sol#L243-L263
The EvolvingProteus._getUtility
function is used to calculate the utility
value of the pool at the time of the function call. The utilitiy
is calculated using a quadratic formula which is shown below:
k(ab - 1)u**2 + (ay + bx)u + xy/k = 0
As per the equation there is a k
value used in the a
adn c
coefficients. But this k
value is not used when calculating the aQuad
and the cQuad
values. This could be due to normalization
of the equation or the k = 1
. But the reason for the missing k
value from the coefficient calculations is not given.
Hence it is fair to assume there is a discrepency between the Natspec comments and the actual implementation of the logic.
https://github.com/code-423n4/2023-08-shell/blob/main/src/proteus/EvolvingProteus.sol#L697 https://github.com/code-423n4/2023-08-shell/blob/main/src/proteus/EvolvingProteus.sol#L712-L714
uint256
CAN HOLD IN EvolvingProteus
CONTRACT DUE TO INT_MAX
CAPIn the EvolvingProteus
contract the input parameter amounts to the transaction functions such as swap, deposit and withdraw are capped by INT_MAX
value. The input parameters are required to be less than this amount as shown below:
require( burnedAmount < INT_MAX && xBalance < INT_MAX && yBalance < INT_MAX && totalSupply < INT_MAX );
Hence the any of the passed in input parameters can not be equal to the INT_MAX
value.
But when deriving an output amount using helper functions of this contract, the resultant amounts upcasted from int256
to uint256
value. When it is converted into a uint256
value those parameters are allowed to have a value = INT_MAX
. This is because upcasting does not lead to data truncation. But it does not treat all the uint256
variables in the function scope equally since the uint256 values calculated inside the function scope can have the type(int256).max
value where as the passed in uint256 input parameters can not be equal to the type(int256).max
value.
Hence there is a discrepency between the amounts that an uint256 can hold inside this smart contract. This discrepency in uint256
allowed values can lead to precision loss and other unexpected behaviours of the contract.
Hence it is recommended to either allow the passed in uint256
input parameters to the functions, to have a value equal to type(int256).max
, or prohibit internal uint256
variables of the EvolvingProteus
contract from having values equal to type(int256).max
value.
https://github.com/code-423n4/2023-08-shell/blob/main/src/proteus/EvolvingProteus.sol#L434-L439 https://github.com/code-423n4/2023-08-shell/blob/main/src/proteus/EvolvingProteus.sol#L589
withdrawGivenInputAmount
FUNCTIONThe natspec
comments of the EvolvingProteus.withdrawGivenInputAmount
function states that the feeDirection
of the function to be used is FEE_UP
. The reason for using FEE_UP
is described as wanting to increase the perceived amount of reserve tokens leaving the pool. But the feeDirection
is determined by the protocol to benefit itself. Hence withdrawGivenInputAmount
transaction will benefit the protocol if perceived amount of reserve tokens leaving the pool is decreased which is equivalent to charging a fee from the withdrawing amount.
The logic implemenation of the withdrawGivenInputAmount
function has implemented this requirement correctly as shown below:
int256 result = _lpTokenSpecified( //@audit-info - amount of reserved token to withdraw withdrawnToken, -int256(burnedAmount), FEE_DOWN, //@audit-issue - discrepency between the NATSPEC and the implementation regarding FEE_DOWN int256(totalSupply), int256(xBalance), int256(yBalance) );
Hence it is recommended to update the natspect
comments of the withdrawGivenInputAmount
function to state that the feeDirection
used for the withdrawGivenInputAmount
transaction is FEE_DOWN
by providing it with the proper reason to do so as well.
https://github.com/code-423n4/2023-08-shell/blob/main/src/proteus/EvolvingProteus.sol#L459-L461 https://github.com/code-423n4/2023-08-shell/blob/main/src/proteus/EvolvingProteus.sol#L478-L485
@param px_final The final price at the `y axis`
Above comment should be corrected as follows:
@param px_final The final price at the `x axis`
https://github.com/code-423n4/2023-08-shell/blob/main/src/proteus/EvolvingProteus.sol#L240
The minimum price value calculated with abdk library equivalent to `10^12(wei)`
The above comment should be corrected as follows since the given value (10^12) in the comment is not the minimum price value
The minimum price value calculated with abdk library equivalent to `10^10(wei)`
https://github.com/code-423n4/2023-08-shell/blob/main/src/proteus/EvolvingProteus.sol#L173
* without caring about which particular function `is was` called.
The above comment should be corrected as follows:
* without caring about which particular function `was` called.
https://github.com/code-423n4/2023-08-shell/blob/main/src/proteus/EvolvingProteus.sol#L736
https://github.com/code-423n4/2023-08-shell/blob/main/src/proteus/EvolvingProteus.sol#L768
The EvolvingProteus
smart contract performs critical operations such as swaps
, deposits
, withdrawals
. But these operations lack events
. Hence it is recommended to emit events when above operations are succefully executed for off-chain analysis.
https://github.com/code-423n4/2023-08-shell/blob/main/src/proteus/EvolvingProteus.sol#L506-L554 https://github.com/code-423n4/2023-08-shell/blob/main/src/proteus/EvolvingProteus.sol#L463-L490
revert
MESSAGES TO THE require
STATEMENTSIn the EvolvingProteus
contract, there are multiple occassions require
statement are used for conditional checks. But these require
statements are not using revert messages
. Hence it these require
statements revert it will be difficult to find the reason for the revert
.
Hence it is recommneded to add meaningful revert messages to these require
statements.
https://github.com/code-423n4/2023-08-shell/blob/main/src/proteus/EvolvingProteus.sol#L414 https://github.com/code-423n4/2023-08-shell/blob/main/src/proteus/EvolvingProteus.sol#L397-L402
#0 - 0xRobocop
2023-08-30T04:02:59Z
L-01 dup of #118
#1 - c4-pre-sort
2023-08-30T04:03:13Z
0xRobocop marked the issue as sufficient quality report
#2 - c4-judge
2023-09-11T19:38:56Z
JustDravee marked the issue as grade-a
#3 - c4-judge
2023-09-12T08:21:04Z
JustDravee marked the issue as selected for report
#4 - JustDravee
2023-09-12T08:24:57Z
Should add the following issues to the report:
1. RECOMMENDED TO ADD AN INPUT VALIDATION FOR THE duration VARIABLE IN THE constructor
)duration
VARIABLE IN THE constructor
Low (0-input validation). Same issue as https://github.com/code-423n4/2023-08-shell-findings/issues/118
Low (Issue with comment)
uint256
CAN HOLD IN EvolvingProteus
CONTRACT DUE TO INT_MAX
CAPInsufficient proof / Invalid
withdrawGivenInputAmount
FUNCTIONLow (Issue with comment)
3 Lows (Issue with comment)
Invalid (view functions can't have Events)
revert
MESSAGES TO THE require
STATEMENTSOut of scope due to https://github.com/code-423n4/2023-08-shell/blob/main/bot-report.md#nc-16-requirerevert-without-any-message
🌟 Selected for report: 0xSmartContract
Also found by: 0xmystery, Udsen, carrotsmuggler, catellatech, ihtishamsudo, moneyversed, oakcobalt, pfapostol, plainshift, rjs
247.503 USDC - $247.50
The EvolvingProteus
is a solution to passively update the liqudity concentration over time. It can be thought of as a hybrid implementation between Balancer liquidity bootstrapping pool and Uniswap v3. Here the starting liquidty concentration and ending liquidty concentration is set by the creator who sets the starting timestamp and ending timestamp as well. Within this duration the liquidity concentratino evolves.
The main smart contract which is within the scope for this audit is EvolvingProteus.sol
.
The EvolvingProteus.sol
is a smart contract that implements a liquidity pool with a time-dependent bonding curve. The bonding curve is used to determine the price of tokens in the pool based on the current supply. The curve evolves over time according to the parameters specified at the contract's deployment.
The contract uses the ABDKMath64x64
library for fixed-point arithmetic
and the OpenZeppelin Math library
for basic mathematical operations. It also imports an interface for a liquidity pool implementation and a specified token type.
The contract includes functions for swapping tokens, depositing tokens into the pool, and withdrawing tokens from the pool. Each of these functions can be called with either a specified input amount or a specified output amount. The contract calculates the corresponding output or input amount, respectively, based on the current state of the bonding curve.
The contract also includes a library, LibConfig
, which is used to calculate the parameters of the bonding curve
at any given time.
The contract includes several checks to ensure that transactions do not result in an invalid state. For example, it checks that the amount of tokens being swapped, deposited, or withdrawn is not too small or too large relative to the size of the reserves in the pool. It also checks that the ratio between the two reserves does not fall below or exceed certain limits.
The majority of my findings have been based on how the key invariants are broken due to edge cases that EvolvingProteus.sol
contract could face. A brief summary of each of the findings is given below:
H1 : How the absoluteValue < FIXED_FEE * 2
key invariant is broken with in the _applyFeeByRounding
function. This function is used to charge fees by rounding values.
H2 : How the key invariants related to reserve balances in the _checkBalances
function can be broken due to the usage of errorneous values.
H3: _reserveTokenSpecified
function does not check whether reserve balance invariants hold after the function exeuction is completed.
M1: The utility
is calculated using a quadratic equation. It's solution gives two answers based on the sign of the discriminant
. The unsafe casting of discriminant
to uint256
value could result in a errorneous value thus providing wrong output
M2: Condtional check which excludes the exact value finalBalanceRatio
in the _checkBalances
function is errorneous
M3 : Important conditinal check to ensure final LP token supply is greater than the MIN_BALANCE
is not performed inside the _reserveTokenSpecified
. Hence the respective invariant could be broken.
I initially started understanding the protocol by referring the documentation. I initially learnt the key workings of the EvolvingProteus
solution. Then I understood the key applications of the EvolvingProteus
solution such as the Dutch auctions
and Dynamic pools
.
Then I learnt about the key parameters of the solution related to prices, liquidity concentration and timestamps. Then I learnt how these parameters are interconnected through the time-weighted interpolation of prices, bonding curve equation related to the liquidity concentration and quadratice equation related to the utility calculations.
Then I analyzed the key invariants of the protocol, price range limitations and types of errors anticipated in the protocol.
After gaining enough knowledge about the protocol I started auditing the EvolvingProteus.sol
smartcontract (Inscope smartcontract). I initially focussed on the state changing functions. I analyzed whether it is possible to change the state of the contract to an invalid state via those functions.
Then I focused on key invariants of the contract. I tries to think of various methods and edge cases in breaking the given invariants. I was able to find several vulnerabilities related to breaking invariants in edge cases.
Later I checked whether all the required conditional checks and invariant checks are performed to ensure the contract states are within required boundaries after specific transactions are sucesfully executed.
By following the above mentioned approach I was able to find vulnerabilities of this project.
The architecture used in the EvolvingProteus
solution is a robust one. It is heavily dependent on mathematics. It uses a novel mehtod of evolving liquidity concentration across a predetermined time duration. A bonding curve equation is used to calculate the current liquidity concentration by taking into consideration the price of tokens (in the units of numeraire token) obtained via time-weighted interpolation of prices. The utility
is used as the measurement of value of the liquidity pool. The utility
is calculated based on a quadratic equation.
Considering the complexity of the algorithms associated with the protocol and interlinks among different components of the procotol such as pricing
, liquidity concentration
, utility
, it must be stated that the codebase quality is high and commendable.
It is recommended to focus more on invariant checks and verify that the key invariants are held after each swap, deposit or withdraw transaction is completed. It is also recommended to implement logic in the EvolvingProteus.sol
contract to ensrue key invariants are not broken via edge cases, becuase few vulnerabilities were found related to this during the audit.
Since there are many hard and soft invariants in this protocol it is recommended to focus more on foundry invariant tests to ensure the codebase is highly robust.
More attention can be paid on the Natspec
comments since there were numerous typos found in them which I have included in my QA report. Since developers and auditors rely on the Natspec
comments to get a general idea about the codebase, accurate Natspec
commnets will increase the quality of the codebase.
There were no centralization risks found within the EvolvingProteus.sol
smart contract. The external
functions of the contract were used for swaps
, deposits
and withdrawals
. They were not controlled via any access control
. And these external
functions were view
functions used for calculations.
Further the internal
functions used as helper functions for above external
functions were also view
functions and were not controlled by any access control.
Hence there was no risk of centralization since there were no admin controlled functions
or any role based access control within these functions of the EvolvingProteus.sol
smart contract.
The EvolvingProteus.sol
contract and the EvolvingProteus
solution is heavily dependent on mathematics. There are bonding curves, time-weighted interpolation and quadratic equations used for various calculations within its scope. The majority of calculations were based on fixed-point arithmetic using the ABDKMath64x64
library. Hence extra attention needed to be paid on the decimal precision and accuracy of resultant values of the calculations.
Since the EvolvingProteus
solution is similar to a hybrid solution of liquidity bootstrapping pools
and Uniswap V3
, the solution can be used for price discovery of newly created tokens and to execute large buy or sell order without significant price impact. The passive liqudity concentration
management of the solution can be used to allow the LPs
to earn more fees on thier provided liquidity since thier liquidity will be concentrated near the spot price of the asset continously.
As a result the uses cases of this protocol are on the Dutch Auctions
and Dynamic Pools
, which can make use of the above mentioned features of the protocol.
The systemic risks of this protocol are related to breaking of key invariants under edge cases. Possible errors related to calculations of fixed-point arithmetic and its precision and accuracy.
Missing checks to verify key invariants are holding after succesful execution of the deposit, withdraw and swap operations could lead to vulnerabilities.
This contract uses Fixed-point arithmatic. Hence the precision errors, rounding errors, underflow and overflow errors could occur. Hence need to pay extra attention on above areas when dealing with arithmatic operations in this contract
In Fixed-point arithmatic if you dedicate more bits to the fractional part for higher precision, you reduce the number of bits available for the integer part, limiting the range of whole numbers you can represent. Conversely, if you want to represent a larger range of whole numbers, you reduce the precision of the fractional part.
25 hours
#0 - c4-pre-sort
2023-08-31T05:54:12Z
0xRobocop marked the issue as high quality report
#1 - viraj124
2023-09-04T07:19:52Z
we have already responded to the findings separately so I'm analyzing the report excluding that and there are some good detailed insights and suggestions in
#2 - c4-sponsor
2023-09-04T07:19:57Z
viraj124 (sponsor) acknowledged
#3 - c4-judge
2023-09-11T20:22:30Z
JustDravee marked the issue as grade-a