Platform: Code4rena
Start Date: 16/01/2024
Pot Size: $80,000 USDC
Total HM: 37
Participants: 178
Period: 14 days
Judge: Picodes
Total Solo HM: 4
Id: 320
League: ETH
Rank: 150/178
Findings: 1
Award: $11.69
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: juancito
Also found by: 0x3b, 0xBinChook, 0xCiphky, 0xHelium, 0xMango, 0xOmer, 0xRobocop, 0xSmartContractSamurai, 0xWaitress, 0xbepresent, 0xpiken, 7ashraf, Arz, Audinarey, Banditx0x, Bauchibred, Draiakoo, IceBear, J4X, Jorgect, Kaysoft, KingNFT, Rhaydden, Rolezn, The-Seraphs, Tigerfrake, Topmark, Tripathi, Udsen, ZanyBonzy, a3yip6, b0g0, chaduke, codeslide, csanuragjain, dharma09, djxploit, eta, ether_sky, grearlake, inzinko, jasonxiale, jesjupyter, josephdara, lanrebayode77, linmiaomiao, lsaudit, niroh, oakcobalt, peanuts, pina, sivanesh_808, slvDev, t0x1c, vnavascues, wangxx2026
11.6897 USDC - $11.69
https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/staking/StakingConfig.sol#L341-L1006 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/dao/DAOConfig.sol#L641-L19710 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/pools/PoolsConfig.sol#L771-L10810 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/rewards/RewardsConfig.sol#L362-L10010 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/stable/StableConfig.sol#L472-L15210 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/price_feed/PriceAggregator.sol#L651-L964 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/pools/PoolMath.sol#L2101-L2283
Inconsistencies between the notices and actual code for range and adjustment functions in multiple contracts, including StakingConfig, DAOConfig, PoolsConfig, RewardsConfig, StableConfig, and PriceAggregator, are causing confusion and potential financial losses. This stems from unclear standards and variations in how ranges are adjusted, leading to discrepancies between expected and actual outcomes. Manual calculations are often required, hindering user experience and increasing the risk of errors. To address this, a clear, unified standard for range and adjustment is urgently needed in both code and whitebook, with consistent implementation across all affected contracts. To remain the standard of Range: x to x with an adjustment of x
, that is, adjusting the range with each adjustment iteration, including raising the upper limit and lowering the lower limit, one of the simplest approach is to replace >
and <
with >=
and <=
, respectively. In cases where 0
might be involved, adding +1
to the lower limit parameters can address this issue.
In the StakingConfig:: changeMaxUnstakeWeeks
, the notice of Range: 20 to 108 with an adjustment of 8
implies that the range for maxUnstakeWeeks is from 20 to 80, with an adjustment of 8, and the upper limit can be adjusted to +8, resulting in 116, and the lower limit can be adjusted to -8, resulting in 22. However, based on the conditional statements in the function, if (maxUnstakeWeeks < 108) maxUnstakeWeeks += 8
, and if (maxUnstakeWeeks > 20) maxUnstakeWeeks -= 1
, the actual upper limit for maxUnstakeWeeks
can be adjusted to 107 + 8 = 115, and the actual lower limit can be adjusted to 21 - 8 = 13. Consequently, the actual adjusted range in this function is from 13 to 115, but the notice indicates a adjusted range of 12 to 116, which means the difference of upper limit is 115 - 116 = -1 while the difference of lower limit is 13 - 12 = +1, making the standard of range and adjustment inconsistent and confused.
Similar inconsistencies exist in the changeMinUnstakePercent
and changeModificationCooldown
functions of the contract. The notice for these functions suggests adjusted ranges of 5 to 55, 6 hours 15 minutes to 0 minutes, respectively. However, the actual adjusted ranges in the functions are 6 to 54, 6 hours 14 minutes to 1 minute, respectively.
Furthermore, in the changeMinUnstakeWeeks
function, there is a case where the notice's adjusted range matches the original range: the notice range is 1 to 12, and the adjusted range after modification is 0 to 13. However, the function' adjusted range remains 1 to 12, similar to its notice unadjusted range, according to if (minUnstakeWeeks < 12) minUnstakeWeeks += 1
and if (minUnstakeWeeks > 1) minUnstakeWeeks -= 1
. This may lead users to misunderstand that the standard is that within the range, there is adjustment, rather than the standard of adjusting the range with each adjustment iteration, including raising the upper limit and lowering the lower limit.
Such inconsistencies between the notice and functions in range and adjustment can lead to confusion. Users may be uncertain about the standards for range and adjustment—whether it is the the standard A, for each adjustment within the range, the upper limit can be increased and the lower limit can be decreased, or whether it is the standard B, within the range, each adjustment is made. Even with the standard A, it is not intuitive and has variations, requiring manual calculations each time. Additionally, such inconsistencies are present in other contracts related to configuration, including DAOConfig.sol, PoolsConfig.sol, RewardsConfig.sol, StableConfig.sol, and PriceAggregator.sol. If calculations are required each time, it can negatively impact the user experience that the calculation errors mentioned can potentially lead to financial losses.
DAOConfig PoolsConfig RewardsConfig StableConfig PriceAggregator
Therefore, in both the code and the whitepaper's parameters section, a clear, simple and unified standard for range and adjustment is needed. This would facilitate users' understanding and usage of these parameters, ultimately enhancing the user experience and finance. To remain the standard A of adjusting the range with each adjustment iteration, including raising the upper limit and lowering the lower limit, one of the simplest approach is to replace >
and <
with >=
and <=
, respectively. In cases where 0
might be involved, adding +1
to the lower limit parameters can address this issue. This adjustment can be applied consistently across all the six contracts, as shown in the example of StakingConfig
contract below:
// Range: 2 to 12 with an adjustment of 1 function changeMinUnstakeWeeks(bool increase) external onlyOwner { if (increase) { - if (minUnstakeWeeks < 12) minUnstakeWeeks += 1; + if (minUnstakeWeeks <= 12) minUnstakeWeeks += 1; } else { - if (minUnstakeWeeks > 1) minUnstakeWeeks -= 1; // @audit: To prevent the occurrence of 0 values, add 1 to the lower limit. + if (minUnstakeWeeks >= 2) minUnstakeWeeks -= 1; } emit MinUnstakeWeeksChanged(minUnstakeWeeks); } // Range: 20 to 108 with an adjustment of 8 function changeMaxUnstakeWeeks(bool increase) external onlyOwner { if (increase) { - if (maxUnstakeWeeks < 108) maxUnstakeWeeks += 8; + if (maxUnstakeWeeks <= 108) maxUnstakeWeeks += 8; } else { - if (maxUnstakeWeeks > 20) maxUnstakeWeeks -= 8; + if (maxUnstakeWeeks >= 20) maxUnstakeWeeks -= 8; } emit MaxUnstakeWeeksChanged(maxUnstakeWeeks); } // Range: 10 to 50 with an adjustment of 5 function changeMinUnstakePercent(bool increase) external onlyOwner { if (increase) { - if (minUnstakePercent < 50) minUnstakePercent += 5; + if (minUnstakePercent <= 50) minUnstakePercent += 5; } else { - if (minUnstakePercent > 10) minUnstakePercent -= 5; + if (minUnstakePercent >= 10) minUnstakePercent -= 5; } emit MinUnstakePercentChanged(minUnstakePercent); } // Range: 16 minutes to 6 hours with an adjustment of 15 minutes function changeModificationCooldown(bool increase) external onlyOwner { if (increase) { - if (modificationCooldown < 6 hours) modificationCooldown += 15 minutes; + if (modificationCooldown <= 6 hours) modificationCooldown += 15 minutes; } else { - if (modificationCooldown > 15 minutes) modificationCooldown -= 15 minutes; // @audit To prevent the occurrence of 0 values, add 1 to the lower limit. + if (modificationCooldown >= 16 minutes) modificationCooldown -= 15 minutes; } emit ModificationCooldownChanged(modificationCooldown); }
Manual Review
To remain the standard of Range: x to x with an adjustment of x
, that is, adjusting the range with each adjustment iteration, including raising the upper limit and lowering the lower limit, one of the simplest approach is to replace >
and <
with >=
and <=
, respectively. In cases where 0
might be involved, adding +1
to the lower limit parameters can address this issue as shown in Proof of Concept.
PoolMath:: _determineZapSwapAmount
Omitting the condition zapAmountA * reserveB == reserveA * zapAmountB
in the _determineZapSwapAmount
function would result in incorrect swap calculations when the ratios of zapAmountA/zapAmountB and reserveA/reserveB are equal.
if the condition zapAmountA * reserveB == reserveA * zapAmountB
was not considered in the _determineZapSwapAmount function
in the PoolMath
contract.
Without this condition, the function would fail to handle cases where the condition zapAmountA * reserveB and reserveA * zapAmountB are exactly equal, which would lead to incorrect swap amounts.
The function's logic relies on the two if statements to determine which asset to swap based on the condition. However, if it is equal, neither if condition would be met, and the function would likely return incorrect swap amounts, potentially leading to unintended financial consequences.
Here's a recommendation to change the code to address the missing condition
PoolMath::_determineZapSwapAmount
function _determineZapSwapAmount( uint256 reserveA, uint256 reserveB, uint256 zapAmountA, uint256 zapAmountB ) internal pure returns (uint256 swapAmountA, uint256 swapAmountB ) { // zapAmountA / zapAmountB exceeds the ratio of reserveA / reserveB? - meaning too much zapAmountA - if ( zapAmountA * reserveB > reserveA * zapAmountB ) + if ( zapAmountA * reserveB >= reserveA * zapAmountB ) (swapAmountA, swapAmountB) = (_zapSwapAmount( reserveA, reserveB, zapAmountA, zapAmountB ), 0); // zapAmountA / zapAmountB is less than the ratio of reserveA / reserveB? - meaning too much zapAmountB if ( zapAmountA * reserveB < reserveA * zapAmountB ) (swapAmountA, swapAmountB) = (0, _zapSwapAmount( reserveB, reserveA, zapAmountB, zapAmountA )); // Ensure we are not swapping more than was specified for zapping if ( ( swapAmountA > zapAmountA ) || ( swapAmountB > zapAmountB ) ) return (0, 0); return (swapAmountA, swapAmountB); } }
Manual Review
It is recommended to add the equal condition to the if statement as shown in the Proof of Concept:
- if ( zapAmountA * reserveB > reserveA * zapAmountB ) + if ( zapAmountA * reserveB >= reserveA * zapAmountB )
Math
#0 - c4-judge
2024-02-03T09:16:02Z
Picodes changed the severity to QA (Quality Assurance)
#1 - Picodes
2024-02-03T09:16:51Z
From C4's rules: 'function incorrect as to spec, issues with comments' is Low
#2 - c4-judge
2024-02-03T14:15:28Z
Picodes marked the issue as grade-b