Platform: Code4rena
Start Date: 26/09/2022
Pot Size: $50,000 USDC
Total HM: 13
Participants: 113
Period: 5 days
Judge: 0xean
Total Solo HM: 6
Id: 166
League: ETH
Rank: 5/113
Findings: 3
Award: $3,335.24
๐ Selected for report: 1
๐ Solo Findings: 1
๐ Selected for report: __141345__
3255.2724 USDC - $3,255.27
The evaluation of exp
function will be inaccurate, and further affect the accuracy of sigmoid()
function, eventually affect fee calculation.
Also the calculation takes quite a lot of gas to calculate to 8th term.
x/g
takes value between 0 to 6, but taylor expansion maintains good approximation only near 0. When x/g
is close to 5, the error is around 7% for exp()
and 7% forsigmoid()
respectively. When x/g
is close to 6, the error could go up to 15% for exp()
and 18% for sigmoid()
.
// src/core/contracts/libraries/AdaptiveFee.sol function exp( uint256 x, uint16 g, uint256 gHighestDegree ) internal pure returns (uint256 res) { // calculating: // g**8 + x * g**7 + (x**2 * g**6) / 2 + (x**3 * g**5) / 6 + (x**4 * g**4) / 24 + (x**5 * g**3) / 120 + (x**6 * g^2) / 720 + x**7 * g / 5040 + x**8 / 40320 // x**8 < 152 bits (19*8) and g**8 < 128 bits (8*16) // so each summand < 152 bits and res < 155 bits uint256 xLowestDegree = x; res = gHighestDegree; // g**8 gHighestDegree /= g; // g**7 res += xLowestDegree * gHighestDegree; gHighestDegree /= g; // g**6 xLowestDegree *= x; // x**2 res += (xLowestDegree * gHighestDegree) / 2; gHighestDegree /= g; // g**5 xLowestDegree *= x; // x**3 res += (xLowestDegree * gHighestDegree) / 6; gHighestDegree /= g; // g**4 xLowestDegree *= x; // x**4 res += (xLowestDegree * gHighestDegree) / 24; gHighestDegree /= g; // g**3 xLowestDegree *= x; // x**5 res += (xLowestDegree * gHighestDegree) / 120; gHighestDegree /= g; // g**2 xLowestDegree *= x; // x**6 res += (xLowestDegree * gHighestDegree) / 720; xLowestDegree *= x; // x**7 res += (xLowestDegree * g) / 5040 + (xLowestDegree * x) / (40320); }
Manual analysis.
The value of exp(1)
, exp(2)
, exp(3)
, exp(4)
, exp(5)
, exp(6)
can be pre-calculated and saved to constants, then be used as denominator or multiplier.
For example, to calculate exp(3.48)
, what we can do is calculate exp(0.48)
, and then multiply by exp(3)
.
When the power is less than 0.5, taylor expansion up to x^3
can give good accuracy. exp(0.5)
and corresponding sigmoid()
has maximum error in the order of 2e-4 to 1e-4.
#0 - vladyan18
2022-10-04T15:37:15Z
Thank you!
Indeed, the Taylor series converges fast enough only close to zero. Tests and practice show that the current degree of accuracy is satisfactory and retains the main property - monotonous growth from volatility. However, the recommendations for gas optimization and accuracy improvement are really good.
#1 - 0xean
2022-10-05T01:27:42Z
I am going to award this one based on the quality of the report and level of detail. While the error tolerance may be acceptable to the sponsor, I think the warden does a great job of demonstrating his point.
๐ Selected for report: 0xNazgul
Also found by: 0x1f8b, 0x52, 0xDecorativePineapple, 0xSmartContract, 0xmatt, Aeros, Aymen0909, Bnke0x0, Chom, CodingNameKiki, Deivitto, DimitarDimitrov, IllIllI, JC, Jeiwan, Lambda, Matin, Migue, Mukund, Ocean_Sky, Olivierdem, RaymondFam, RockingMiles, Rolezn, Ruhum, Satyam_Sharma, Shinchan, Tomo, Trabajo_de_mates, V_B, Waze, __141345__, a12jmx, ajtra, asutorufos, aysha, brgltd, bulej93, carrotsmuggler, catchup, cccz, chrisdior4, cryptonue, cryptphi, d3e4, defsec, delfin454000, durianSausage, erictee, fatherOfBlocks, gogo, kaden, karanctf, ladboy233, lukris02, mahdikarimi, martin, mics, natzuu, oyc_109, p_crypt0, pedr02b2, rbserver, reassor, rotcivegaf, rvierdiiev, sikorico, slowmoses, sorrynotsorry, tnevler, trustindistrust
55.8441 USDC - $55.84
When critical parameters of systems need to be changed, it is required to broadcast the change via event emission and recommended to enforce the changes after a time-delay. This is to allow system users to be aware of such critical changes and give them an opportunity to exit or adjust their engagement with the system accordingly. None of the onlyOwner functions that change critical protocol addresses/parameters have a timelock for a time-delayed change to alert: (1) users and give them a chance to engage/exit protocol if they are not agreeable to the changes (2) team in case of compromised owner(s) and give them a chance to perform incident response.
Instances number of this issue:
// src/core/contracts/AlgebraFactory.sol function setFarmingAddress(address _farmingAddress) external override onlyOwner {} function setVaultAddress(address _vaultAddress) external override onlyOwner {} function setBaseFeeConfiguration() external override onlyOwner {} // src/core/contracts/AlgebraPool.sol function setCommunityFee(uint8 communityFee0, uint8 communityFee1) external override lock onlyFactoryOwner {} function setIncentive(address virtualPoolAddress) external override {} function setLiquidityCooldown(uint32 newLiquidityCooldown) external override onlyFactoryOwner {}
Suggestion: Users may be surprised when critical parameters are changed. Furthermore, it can erode users' trust since they canโt be sure the protocol rules wonโt be changed later on. Compromised owner keys may be used to change protocol addresses/parameters to benefit attackers. Without a time-delay, authorized owners have no time for any planned incident response. (May be medium since owner can change critical parameters at anytime that can affect the users poorly).
Description: Lack of zero-address validation on address parameters may lead to transaction reverts, waste gas, require resubmission of transactions and may even force contract redeployments in certain cases within the protocol.
Suggestion: Consider adding explicit zero-address validation on input parameters of address type.
Instances number of this issue:
// src/core/contracts/AlgebraFactory.sol function setFarmingAddress(address _farmingAddress) external override onlyOwner { require(farmingAddress != _farmingAddress); emit FarmingAddress(_farmingAddress); farmingAddress = _farmingAddress; } function setVaultAddress(address _vaultAddress) external override onlyOwner { require(vaultAddress != _vaultAddress); emit VaultAddress(_vaultAddress); vaultAddress = _vaultAddress; } // src/core/contracts/AlgebraPool.sol function setIncentive(address virtualPoolAddress) external override { require(msg.sender == IAlgebraFactory(factory).farmingAddress()); activeIncentive = virtualPoolAddress; emit Incentive(virtualPoolAddress); }
Description: The best-practice layout for a contract should follow the following order: state variables, events, modifiers, constructor and functions. Function ordering helps readers identify which functions they can call and find constructor and fallback functions easier. Functions should be grouped according to their visibility and ordered as: constructor, receive function (if exists), fallback function (if exists), external, public, internal, private. Some constructs deviate from this recommended best-practice: Modifiers are in the middle of contracts. External/public functions are mixed with internal/private ones. Few events are declared in contracts while most others are in corresponding interfaces.
Suggestion: Consider adopting recommended best-practice for code structure and layout.
src/core/contracts/AlgebraFactory.sol
Description: Some functions are missing @notice/@dev NatSpec comments for the function, @param for all/some of their parameters and @return for return values. Given that NatSpec is an important part of code documentation, this affects code comprehension, auditability and usability.
Suggestion: Consider adding in full NatSpec comments for all functions to have complete code documentation for future use.
src/core/contracts/AlgebraFactory.sol src/core/contracts/AlgebraPool.sol
#0 - 0xean
2022-10-05T01:30:48Z
note to self: Warden submitted several, extremely high quality reports listed above as dupes that need to be accounted for awarding QA.
๐ Selected for report: IllIllI
Also found by: 0x1f8b, 0x5rings, 0xNazgul, 0xRoxas, 0xSmartContract, 0xbepresent, 0xmatt, Aeros, Amithuddar, Awesome, Aymen0909, B2, Bnke0x0, ChristianKuri, CodingNameKiki, Deivitto, Diraco, Fitraldys, HardlyCodeMan, JC, Mukund, Noah3o6, Olivierdem, RaymondFam, ReyAdmirado, RockingMiles, Rolezn, Ruhum, Saintcode_, Shinchan, SnowMan, TomJ, Tomio, Tomo, V_B, Waze, __141345__, ajtra, asutorufos, aysha, beardofginger, bobirichman, brgltd, bulej93, c3phas, ch0bu, cryptonue, defsec, delfin454000, dharma09, durianSausage, emrekocak, erictee, fatherOfBlocks, francoHacker, gianganhnguyen, gogo, imare, kaden, karanctf, ladboy233, lukris02, m_Rassska, martin, medikko, mics, natzuu, oyc_109, peiw, rbserver, ret2basic, rotcivegaf, saian, shark, slowmoses, tnevler, trustindistrust, zeesaw, zishansami
24.1343 USDC - $24.13
Some functions have return
statement even with named return variables.
Consider changing the variable to be an unnamed one.
Instances number of this issue: 20
// src/core/contracts/libraries/AdaptiveFee.sol function getFee() internal pure returns (uint16 fee) {} function sigmoid() internal pure returns (uint256 res) {} // src/core/contracts/AlgebraPool.sol function getInnerCumulatives() returns ( int56 innerTickCumulative, uint160 innerSecondsSpentPerLiquidity, uint32 innerSecondsSpent ) {} function getTimepoints() external view override returns ( int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulatives, uint112[] memory volatilityCumulatives, uint256[] memory volumePerAvgLiquiditys ) {} function _writeTimepoint() private returns (uint16 newTimepointIndex) {} function _getSingleTimepoint( uint32 blockTimestamp, uint32 secondsAgo, int24 startTick, uint16 timepointIndex, uint128 liquidityStart ) private view returns ( int56 tickCumulative, uint160 secondsPerLiquidityCumulative, uint112 volatilityCumulative, uint256 volumePerAvgLiquidity ) {} // src/core/contracts/DataStorageOperator.sol function getTimepoints() external view override onlyPool returns ( int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulatives, uint112[] memory volatilityCumulatives, uint256[] memory volumePerAvgLiquiditys ) {} function getAverages() external view override onlyPool returns (uint112 TWVolatilityAverage, uint256 TWVolumePerLiqAverage) {} function write() external override onlyPool returns (uint16 indexUpdated) {} function calculateVolumePerLiquidity() external pure override returns (uint128 volumePerLiquidity) {} function getFee() external view override onlyPool returns (uint16 fee) {} // src/core/contracts/libraries/DataStorage.sol function binarySearch() private view returns (Timepoint storage beforeOrAt, Timepoint storage atOrAfter) {} function getSingleTimepoint() internal view returns (Timepoint memory targetTimepoint) {} function getAverages() internal view returns (uint88 volatilityAverage, uint256 volumePerLiqAverage) {} // function getNewPriceAfterInput() internal pure returns (uint160 resultPrice) {} function getNewPriceAfterOutput() internal pure returns (uint160 resultPrice) {} function getNewPrice() internal pure returns (uint160 resultPrice) {} // src/core/contracts/libraries/TickManager.sol function cross() internal returns (int128 liquidityDelta) {} // src/core/contracts/libraries/TickTable.sol function getMostSignificantBit() internal pure returns (uint8 mostBitPos) {} function nextTickInTheSameRow() internal view returns (int24 nextTick, bool initialized) {}
Some function can directly return the results, no need to allocate for new variables.
Instances number of this issue: 4
// src/core/contracts/libraries/TokenDeltaMath.sol function getToken0Delta() internal pure returns (uint256 token0Delta) { // ... token0Delta = roundUp ? FullMath.divRoundingUp(FullMath.mulDivRoundingUp(priceDelta, liquidityShifted, priceUpper), priceLower) : FullMath.mulDiv(priceDelta, liquidityShifted, priceUpper) / priceLower; } function getToken1Delta() internal pure returns (uint256 token1Delta) { // ... token1Delta = roundUp ? FullMath.mulDivRoundingUp(priceDelta, liquidity, Constants.Q96) : FullMath.mulDiv(priceDelta, liquidity, Constants.Q96); } function getToken0Delta( uint160 priceLower, uint160 priceUpper, int128 liquidity ) internal pure returns (int256 token0Delta) { token0Delta = liquidity >= 0 ? getToken0Delta(priceLower, priceUpper, uint128(liquidity), true).toInt256() : -getToken0Delta(priceLower, priceUpper, uint128(-liquidity), false).toInt256(); } function getToken1Delta( uint160 priceLower, uint160 priceUpper, int128 liquidity ) internal pure returns (int256 token1Delta) { token1Delta = liquidity >= 0 ? getToken1Delta(priceLower, priceUpper, uint128(liquidity), true).toInt256() : -getToken1Delta(priceLower, priceUpper, uint128(-liquidity), false).toInt256(); }
array.length
should not be looked up in every loopeven for memory variables. The demo of the loop gas comparison can be seen here.
Instances number of this issue: 1
// src/core/contracts/libraries/DataStorage.sol for (uint256 i = 0; i < secondsAgos.length; i++) {}
If a variable is not set/initialized, it is assumed to have the default value (0 for uint, false for bool, address(0) for addressโฆ). Explicitly initializing it with its default value is an anti-pattern and wastes gas.
Instances number of this issue: 1
// src/core/contracts/libraries/DataStorage.sol for (uint256 i = 0; i < secondsAgos.length; i++) {}
The demo of the loop gas comparison can be seen here.
++i/i++
should be unchecked{++i}/unchecked{i++}
when it is not possible for them to overflow, as is the case when used in for- and while-loopsThis saves 30-40 gas per loop
Instances number of this issue: 1
// src/core/contracts/libraries/DataStorage.sol for (uint256 i = 0; i < secondsAgos.length; i++) {}
Each storage read uses opcode sload
which costs 100 gas (warm access), while memory read uses mload
which only cost 3 gas. (reference)
Even for a memory struct variable, the member variable access still has overhead. It can be saved further by caching the final result.
Instances number of this issue: 1
secondsAgos.length
can be cached.
contracts/VotingEscrow.sol
// src/core/contracts/libraries/DataStorage.sol 294-307: tickCumulatives = new int56[](secondsAgos.length); secondsPerLiquidityCumulatives = new uint160[](secondsAgos.length); volatilityCumulatives = new uint112[](secondsAgos.length); volumePerAvgLiquiditys = new uint256[](secondsAgos.length); // ... for (uint256 i = 0; i < secondsAgos.length; i++) {
require()
statements that use &&REQUIRE()
statements with multiple conditions can be split.
See this issue which describes the fact that there is a larger deployment gas cost, but with enough runtime calls, the change ends up being cheaper.
The demo of the gas comparison can be seen here.
Instances number of this issue: 6
// src/core/contracts/AlgebraFactory.sol 110: require(gamma1 != 0 && gamma2 != 0 && volumeGamma != 0, 'Gammas must be > 0'); // src/core/contracts/AlgebraPool.sol 739: require(limitSqrtPrice < currentPrice && limitSqrtPrice > TickMath.MIN_SQRT_RATIO, 'SPL'); 743: require(limitSqrtPrice > currentPrice && limitSqrtPrice < TickMath.MAX_SQRT_RATIO, 'SPL'); 953: require((communityFee0 <= Constants.MAX_COMMUNITY_FEE) && (communityFee1 <= Constants.MAX_COMMUNITY_FEE)); 968: require(newLiquidityCooldown <= Constants.MAX_LIQUIDITY_COOLDOWN && liquidityCooldown != newLiquidityCooldown); // src/core/contracts/DataStorageOperator.sol 46: require(_feeConfig.gamma1 != 0 && _feeConfig.gamma2 != 0 && _feeConfig.volumeGamma != 0, 'Gammas must be > 0');
Use a solidity version of at least 0.8.2 to get simple compiler automatic inlining Use a solidity version of at least 0.8.3 to get better struct packing and cheaper multiple storage reads Use a solidity version of at least 0.8.4 to get custom errors, which are cheaper at deployment than revert()/require() strings Use a solidity version of at least 0.8.10 to have external calls skip contract existence checks if the external call has a return value
require()
stringsCustom errors are available from solidity version 0.8.4. Custom errors save ~50 gas each time they're hit by avoiding having to allocate and store the revert string. Not defining the strings also save deployment gas
Instances number of this issue: 59
// src/core/contracts/AlgebraFactory.sol 43: require(msg.sender == owner); 60: require(tokenA != tokenB); 62: require(token0 != address(0)); 63: require(poolByPair[token0][token1] == address(0)); 78: require(owner != _owner); 85: require(farmingAddress != _farmingAddress); 92: require(vaultAddress != _vaultAddress); 109: require(uint256(alpha1) + uint256(alpha2) + uint256(baseFee) <= type(uint16).max, 'Max fee exceeded'); 110: require(gamma1 != 0 && gamma2 != 0 && volumeGamma != 0, 'Gammas must be > 0'); // src/core/contracts/AlgebraPool.sol 55: require(msg.sender == IAlgebraFactory(factory).owner()); 60: require(topTick < TickMath.MAX_TICK + 1, 'TUM'); 61: require(topTick > bottomTick, 'TLU'); 62: require(bottomTick > TickMath.MIN_TICK - 1, 'TLM'); 122: require(_lower.initialized); 134: require(_upper.initialized); 194: require(globalState.price == 0, 'AI'); 224: require(currentLiquidity > 0, 'NP'); // Do not recalculate the empty ranges 229: require((_blockTimestamp() - lastLiquidityAddTimestamp) >= _liquidityCooldown); 434: require(liquidityDesired > 0, 'IL'); 454: if (amount0 > 0) require((receivedAmount0 = balanceToken0() - receivedAmount0) > 0, 'IIAM'); 455: if (amount1 > 0) require((receivedAmount1 = balanceToken1() - receivedAmount1) > 0, 'IIAM'); 469: require(liquidityActual > 0, 'IIL2'); 474: require((amount0 = uint256(amount0Int)) <= receivedAmount0, 'IIAM2'); 475: require((amount1 = uint256(amount1Int)) <= receivedAmount1, 'IIAM2'); 608: require(balance0Before.add(uint256(amount0)) <= balanceToken0(), 'IIA'); 614: require(balance1Before.add(uint256(amount1)) <= balanceToken1(), 'IIA'); 636: require(globalState.unlocked, 'LOK'); 641: require((amountRequired = int256(balanceToken0().sub(balance0Before))) > 0, 'IIA'); 645: require((amountRequired = int256(balanceToken1().sub(balance1Before))) > 0, 'IIA'); 731: require(unlocked, 'LOK'); 733: require(amountRequired != 0, 'AS'); 739: require(limitSqrtPrice < currentPrice && limitSqrtPrice > TickMath.MIN_SQRT_RATIO, 'SPL'); 743: require(limitSqrtPrice > currentPrice && limitSqrtPrice < TickMath.MAX_SQRT_RATIO, 'SPL'); 898: require(_liquidity > 0, 'L'); 921: require(balance0Before.add(fee0) <= paid0, 'F0'); 935: require(balance1Before.add(fee1) <= paid1, 'F1'); 953: require((communityFee0 <= Constants.MAX_COMMUNITY_FEE) && (communityFee1 <= Constants.MAX_COMMUNITY_FEE)); 960: require(msg.sender == IAlgebraFactory(factory).farmingAddress()); 968: require(newLiquidityCooldown <= Constants.MAX_LIQUIDITY_COOLDOWN && liquidityCooldown != newLiquidityCooldown); // src/core/contracts/AlgebraPoolDeployer.sol 22: require(msg.sender == factory); 27: require(msg.sender == owner); 37: require(_factory != address(0)); 38: require(factory == address(0)); // src/core/contracts/DataStorageOperator.sol 27: require(msg.sender == pool, 'only pool can call this'); 43: require(msg.sender == factory || msg.sender == IAlgebraFactory(factory).owner()); 45: require(uint256(_feeConfig.alpha1) + uint256(_feeConfig.alpha2) + uint256(_feeConfig.baseFee) <= type(uint16).max, 'Max fee exceeded'); 46: require(_feeConfig.gamma1 != 0 && _feeConfig.gamma2 != 0 && _feeConfig.volumeGamma != 0, 'Gammas must be > 0'); // src/core/contracts/base/PoolState.sol 41: require(globalState.unlocked, 'LOK'); // src/core/contracts/libraries/DataStorage.sol 238: require(lteConsideringOverflow(self[oldestIndex].blockTimestamp, target, time), 'OLD'); 369: require(!self[0].initialized); // src/core/contracts/libraries/PriceMovementMath.sol 52: require(price > 0); 53: require(liquidity > 0); 70: require((product = amount * price) / amount == price); 71: require(liquidityShifted > product); 87: require(price > quotient); // src/core/contracts/libraries/TickManager.sol 96: require(liquidityTotalAfter < Constants.MAX_LIQUIDITY_PER_TICK + 1, 'LO'); // src/core/contracts/libraries/TickTable.sol 15: require(tick % Constants.TICK_SPACING == 0, 'tick is not spaced'); // src/core/contracts/libraries/TokenDeltaMath.sol 30: require(priceDelta < priceUpper); // forbids underflow and 0 priceLower 51: require(priceUpper >= priceLower);
If a function modifier such as onlyOwner()
is used, the function will revert if a normal user tries to pay the function. Marking the function as payable will lower the gas cost for legitimate callers because the compiler will not include checks for whether a payment was provided.
The extra opcodes avoided are
CALLVALUE(2), DUP1(3), ISZERO(3), PUSH2(3), JUMPI(10), PUSH1(3), DUP1(3), REVERT(0), JUMPDEST(1), POP(2)
which costs an average of about 21 gas per call to the function, in addition to the extra deployment cost.
Instances number of this issue: 14
// src/core/contracts/AlgebraFactory.sol 77: function setOwner(address _owner) external override onlyOwner { 84: function setFarmingAddress(address _farmingAddress) external override onlyOwner { 91: function setVaultAddress(address _vaultAddress) external override onlyOwner { 98: function setBaseFeeConfiguration() external override onlyOwner { // src/core/contracts/AlgebraPoolDeployer.sol 36: function setFactory(address _factory) external override onlyOwner { 44-49: function deploy() external override onlyFactory returns (address pool) { src/core/contracts/AlgebraPool.sol 952: function setCommunityFee(uint8 communityFee0, uint8 communityFee1) external override lock onlyFactoryOwner { 967: function setLiquidityCooldown(uint32 newLiquidityCooldown) external override onlyFactoryOwner { // src/core/contracts/DataStorageOperator.sol 37: function initialize(uint32 time, int24 tick) external override onlyPool { 53-69 function getSingleTimepoint() external view override onlyPool returns () 88-104 function getTimepoints() external view override onlyPool returns () 110-115 function getAverages() external view override onlyPool returns (uint112 TWVolatilityAverage, uint256 TWVolumePerLiqAverage) { 120-126 function write() external override onlyPool returns (uint16 indexUpdated) { 150-150 function getFee() external view override onlyPool returns (uint16 fee) {