Platform: Code4rena
Start Date: 30/05/2023
Pot Size: $300,500 USDC
Total HM: 79
Participants: 101
Period: about 1 month
Judge: Trust
Total Solo HM: 36
Id: 242
League: ETH
Rank: 21/101
Findings: 3
Award: $2,992.34
🌟 Selected for report: 2
🚀 Solo Findings: 1
🌟 Selected for report: AlexCzm
Also found by: T1MOH, los_chicos, said
1040.1433 USDC - $1,040.14
https://github.com/Maia-DAO/maia-ecosystem-monorepo/blob/2f6e87348877684aa0c12aec204fea210cfbe6eb/src/scope/talos/base/TalosBaseStrategy.sol#L99-L147 https://github.com/Maia-DAO/maia-ecosystem-monorepo/blob/2f6e87348877684aa0c12aec204fea210cfbe6eb/src/scope/talos/base/TalosBaseStrategy.sol#L419-L425
checkDeviation
modifier purpose is to add slippage protection for increase/decrease liquidity operations. It's applied to deposit/redeem
, rerange/rebalance
but init()
is missing it.
There is no slippage protection on init()
.
In the init()
function of TalosBaseStrategy, the following actions are performed: an initial deposit is made, a tokenId and shares are minted.
The _nonfungiblePositionManager.mint()
function is called with hardcoded values of amount0Min
and amount1Min
, both set to 0. Additionally, it should be noted that the init()
function does not utilize the checkDeviation
modifier, which was specifically designed to safeguard users against slippage.
function init(uint256 amount0Desired, uint256 amount1Desired, address receiver) external virtual nonReentrant returns (uint256 shares, uint256 amount0, uint256 amount1) { ... (_tokenId, _liquidity, amount0, amount1) = _nonfungiblePositionManager.mint( INonfungiblePositionManager.MintParams({ token0: address(_token0), token1: address(_token1), fee: poolFee, tickLower: tickLower, tickUpper: tickUpper, amount0Desired: amount0Desired, amount1Desired: amount1Desired, amount0Min: 0, amount1Min: 0, recipient: address(this), deadline: block.timestamp }) ); ...
/// @notice Function modifier that checks if price has not moved a lot recently. /// This mitigates price manipulation during rebalance and also prevents placing orders when it's too volatile. modifier checkDeviation() { ITalosOptimizer _optimizer = optimizer; pool.checkDeviation(_optimizer.maxTwapDeviation(), _optimizer.twapDuration()); _; }
VS Code, uniswapv3book
Apply checkDeviation
to init()
function.
Other
#0 - c4-judge
2023-07-11T09:46:30Z
trust1995 marked the issue as duplicate of #271
#1 - c4-judge
2023-07-11T09:46:46Z
trust1995 marked the issue as satisfactory
#2 - c4-judge
2023-07-11T17:15:04Z
trust1995 changed the severity to 3 (High Risk)
#3 - c4-judge
2023-07-25T13:40:05Z
trust1995 marked the issue as selected for report
#4 - c4-sponsor
2023-07-28T13:15:45Z
0xLightt marked the issue as sponsor confirmed
#5 - 0xLightt
2023-09-06T17:31:12Z
🌟 Selected for report: AlexCzm
1712.1701 USDC - $1,712.17
https://github.com/code-423n4/2023-05-maia/blob/54a45beb1428d85999da3f721f923cbf36ee3d35/src/erc-20/ERC20Boost.sol#L150-L172 https://github.com/code-423n4/2023-05-maia/blob/54a45beb1428d85999da3f721f923cbf36ee3d35/src/erc-20/ERC20Boost.sol#L80-L83 https://github.com/code-423n4/2023-05-maia/blob/54a45beb1428d85999da3f721f923cbf36ee3d35/src/erc-20/ERC20Boost.sol#L336-L344
When a user boosted gauge became deprecated, the user can transfer his boost tokens. When the same gauge is reintroduced to the active gauge list, the user will boost it again even if his boost token balance is zero.
Same amount of boost tokens can be allocated to gauges by multiple addresses.
Let's take an example:
attach()
from gaugeA to boost it;getUserBoost[alice]
is set to balanceOf(alice)Owner removes gaugeA and it's added to _deprecatedGauges
;
Alice calls updateUserBoost()
; because gaugeA is now deprecated her allocated boost is set to userBoost
which is initialized to zero (0) :
function updateUserBoost(address user) external { uint256 userBoost = 0; address[] memory gaugeList = _userGauges[user].values(); uint256 length = gaugeList.length; for (uint256 i = 0; i < length;) { address gauge = gaugeList[i]; if (!_deprecatedGauges.contains(gauge)) { uint256 gaugeBoost = getUserGaugeBoost[user][gauge].userGaugeBoost; if (userBoost < gaugeBoost) userBoost = gaugeBoost; } unchecked { i++; } } getUserBoost[user] = userBoost; emit UpdateUserBoost(user, userBoost); }
freeGaugeBoost()
returns the amount of unallocated boost tokens:function freeGaugeBoost(address user) public view returns (uint256) { return balanceOf[user] - getUserBoost[user]; }
transfer()
has notAttached()
modifier that ensures the transferred amount is free (not allocated to any gauge);/** * @notice Transfers `amount` of tokens from `msg.sender` to `to` address. * @dev User must have enough free boost. * @param to the address to transfer to. * @param amount the amount to transfer. */ function transfer(address to, uint256 amount) public override notAttached(msg.sender, amount) returns (bool) { return super.transfer(to, amount); }
Alice transfer her tokens
When gaugeA is added back, addGauge(gaugeA)
she will continue to boost gaugeA even if her balance is 0
VS Code
One solution is updateUserBoost()
to loop all gauges (active and deprecated) not only the active ones:
function updateUserBoost(address user) external { uint256 userBoost = 0; address[] memory gaugeList = _userGauges[user].values(); uint256 length = gaugeList.length; for (uint256 i = 0; i < length;) { address gauge = gaugeList[i]; uint256 gaugeBoost = getUserGaugeBoost[user][gauge].userGaugeBoost; if (userBoost < gaugeBoost) userBoost = gaugeBoost; unchecked { i++; } } getUserBoost[user] = userBoost; emit UpdateUserBoost(user, userBoost); }
Even the updateUserBoost()
comments indicates all _userGauges
should be iterated over.
/** * @notice Update geUserBoost for a user, loop through all _userGauges * @param user the user to update the boost for. */ function updateUserBoost(address user) external;
Other
#0 - c4-judge
2023-07-11T09:41:43Z
trust1995 marked the issue as primary issue
#1 - c4-judge
2023-07-11T09:41:48Z
trust1995 marked the issue as satisfactory
#2 - c4-sponsor
2023-07-12T17:27:04Z
0xLightt marked the issue as sponsor confirmed
#3 - c4-judge
2023-07-25T13:07:47Z
trust1995 marked the issue as selected for report
#4 - 0xLightt
2023-09-06T17:31:25Z
240.0331 USDC - $240.03
Free weight is calculated as userFreeWeight
(already free weights) plus weights freed from non-deprecated gauges. Non-deprecated gauges condition is not required and leads to freeing weight from more gauges than needed.
* @notice A greedy algorithm for freeing weight before a token burn/transfer * @dev Frees up entire gauges, so likely will free more than `weight` * @param user the user to free weight for * @param weight the weight to free */ function _decrementWeightUntilFree(address user, uint256 weight) internal nonReentrant { uint256 userFreeWeight = freeVotes(user) + userUnusedVotes(user); // early return if already free if (userFreeWeight >= weight) return; uint32 currentCycle = _getGaugeCycleEnd(); // cache totals for batch updates uint112 userFreed; uint112 totalFreed; // Loop through all user gauges, live and deprecated address[] memory gaugeList = _userGauges[user].values(); // Free gauges through the entire list or until underweight uint256 size = gaugeList.length; //@audit use userFreed instead of totalFreed for (uint256 i = 0; i < size && (userFreeWeight + totalFreed) < weight;) { address gauge = gaugeList[i]; uint112 userGaugeWeight = getUserGaugeWeight[user][gauge]; if (userGaugeWeight != 0) { // If the gauge is live (not deprecated), include its weight in the total to remove if (!_deprecatedGauges.contains(gauge)) { totalFreed += userGaugeWeight; } userFreed += userGaugeWeight; _decrementGaugeWeight(user, gauge, userGaugeWeight, currentCycle); unchecked { i++; } } } getUserWeight[user] -= userFreed; _writeGaugeWeight(_totalWeight, _subtract112, totalFreed, currentCycle); }
More gauges than necessary are freed means the user accumulate rewards from fewer gauges.
Let's suppose Alice has 3 weights in total and allocate them to 3 gauges: A, B and C, where gauge A is deprecated.
_decrementWeightUntilFree(alice, weight = 2)
userFreeWeight
= 0totalFreed
= 0, userFreed
= 1userFreeWeight
+ totalFreed
) < weight
, continue to free next gaugetotalFreed
= 1, userFreed
= 2userFreeWeight
+ totalFreed
) < weight, continue to free next gaugetotalFreed
= 2, userFreed
= 3Now consider following case:
_decrementWeightUntilFree(alice, 1)
userFreeWeight
= 0totalFreed
= 0, userFreed
= 1userFreeWeight
+ totalFreed
) < weight
, continue to free next gaugetotalFreed
= 1, userFreed
= 2userFreeWeight
+ totalFreed
) >= weight
, break_decrementWeightUntilFree(alice, weight = 2)
userFreeWeight
= 1totalFreed
= 1, userFreed
= 1userFreeWeight
+ totalFreed
) >= weight, breakVS Code
The freed weight should be considered as weight released from all gauges, not just limited to the non-deprecated ones.
// Free gauges through the entire list or until underweight uint256 size = gaugeList.length; for ( uint256 i = 0; i < size && (userFreeWeight + userFreed) < weight; ) { ... }
Other
#0 - c4-judge
2023-07-11T09:44:15Z
trust1995 marked the issue as duplicate of #477
#1 - c4-judge
2023-07-11T09:44:29Z
trust1995 marked the issue as satisfactory