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: 4/101
Findings: 18
Award: $15,783.00
π Selected for report: 7
π Solo Findings: 3
748.9032 USDC - $748.90
Logic error
setWeight()
Used to set the new weight
The code is as follows:
function setWeight(uint256 poolId, uint8 weight) external nonReentrant onlyOwner { if (weight == 0) revert InvalidWeight(); uint256 poolIndex = destinations[poolId]; if (poolIndex == 0) revert NotUlyssesLP(); uint256 oldRebalancingFee; for (uint256 i = 1; i < bandwidthStateList.length; i++) { uint256 targetBandwidth = totalSupply.mulDiv(bandwidthStateList[i].weight, totalWeights); oldRebalancingFee += _calculateRebalancingFee(bandwidthStateList[i].bandwidth, targetBandwidth, false); } uint256 oldTotalWeights = totalWeights; uint256 weightsWithoutPool = oldTotalWeights - bandwidthStateList[poolIndex].weight; uint256 newTotalWeights = weightsWithoutPool + weight; totalWeights = newTotalWeights; if (totalWeights > MAX_TOTAL_WEIGHT || oldTotalWeights == newTotalWeights) { revert InvalidWeight(); } uint256 leftOverBandwidth; BandwidthState storage poolState = bandwidthStateList[poolIndex]; poolState.weight = weight; @> if (oldTotalWeights > newTotalWeights) { for (uint256 i = 1; i < bandwidthStateList.length;) { if (i != poolIndex) { uint256 oldBandwidth = bandwidthStateList[i].bandwidth; if (oldBandwidth > 0) { bandwidthStateList[i].bandwidth = oldBandwidth.mulDivUp(oldTotalWeights, newTotalWeights).toUint248(); leftOverBandwidth += oldBandwidth - bandwidthStateList[i].bandwidth; } } unchecked { ++i; } } poolState.bandwidth += leftOverBandwidth.toUint248(); } else { uint256 oldBandwidth = poolState.bandwidth; if (oldBandwidth > 0) { @> poolState.bandwidth = oldBandwidth.mulDivUp(oldTotalWeights, newTotalWeights).toUint248(); leftOverBandwidth += oldBandwidth - poolState.bandwidth; } for (uint256 i = 1; i < bandwidthStateList.length;) { if (i != poolIndex) { if (i == bandwidthStateList.length - 1) { @> bandwidthStateList[i].bandwidth += leftOverBandwidth.toUint248(); } else if (leftOverBandwidth > 0) { @> bandwidthStateList[i].bandwidth += @> leftOverBandwidth.mulDiv(bandwidthStateList[i].weight, weightsWithoutPool).toUint248(); } } unchecked { ++i; } } }
There are several problems with the above code
if (oldTotalWeights > newTotalWeights)
should be changed to if (oldTotalWeights < newTotalWeights)
because the logic inside the if is to calculate the case of increasing weight
poolState.bandwidth = oldBandwidth.mulDivUp(oldTotalWeights , newTotalWeights).toUint248();
should be modified to poolState.bandwidth = oldBandwidth.mulDivUp(newTotalWeights, oldTotalWeights).toUint248();
Because this calculates the extra number
leftOverBandwidth
has a problem with the processing logic
function setWeight(uint256 poolId, uint8 weight) external nonReentrant onlyOwner { ... - if (oldTotalWeights > newTotalWeights) { + if (oldTotalWeights < newTotalWeights) { for (uint256 i = 1; i < bandwidthStateList.length;) { if (i != poolIndex) { uint256 oldBandwidth = bandwidthStateList[i].bandwidth; if (oldBandwidth > 0) { bandwidthStateList[i].bandwidth = oldBandwidth.mulDivUp(oldTotalWeights, newTotalWeights).toUint248(); leftOverBandwidth += oldBandwidth - bandwidthStateList[i].bandwidth; } } unchecked { ++i; } } poolState.bandwidth += leftOverBandwidth.toUint248(); } else { uint256 oldBandwidth = poolState.bandwidth; if (oldBandwidth > 0) { - poolState.bandwidth = oldBandwidth.mulDivUp(oldTotalWeights, newTotalWeights).toUint248(); + poolState.bandwidth = oldBandwidth.mulDivUp(newTotalWeights, oldTotalWeights).toUint248(); leftOverBandwidth += oldBandwidth - poolState.bandwidth; } + uint256 currentGiveWidth = 0; + uint256 currentGiveCount = 0; for (uint256 i = 1; i < bandwidthStateList.length;) { + if (i != poolIndex) { + if(currentGiveCount == bandwidthStateList.length - 2 - 1) { //last + bandwidthStateList[i].bandwidth += leftOverBandwidth - currentGiveWidth; + } + uint256 sharesWidth = leftOverBandwidth.mulDiv(bandwidthStateList[i].weight, weightsWithoutPool).toUint248(); + bandwidthStateList[i].bandwidth += sharesWidth; + currentGiveWidth +=sharesWidth; + currentCount++; + } - if (i != poolIndex) { - if (i == bandwidthStateList.length - 1) { - bandwidthStateList[i].bandwidth += leftOverBandwidth.toUint248(); - } else if (leftOverBandwidth > 0) { - bandwidthStateList[i].bandwidth += - leftOverBandwidth.mulDiv(bandwidthStateList[i].weight, weightsWithoutPool).toUint248(); - } - } unchecked { ++i; } } } ...
Context
#0 - c4-judge
2023-07-09T17:00:14Z
trust1995 marked the issue as primary issue
#1 - c4-judge
2023-07-11T11:47:16Z
trust1995 marked the issue as satisfactory
#2 - c4-sponsor
2023-07-12T18:23:22Z
0xLightt marked the issue as sponsor confirmed
#3 - c4-judge
2023-07-25T11:26:06Z
trust1995 marked the issue as selected for report
#4 - c4-judge
2023-07-27T11:04:16Z
trust1995 changed the severity to 3 (High Risk)
#5 - 0xLightt
2023-07-28T13:05:00Z
We recognize the audit's findings on Ulysses AMM. These will not be rectified due to the upcoming migration of this section to Balancer Stable Pools.
π Selected for report: peakbolt
Also found by: 0xStalin, 0xTheC0der, BPZ, LokiThe5th, RED-LOTUS-REACH, adeolu, bin2chen, jasonxiale, kodyvim, kutugu, ltyu, ubermensch
47.6891 USDC - $47.69
Wrong decimal place conversion, resulting in wrong quantity
in callOutSignedAndBridge()
The number of tokens will be converted to 18 decimal when packedData
is performed.
function callOutSignedAndBridge(bytes calldata _params, DepositInput memory _dParams, uint128 _remoteExecutionGas) external payable lock requiresFallbackGas { //Encode Data for cross-chain call. bytes memory packedData = abi.encodePacked( bytes1(0x05), msg.sender, depositNonce, _dParams.hToken, _dParams.token, _dParams.amount, @> _normalizeDecimals(_dParams.deposit, ERC20(_dParams.token).decimals()), _dParams.toChain, _params, msg.value.toUint128(), _remoteExecutionGas );
But the method of converting to 18 decimal is wrong
/** * @notice Internal function that normalizes an input to 18 decimal places. * @param _amount amount of tokens * @param _decimals number of decimal places */ function _normalizeDecimals(uint256 _amount, uint8 _decimals) internal pure returns (uint256) { @> return _decimals == 18 ? _amount : _amount * (10 ** _decimals) / 1 ether; }
The correct should be _amount * 1 ether / (10 ** _decimals)
Many places have problems using this method
Note: BranchPort._denormalizeDecimals()
Have similar questions
/** * @notice Internal function that normalizes an input to 18 decimal places. * @param _amount amount of tokens * @param _decimals number of decimal places */ function _normalizeDecimals(uint256 _amount, uint8 _decimals) internal pure returns (uint256) { - return _decimals == 18 ? _amount : _amount * (10 ** _decimals) / 1 ether; + return _decimals == 18 ? _amount : _amount * 1 ether / (10 ** _decimals); }
Context
#0 - c4-judge
2023-07-11T13:40:43Z
trust1995 marked the issue as duplicate of #758
#1 - c4-judge
2023-07-11T13:40:48Z
trust1995 marked the issue as satisfactory
#2 - c4-judge
2023-07-28T11:11:25Z
trust1995 marked the issue as partial-50
π Selected for report: peakbolt
Also found by: 0xStalin, 0xTheC0der, BPZ, LokiThe5th, RED-LOTUS-REACH, adeolu, bin2chen, jasonxiale, kodyvim, kutugu, ltyu, ubermensch
47.6891 USDC - $47.69
Wrong decimal place conversion, resulting in wrong quantity
in _createDepositSingle()
will call IPort(localPortAddress).bridgeOut()
The parameter _deposit
is not converted to 18 decimal
_createDepositSingle()
function _createDepositSingle( address _user, address _hToken, address _token, uint256 _amount, uint256 _deposit, uint128 _gasToBridgeOut ) internal { //Deposit / Lock Tokens into Port @> IPort(localPortAddress).bridgeOut(_user, _hToken, _token, _amount, _deposit); //Deposit Gas to Port _depositGas(_gasToBridgeOut); ...
but in IPort(localPortAddress).bridgeOut()
method οΌwill do _denormalizeDecimals
on deposit
from 18 decimal to token.decimals
function bridgeOut( address _depositor, address _localAddress, address _underlyingAddress, uint256 _amount, uint256 _deposit ) external virtual requiresBridgeAgent { if (_amount - _deposit > 0) { _localAddress.safeTransferFrom(_depositor, address(this), _amount - _deposit); ERC20hTokenBranch(_localAddress).burn(_amount - _deposit); } if (_deposit > 0) { _underlyingAddress.safeTransferFrom( @> _depositor, address(this), _denormalizeDecimals(_deposit, ERC20(_underlyingAddress).decimals()) ); } }
So we need to convert deposit
to 18 decimal before calling bridgeOut
Note: _createDepositMultiple
have similar questions
function _createDepositSingle( address _user, address _hToken, address _token, uint256 _amount, uint256 _deposit, uint128 _gasToBridgeOut ) internal { //Deposit / Lock Tokens into Port - IPort(localPortAddress).bridgeOut(_user, _hToken, _token, _amount, _deposit); + IPort(localPortAddress).bridgeOut(_user, _hToken, _token, _amount, _normalizeDecimals(_deposit)); //Deposit Gas to Port _depositGas(_gasToBridgeOut); ...
Context
#0 - c4-judge
2023-07-11T13:49:59Z
trust1995 marked the issue as duplicate of #758
#1 - c4-judge
2023-07-11T13:50:04Z
trust1995 marked the issue as satisfactory
#2 - c4-judge
2023-07-28T11:14:45Z
trust1995 marked the issue as partial-50
π Selected for report: bin2chen
Also found by: lukejohn, tsvetanovv
1540.9531 USDC - $1,540.95
claimReward()
will take all rewards if the amountRequested
passed in is 0, which may result in user's rewards lost
In BoostAggregator.withdrawProtocolFees()
, the owner can take the protocol rewards protocolRewards
The code is as follows:
function withdrawProtocolFees(address to) external onlyOwner { uniswapV3Staker.claimReward(to, protocolRewards); @> delete protocolRewards; }
From the above code we can see that uniswapV3Staker
is called to fetch and then clear protocolRewards
Let's look at the implementation of uniswapV3Staker.claimReward()
.
contract UniswapV3Staker is IUniswapV3Staker, Multicallable { .... function claimReward(address to, uint256 amountRequested) external returns (uint256 reward) { reward = rewards[msg.sender]; @> if (amountRequested != 0 && amountRequested < reward) { reward = amountRequested; rewards[msg.sender] -= reward; } else { rewards[msg.sender] = 0; } if (reward > 0) hermes.safeTransfer(to, reward); emit RewardClaimed(to, reward); }
The current implementation is that if the amountRequested==0
passed in means that all rewards[msg.sender]
of this msg.sender
are taken
This leads to problems.
owner
calls withdrawProtocolFees()
twice in a row, it will definitely take all the rewards
of the BoostAggregator
.withdrawProtocolFees()
was called when protocolRewards==0
As a result, the rewards that belong to users in BoostAggregator
are lost
Modify claimReward()
to remove amountRequested != 0
contract UniswapV3Staker is IUniswapV3Staker, Multicallable { .... function claimReward(address to, uint256 amountRequested) external returns (uint256 reward) { reward = rewards[msg.sender]; - if (amountRequested != 0 && amountRequested < reward) { + if (amountRequested < reward) { reward = amountRequested; rewards[msg.sender] -= reward; } else { rewards[msg.sender] = 0; } if (reward > 0) hermes.safeTransfer(to, reward); emit RewardClaimed(to, reward); }
Context
#0 - c4-judge
2023-07-10T14:32:43Z
trust1995 marked the issue as duplicate of #139
#1 - c4-judge
2023-07-10T14:32:54Z
trust1995 marked the issue as satisfactory
#2 - c4-judge
2023-07-11T17:25:20Z
trust1995 marked the issue as not a duplicate
#3 - c4-judge
2023-07-11T17:25:25Z
trust1995 marked the issue as primary issue
#4 - c4-sponsor
2023-07-12T17:50:41Z
0xLightt marked the issue as sponsor confirmed
#5 - c4-judge
2023-07-25T13:01:06Z
trust1995 marked the issue as selected for report
#6 - 0xLightt
2023-07-28T15:43:35Z
We prefer to leave the original's UniswapV3Staker
claim logic intact and have the BoostAggregator
not allow the owner or stakers claim 0 rewards.
#7 - 0xLightt
2023-09-06T17:25:46Z
π Selected for report: bin2chen
5707.2337 USDC - $5,707.23
Wrong owner parameter, causing users to lose rewards
In TalosStrategyStaked.sol
If the user's shares
have changed, we need to do flywheel.accrue()
first, which will accrue rewards
and update the corresponding userIndex
.
This way we can ensure the accuracy of rewards
.
So we will call flywheel.accrue()
beforeDeposit/beforeRedeem/transfer etc.
Take redeem()
as an example, the code is as follows:
contract TalosStrategyStaked is TalosStrategySimple, ITalosStrategyStaked { ... function beforeRedeem(uint256 _tokenId, address _owner) internal override { _earnFees(_tokenId); @> flywheel.accrue(_owner); }
But when beforeRedeem()
is called with the wrong owner passed in, the redeem()
code is as follows:
function redeem(uint256 shares, uint256 amount0Min, uint256 amount1Min, address receiver, address _owner) public virtual override nonReentrant checkDeviation returns (uint256 amount0, uint256 amount1) { ... if (msg.sender != _owner) { uint256 allowed = allowance[_owner][msg.sender]; // Saves gas for limited approvals. if (allowed != type(uint256).max) allowance[_owner][msg.sender] = allowed - shares; } if (shares == 0) revert RedeemingZeroShares(); if (receiver == address(0)) revert ReceiverIsZeroAddress(); uint256 _tokenId = tokenId; @> beforeRedeem(_tokenId, receiver); INonfungiblePositionManager _nonfungiblePositionManager = nonfungiblePositionManager; // Saves an extra SLOAD { uint128 liquidityToDecrease = uint128((liquidity * shares) / totalSupply); (amount0, amount1) = _nonfungiblePositionManager.decreaseLiquidity( INonfungiblePositionManager.DecreaseLiquidityParams({ tokenId: _tokenId, liquidity: liquidityToDecrease, amount0Min: amount0Min, amount1Min: amount1Min, deadline: block.timestamp }) ); if (amount0 == 0 && amount1 == 0) revert AmountsAreZero(); @> _burn(_owner, shares); liquidity -= liquidityToDecrease; }
From the above code, we see that the parameter is receiver
, but the person whose shares are burn is _owner
.
We need to accrue _owner
, not receiver
This leads to a direct reduction of the user's shares without accrue
, and the user loses the corresponding rewards
function redeem(uint256 shares, uint256 amount0Min, uint256 amount1Min, address receiver, address _owner) public virtual override nonReentrant checkDeviation returns (uint256 amount0, uint256 amount1) { if (msg.sender != _owner) { uint256 allowed = allowance[_owner][msg.sender]; // Saves gas for limited approvals. if (allowed != type(uint256).max) allowance[_owner][msg.sender] = allowed - shares; } if (shares == 0) revert RedeemingZeroShares(); if (receiver == address(0)) revert ReceiverIsZeroAddress(); uint256 _tokenId = tokenId; - beforeRedeem(_tokenId, receiver); + beforeRedeem(_tokenId, _owner);
Context
#0 - c4-judge
2023-07-11T11:18:07Z
trust1995 marked the issue as unsatisfactory: Insufficient proof
#1 - c4-sponsor
2023-07-20T16:08:02Z
0xLightt marked the issue as sponsor confirmed
#2 - c4-judge
2023-07-25T09:24:04Z
trust1995 marked the issue as satisfactory
#3 - 0xLightt
2023-09-06T17:26:19Z
π Selected for report: 0xTheC0der
Also found by: Atree, BLOS, BPZ, Fulum, KupiaSec, SpicyMeatball, bin2chen, jasonxiale, lsaudit, minhquanym, xuwinnie, zzzitron
95.3782 USDC - $95.38
in removeAsset()
, Incorrectly set the assetIndex, resulting in subsequent remove fail,Even remove the wrong asset
removeAsset()
The code is as follows:
function removeAsset(address asset) external nonReentrant onlyOwner { // No need to check if index is 0, it will underflow and revert if it is 0 @> uint256 assetIndex = assetId[asset] - 1; uint256 newAssetsLength = assets.length - 1; if (newAssetsLength == 0) revert CannotRemoveLastAsset(); totalWeights -= weights[assetIndex]; address lastAsset = assets[newAssetsLength]; @> assetId[lastAsset] = assetIndex; assets[assetIndex] = lastAsset; weights[assetIndex] = weights[newAssetsLength]; assets.pop(); weights.pop(); assetId[asset] = 0; emit AssetRemoved(asset); updateAssetBalances(); asset.safeTransfer(msg.sender, asset.balanceOf(address(this))); }
When setting assetId[lastAsset]
use assetId[lastAsset] = assetIndex
However, the current protocol need use index + 1
for assetId
The correct value should be assetId[lastAsset] = assetIndex + 1
.
For example, suppose Current: assets = [A,B] assetId[A] = 1 assetId[B] = 2
removeAsset(A)
it becomesassets = [B] assetId[B] = 0 (error , correct is 1)
If execute addAsset(C), it will become assets = [B,C] assetId[B] = 0 assetId[C] = 2
then execute remove(B), it will underflow
function removeAsset(address asset) external nonReentrant onlyOwner { // No need to check if index is 0, it will underflow and revert if it is 0 @> uint256 assetIndex = assetId[asset] - 1;
The above example demonstrates the failure to remove Due to the wrong assetIndex is also possible to cause the removal the wrong asset
function removeAsset(address asset) external nonReentrant onlyOwner { // No need to check if index is 0, it will underflow and revert if it is 0 uint256 assetIndex = assetId[asset] - 1; uint256 newAssetsLength = assets.length - 1; if (newAssetsLength == 0) revert CannotRemoveLastAsset(); totalWeights -= weights[assetIndex]; address lastAsset = assets[newAssetsLength]; - assetId[lastAsset] = assetIndex; + assetId[lastAsset] = assetIndex + 1; ....
Decimal
#0 - c4-judge
2023-07-09T16:29:09Z
trust1995 marked the issue as duplicate of #703
#1 - c4-judge
2023-07-09T16:29:15Z
trust1995 marked the issue as satisfactory
#2 - c4-judge
2023-07-11T17:20:49Z
trust1995 changed the severity to 3 (High Risk)
987.7904 USDC - $987.79
missing the first transfer of the asset to router
, addLiquidity()
unable to work
UlyssesRouter.addLiquidity
use for mint LP
The code is as follows:
function addLiquidity(uint256 amount, uint256 minOutput, uint256 poolId) external returns (uint256) { UlyssesPool ulysses = getUlyssesLP(poolId); amount = ulysses.deposit(amount, msg.sender); if (amount < minOutput) revert OutputTooLow(); return amount; }
The above code, missing the first transfer of the asset to router
e.g.: ulysses.asset().safeTransferFrom(msg.sender, address(this), amount);
Cannot work
function addLiquidity(uint256 amount, uint256 minOutput, uint256 poolId) external returns (uint256) { UlyssesPool ulysses = getUlyssesLP(poolId); + ulysses.asset().safeTransferFrom(msg.sender, address(this), amount); amount = ulysses.deposit(amount, msg.sender); if (amount < minOutput) revert OutputTooLow(); return amount; }
Context
#0 - c4-judge
2023-07-09T16:22:40Z
trust1995 marked the issue as duplicate of #201
#1 - c4-judge
2023-07-11T11:48:00Z
trust1995 marked the issue as satisfactory
#2 - c4-judge
2023-07-11T17:19:23Z
trust1995 changed the severity to 3 (High Risk)
#3 - c4-judge
2023-07-11T17:19:31Z
trust1995 marked the issue as partial-50
π Selected for report: T1MOH
Also found by: SpicyMeatball, bin2chen, los_chicos, lukejohn, max10afternoon, said
333.3031 USDC - $333.30
rerange/rebalance
occupied the protocolFees
that are temporarily stored in the contract and may cause permanent loss
When rerange()
is executed, there are two important steps as follows
_withdrawAll(_tokenId)
to retrieve all the tokens
from the LP and put them into the contractfunction _withdrawAll(uint256 _tokenId) internal { uint128 _liquidity = liquidity; // Saves an extra SLOAD if totalSupply is non-zero. if (_liquidity == 0) return; INonfungiblePositionManager _nonfungiblePositionManager = nonfungiblePositionManager; @> _nonfungiblePositionManager.decreaseLiquidity( INonfungiblePositionManager.DecreaseLiquidityParams({ tokenId: _tokenId, liquidity: _liquidity, amount0Min: 0, amount1Min: 0, deadline: block.timestamp }) ); liquidity = 0; _nonfungiblePositionManager.collect( INonfungiblePositionManager.CollectParams({ tokenId: _tokenId, @> recipient: address(this), amount0Max: type(uint128).max, amount1Max: type(uint128).max }) ); }
2.Execute rerange()
to use all the balance in the contract to deposit to LP again
TalosStrategySimple.doRerange()
-> PoolActions.rerange()
-> getThisPositionTicks
:
function rerange( INonfungiblePositionManager nonfungiblePositionManager, ActionParams memory actionParams, uint24 poolFee ) internal returns (int24 tickLower, int24 tickUpper, uint256 amount0, uint256 amount1, uint256 tokenId, uint128 liquidity) { int24 baseThreshold = actionParams.tickSpacing * actionParams.optimizer.tickRangeMultiplier(); uint256 balance0; uint256 balance1; @> (balance0, balance1, tickLower, tickUpper) = getThisPositionTicks( actionParams.pool, actionParams.token0, actionParams.token1, baseThreshold, actionParams.tickSpacing ); emit Snapshot(balance0, balance1); (tokenId, liquidity, amount0, amount1) = nonfungiblePositionManager.mint( INonfungiblePositionManager.MintParams({ token0: address(actionParams.token0), token1: address(actionParams.token1), fee: poolFee, tickLower: tickLower, tickUpper: tickUpper, amount0Desired: balance0, amount1Desired: balance1, amount0Min: 0, amount1Min: 0, recipient: address(this), deadline: block.timestamp }) ); } function getThisPositionTicks( IUniswapV3Pool pool, ERC20 token0, ERC20 token1, int24 baseThreshold, int24 tickSpacing ) private view returns (uint256 balance0, uint256 balance1, int24 tickLower, int24 tickUpper) { // Emit snapshot to record balances @> balance0 = token0.balanceOf(address(this)); @> balance1 = token1.balanceOf(address(this)); //Get exact ticks depending on Optimizer's balances (tickLower, tickUpper) = pool.getPositionTicks(balance0, balance1, baseThreshold, tickSpacing); }
In this case, one thing is missing.
protocolFees0/_protocolFees1
is temporarily stored in the contract until it is taken away
So when rerange()
, it will use up the protocolFees
that belong to the protocol.
Problems caused
protocolFee
may be lost permanently
2.TalosStrategyVanilla
may _compoundFees()
underflow when calculating protocol fees, resulting in failure to deposit/redeem
.contract TalosStrategyVanilla is TalosStrategySimple { ... function _compoundFees(uint256 _tokenId) internal returns (uint256 amount0, uint256 amount1) { @> uint256 balance0 = token0.balanceOf(address(this)) - protocolFees0; @> uint256 balance1 = token1.balanceOf(address(this)) - protocolFees1; emit Snapshot(balance0, balance1);
Trigger collectProtocolFees()
before rerange/rebalance to fetch ProtocolFees
Context
#0 - c4-judge
2023-07-11T11:24:22Z
trust1995 marked the issue as duplicate of #80
#1 - c4-judge
2023-07-11T11:24:30Z
trust1995 marked the issue as satisfactory
π Selected for report: bin2chen
1712.1701 USDC - $1,712.17
In the migratePartnerVault()
method, if vaultId == 0 means illegal address, but the id of the vaults starts from 0, resulting in the first vault being mistaken as an illegal vault address
In the migratePartnerVault()
method, it will determine whether newPartnerVault
is legal or not, by vaultId!=0 of vault
The code is as follows:
function migratePartnerVault(address newPartnerVault) external onlyOwner { @> if (factory.vaultIds(IBaseVault(newPartnerVault)) == 0) revert UnrecognizedVault(); address oldPartnerVault = partnerVault; if (oldPartnerVault != address(0)) IBaseVault(oldPartnerVault).clearAll(); bHermesToken.claimOutstanding();
But when factory
adds vault, the index starts from 0, so the id of the first vault is 0
PartnerManagerFactory.addVault()
contract PartnerManagerFactory is Ownable, IPartnerManagerFactory { constructor(ERC20 _bHermes, address _owner) { _initializeOwner(_owner); bHermes = _bHermes; partners.push(PartnerManager(address(0))); } function addVault(IBaseVault newVault) external onlyOwner { @> uint256 id = vaults.length; vaults.push(newVault); vaultIds[newVault] == id; emit AddedVault(newVault, id); }
The id of the first vault starts from 0, because in constructor
does not add address(0) to the vaults similar to partners
So migratePartnerVault()
can't be processed for the first vault
Similar to partners
, in the constructor
method, a vault with address(0) is added by default.
contract PartnerManagerFactory is Ownable, IPartnerManagerFactory { constructor(ERC20 _bHermes, address _owner) { _initializeOwner(_owner); bHermes = _bHermes; partners.push(PartnerManager(address(0))); + vaults.push(IBaseVault(address(0))); }
Context
#0 - c4-judge
2023-07-09T15:45:38Z
trust1995 marked the issue as primary issue
#1 - c4-judge
2023-07-09T15:45:42Z
trust1995 marked the issue as satisfactory
#2 - c4-sponsor
2023-07-12T17:56:12Z
0xLightt marked the issue as sponsor confirmed
#3 - c4-judge
2023-07-25T12:58:53Z
trust1995 marked the issue as selected for report
#4 - 0xLightt
2023-09-06T17:24:03Z
π Selected for report: bin2chen
1712.1701 USDC - $1,712.17
Lack of override forfeitBoost , when need forfeit
will underflow
In vMaia
, override the claimBoost()
code to be empty to avoid fail
The code and comments are as follows.
/// @dev Boost can't be claimed; does not fail. It is all used by the partner vault. function claimBoost(uint256 amount) public override {}
But it does not override the corresponding forfeitBoost()
This will still reduce userClaimedBoost
when forfeit()
is needed, resulting in underflow
UtilityManager.forfeitBoost()
function forfeitBoost(uint256 amount) public virtual { if (amount == 0) return; @> userClaimedBoost[msg.sender] -= amount; address(gaugeBoost).safeTransferFrom(msg.sender, address(this), amount); emit ForfeitBoost(msg.sender, amount); }
So you should also override forfeitBoost()
and turn it into an empty code to avoid failure when you need to use forfeit
contract vMaia is ERC4626PartnerManager { + /// @dev Boost can't be forfeit; does not fail. + function forfeitBoost(uint256 amount) public override {} ...
Context
#0 - c4-judge
2023-07-11T11:27:06Z
trust1995 marked the issue as primary issue
#1 - c4-judge
2023-07-11T11:27:10Z
trust1995 marked the issue as satisfactory
#2 - c4-sponsor
2023-07-12T17:54:01Z
0xLightt marked the issue as sponsor confirmed
#3 - c4-judge
2023-07-25T12:59:12Z
trust1995 marked the issue as selected for report
#4 - 0xLightt
2023-09-06T17:24:53Z
770.4765 USDC - $770.48
If there is a weekly
that has not been taken, it may result in insufficient mint HERMES
In updatePeriod()
, mint new HERMES
every week with a certain percentage weeklyEmission
.
The code is as follows.
function updatePeriod() public returns (uint256) { uint256 _period = activePeriod; // only trigger if new week if (block.timestamp >= _period + week && initializer == address(0)) { _period = (block.timestamp / week) * week; activePeriod = _period; uint256 newWeeklyEmission = weeklyEmission(); @> weekly += newWeeklyEmission; uint256 _circulatingSupply = circulatingSupply(); uint256 _growth = calculateGrowth(newWeeklyEmission); uint256 _required = _growth + newWeeklyEmission; /// @dev share of newWeeklyEmission emissions sent to DAO. uint256 share = (_required * daoShare) / base; _required += share; uint256 _balanceOf = underlying.balanceOf(address(this)); @> if (_balanceOf < _required) { HERMES(underlying).mint(address(this), _required - _balanceOf); } underlying.safeTransfer(address(vault), _growth); if (dao != address(0)) underlying.safeTransfer(dao, share); emit Mint(msg.sender, newWeeklyEmission, _circulatingSupply, _growth, share); /// @dev queue rewards for the cycle, anyone can call if fails /// queueRewardsForCycle will call this function but won't enter /// here because activePeriod was updated try flywheelGaugeRewards.queueRewardsForCycle() {} catch {} } return _period; }
The above code will first determine if the balance of the current contract is less than _required
and if it is less, then mint new HERMES
, so that there will be enough HERMES
for the distribution.
But there is a problem that the current balance of contract may contain the last weekly
HERMES,that flywheelGaugeRewards
have not yet been taken: (e.g. last week's allocation of weeklyEmission)
Because the gaugeCycle
of flywheelGaugeRewards
may be greater than one week. So it is possible that the last weekly
HERMES has not yet been taken.
So we can't use the current balance to compare with _required
directly, we need to Consider the weekly
staying in the contract, it hasn't been taken, to avoid not having enough balance when flywheelGaugeRewards
comes to take weekly
.
function updatePeriod() public returns (uint256) { uint256 _period = activePeriod; // only trigger if new week if (block.timestamp >= _period + week && initializer == address(0)) { _period = (block.timestamp / week) * week; activePeriod = _period; uint256 newWeeklyEmission = weeklyEmission(); weekly += newWeeklyEmission; uint256 _circulatingSupply = circulatingSupply(); uint256 _growth = calculateGrowth(newWeeklyEmission); uint256 _required = _growth + newWeeklyEmission; /// @dev share of newWeeklyEmission emissions sent to DAO. uint256 share = (_required * daoShare) / base; _required += share; uint256 _balanceOf = underlying.balanceOf(address(this)); - if (_balanceOf < _required) { + if (_balanceOf < weekly + _growth + share ) { - HERMES(underlying).mint(address(this), _required - _balanceOf); + HERMES(underlying).mint(address(this),weekly + _growth + share - _balanceOf); } underlying.safeTransfer(address(vault), _growth); if (dao != address(0)) underlying.safeTransfer(dao, share); emit Mint(msg.sender, newWeeklyEmission, _circulatingSupply, _growth, share); /// @dev queue rewards for the cycle, anyone can call if fails /// queueRewardsForCycle will call this function but won't enter /// here because activePeriod was updated try flywheelGaugeRewards.queueRewardsForCycle() {} catch {} } return _period; }
Context
#0 - c4-judge
2023-07-09T08:27:51Z
trust1995 marked the issue as primary issue
#1 - c4-judge
2023-07-09T08:28:03Z
trust1995 marked the issue as satisfactory
#2 - c4-sponsor
2023-07-12T17:52:01Z
0xLightt marked the issue as sponsor acknowledged
#3 - c4-sponsor
2023-07-18T12:25:15Z
0xLightt marked the issue as sponsor disputed
#4 - c4-sponsor
2023-07-18T12:30:16Z
0xLightt marked the issue as sponsor confirmed
#5 - c4-judge
2023-07-25T12:59:44Z
trust1995 marked the issue as selected for report
#6 - deadrosesxyz
2023-07-26T21:32:59Z
Because the gaugeCycle of flywheelGaugeRewards may be greater than one week.
The warden describes a possible vulnerability if a gauge has a cycle length longer than a week. This is incorrect. gaugeCycle
refers to the block.timestamp
of the current cycle. I suppose the warden refers to gaugeCycleLength
, which however is an immutable set to a week.
#7 - 0xLightt
2023-07-26T21:58:12Z
It might make sense to be a low/QA, because it does require a rare edge case for this to happen: no one queuing rewards for any gauge during 1 week and have a large amount of gauges. Everyone in the protocol is economically incentivized to queue rewards asap every week: team, LPs, voters, etc.
But it is a valid issue, because if this were to happen and queueRewardsForCycle
revert, for example because the gauge's array is too large. It would mean that weekly
could be larger than _required
. So not enough tokens would be minted and getRewards
would revert because the minter contract wouldn't have enough balance to transfer the desired tokens.
#8 - trust1995
2023-07-27T09:22:11Z
Will leave as Med, as rare edge cases are still in-scope for this severity level and theoretical monetary loss is involved.
#9 - 0xLightt
2023-09-06T17:25:19Z
π Selected for report: bin2chen
Also found by: Audinarey, SpicyMeatball, tsvetanovv
312.043 USDC - $312.04
the position of i++
is wrong, which may lead to an infinite loop
In the loop of the _decrementWeightUntilFree()
method, the position of i++
is wrong, which may lead to a infinite loop
function _decrementWeightUntilFree(address user, uint256 weight) internal nonReentrant { ... 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++; } } }
In the above code, when userGaugeWeight == 0
, i
is not incremented, resulting in a infinite loop.
The current protocol does not restrict getUserGaugeWeight[user][gauge] == 0
.
function _decrementWeightUntilFree(address user, uint256 weight) internal nonReentrant { ... 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++; - } } + unchecked { + i++; + } }
Context
#0 - c4-judge
2023-07-11T11:25:16Z
trust1995 marked the issue as duplicate of #260
#1 - c4-judge
2023-07-11T11:25:21Z
trust1995 marked the issue as satisfactory
#2 - c4-judge
2023-07-11T17:24:01Z
trust1995 marked the issue as duplicate of #717
#3 - c4-judge
2023-07-25T13:03:04Z
trust1995 marked the issue as selected for report
#4 - c4-sponsor
2023-07-28T13:17:10Z
0xLightt marked the issue as sponsor confirmed
#5 - 0xLightt
2023-09-06T17:25:32Z
π Selected for report: Kamil-Chmielewski
Also found by: ByteBandits, Co0nan, Madalad, T1MOH, Udsen, Voyvoda, bin2chen, chaduke, jasonxiale, kutugu, said, xuwinnie, zzebra83
23.9127 USDC - $23.91
in restakeToken()
, wrong use isNotRestake = true, even if the time end, only owner
can restake()
anyone can call restakeToken if the block time is after the end time of the incentive
function _unstakeToken(IncentiveKey memory key, uint256 tokenId, bool isNotRestake) private { Deposit storage deposit = deposits[tokenId]; (uint96 endTime, uint256 stakedDuration) = IncentiveTime.getEndAndDuration(key.startTime, deposit.stakedTimestamp, block.timestamp); address owner = deposit.owner; // anyone can call restakeToken if the block time is after the end time of the incentive @> if ((isNotRestake || block.timestamp < endTime) && owner != msg.sender) revert NotCalledByOwner(); {
But restakeToken()
is passed the wrong isNotRestake=true, so even if the time end, only owner
can restake()
Error passing isNotRestake==true
, should pass isNotRestake=false
function restakeToken(uint256 tokenId) external { IncentiveKey storage incentiveId = stakedIncentiveKey[tokenId]; @> if (incentiveId.startTime != 0) _unstakeToken(incentiveId, tokenId, true); (IUniswapV3Pool pool, int24 tickLower, int24 tickUpper, uint128 liquidity) = NFTPositionInfo.getPositionInfo(factory, nonfungiblePositionManager, tokenId); _stakeToken(tokenId, pool, tickLower, tickUpper, liquidity); }
function restakeToken(uint256 tokenId) external { IncentiveKey storage incentiveId = stakedIncentiveKey[tokenId]; - if (incentiveId.startTime != 0) _unstakeToken(incentiveId, tokenId, true); + if (incentiveId.startTime != 0) _unstakeToken(incentiveId, tokenId, false); (IUniswapV3Pool pool, int24 tickLower, int24 tickUpper, uint128 liquidity) = NFTPositionInfo.getPositionInfo(factory, nonfungiblePositionManager, tokenId); _stakeToken(tokenId, pool, tickLower, tickUpper, liquidity); }
Context
#0 - c4-judge
2023-07-09T11:35:38Z
trust1995 marked the issue as primary issue
#1 - c4-judge
2023-07-11T11:33:29Z
trust1995 marked the issue as satisfactory
#2 - c4-sponsor
2023-07-12T17:56:26Z
0xLightt marked the issue as sponsor confirmed
#3 - c4-sponsor
2023-07-17T16:38:07Z
0xLightt marked the issue as disagree with severity
#4 - 0xLightt
2023-07-17T16:44:29Z
This has no direct impact on the user or any of the integrations we have in scope. The restake() function is purely for third parties automation like chainlink keepers, and loosing this integration is the only way this issue affects users.
#5 - trust1995
2023-07-24T13:28:38Z
Breaking of assumed functionality when interfacing with 3rd party contracts constitutes a Med-severity impact.
#6 - c4-judge
2023-07-25T11:31:28Z
trust1995 marked issue #534 as primary and marked this issue as a duplicate of 534
240.0331 USDC - $240.03
Wrong judgment condition, resulting in extra weight release, resulting in the user losing the corresponding reward
When a user makes a transfer of ERC20Gauges
, if the user does not have enough freeWeight
will first call _decrementWeightUntilFree()
to reduce the weights already assigned to gauges
until there is enough freeWeight
for the transfer
The _decrementWeightUntilFree()
code is as follows:
function _decrementWeightUntilFree(address user, uint256 weight) internal nonReentrant { ... 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; @> 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++; } } }
In the above code, there is a mistake in the loop condition: (userFreeWeight + totalFreed) < weight
.
This expression uses totalFreed
, which is the variable used for the reduction of _totalWeight
(it don't contains userGaugeWeight
of _deprecatedGauges
).
But now we are trying to free the user's weight
the correct variable to use is userFreed
, which is the total weight
be free for the user.
It contains the userGaugeWeight
of _deprecatedGauges
, which is also a legitimate weight.
So it would be more accurate to use userFreed
instead of totalFreed
to avoid free too much weight
.
function _decrementWeightUntilFree(address user, uint256 weight) internal nonReentrant { ... 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; - for (uint256 i = 0; i < size && (userFreeWeight + totalFreed) < weight;) { + for (uint256 i = 0; i < size && (userFreeWeight + userFreed) < 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++; } } }
Context
#0 - c4-judge
2023-07-11T06:55:22Z
trust1995 marked the issue as duplicate of #477
#1 - c4-judge
2023-07-11T06:55:31Z
trust1995 marked the issue as satisfactory
172.8238 USDC - $172.82
addBridgeAgentFactory() Does not work properly
addBridgeAgentFactory()
The implementation code is as followsοΌ
contract RootPort is Ownable, IRootPort { ... address[] public bridgeAgentFactories; function addBridgeAgentFactory(address _bridgeAgentFactory) external onlyOwner { @> bridgeAgentFactories[bridgeAgentsLenght++] = _bridgeAgentFactory; emit BridgeAgentFactoryAdded(_bridgeAgentFactory); }
The current implementation is that the owner cannot be newly added because it will Index out of bounds
.
we need to use bridgeAgentFactories.push()
, and set isBridgeAgentFactory[_bridgeAgentFactory] = true;
contract RootPort is Ownable, IRootPort { ... address[] public bridgeAgentFactories; function addBridgeAgentFactory(address _bridgeAgentFactory) external onlyOwner { - bridgeAgentFactories[bridgeAgentsLenght++] = _bridgeAgentFactory; + require(!isBridgeAgentFactory[_bridgeAgentFactory],"exists"); + isBridgeAgentFactory[_bridgeAgentFactory] = true; + bridgeAgentFactories.push(_bridgeAgentFactory); + bridgeAgentFactoriesLenght++; emit BridgeAgentFactoryAdded(_bridgeAgentFactory); }
Context
#0 - c4-judge
2023-07-10T15:14:08Z
trust1995 marked the issue as duplicate of #372
#1 - c4-judge
2023-07-10T15:14:16Z
trust1995 marked the issue as satisfactory
355.6046 USDC - $355.60
Malicious transfer 1 wei
rewardToken to address(0), will cause BribesFactory.createBribeFlywheel()
to fail forever
BribesFactory.createBribeFlywheel()
use for create Flywheel
The code is as follows:
function createBribeFlywheel(address bribeToken) public { if (address(flywheelTokens[bribeToken]) != address(0)) revert BribeFlywheelAlreadyExists(); FlywheelCore flywheel = new FlywheelCore( bribeToken, @> FlywheelBribeRewards(address(0)), flywheelGaugeWeightBooster, address(this) ); flywheelTokens[bribeToken] = flywheel; uint256 id = bribeFlywheels.length; bribeFlywheels.push(flywheel); bribeFlywheelIds[flywheel] = id; activeBribeFlywheels[flywheel] = true; @> flywheel.setFlywheelRewards(address(new FlywheelBribeRewards(flywheel, rewardsCycleLength))); emit BribeFlywheelCreated(bribeToken, flywheel); }
From the above code, we can see that
FlywheelBribeRewards == address(0)
setFlywheelRewards()
to set new FlywheelRewards: flywheel.setFlywheelRewards(address(new FlywheelBribeRewards(flywheel, rewardsCycleLength)));
Let's look at the implementation of setFlywheelRewards()
:
function setFlywheelRewards(address newFlywheelRewards) external onlyOwner { @> uint256 oldRewardBalance = rewardToken.balanceOf(address(flywheelRewards)); if (oldRewardBalance > 0) { @> rewardToken.safeTransferFrom(address(flywheelRewards), address(newFlywheelRewards), oldRewardBalance); } flywheelRewards = newFlywheelRewards; emit FlywheelRewardsUpdate(address(newFlywheelRewards)); }
From the above code, we can see that
When setting, it will get the balance of rewardToken.balanceOf(address(flywheelRewards))
first.
Note: At this time flywheelRewards == address(0)
, normally rewardToken.balanceOf(address(0))
has no balance
so it will not execute rewardToken.safeTransferFrom(address( flywheelRewards), address(newFlywheelRewards), oldRewardBalance);
But if someone has malicious DOS, they can transfer 1 wei
's rewardToken to address(0)
, currently many token implementations support transferring to address(0)
, for example, the library using solmate
.
so oldRewardBalance
will bigger then 0
This lead that this method will execute : rewardToken.safeTransferFrom(address(0), address(newFlywheelRewards), 1);
This will revert, and the whole method will fail
function setFlywheelRewards(address newFlywheelRewards) external onlyOwner { + if(flywheelRewards != address(0)) { uint256 oldRewardBalance = rewardToken.balanceOf(address(flywheelRewards)); if (oldRewardBalance > 0) { rewardToken.safeTransferFrom(address(flywheelRewards), address(newFlywheelRewards), oldRewardBalance); } + } flywheelRewards = newFlywheelRewards; emit FlywheelRewardsUpdate(address(newFlywheelRewards)); }
DoS
#0 - c4-judge
2023-07-11T11:33:11Z
trust1995 marked the issue as duplicate of #342
#1 - c4-judge
2023-07-11T11:33:17Z
trust1995 marked the issue as satisfactory
π Selected for report: 0xTheC0der
355.6046 USDC - $355.60
deposit()
The default is to take the smallest shares, if weights
is modified, the user may unknowingly donate the assets by default and lose assets
In ERC4626MultiToken.deposit()
, the shares
that can be obtained are calculated from the assetsAmounts
passed in.
Use the method previewDeposit()
to calculate shares, the code is as follows.
previewDeposit()
-> convertToShares
function convertToShares(uint256[] calldata assetsAmounts) public view virtual returns (uint256 shares) { uint256 _totalWeights = totalWeights; uint256 length = assetsAmounts.length; if (length != assets.length) revert InvalidLength(); shares = type(uint256).max; for (uint256 i = 0; i < length;) { uint256 share = assetsAmounts[i].mulDiv(_totalWeights, weights[i]); @> if (share < shares) shares = share; unchecked { i++; } } }
From the above code, we can see that
it will calculate the corresponding shares by weight
for each asset
and the smallest shares
is used to mint
.
The normal situation is OK, because the user will calculate the best assetsAmounts
before submit transaction, to avoid the excess asset
being donated by default.
But there is a problem here, weights
may be modified by owner
.
If the user calculates the best assetsAmounts
, and submit deposit()
.
Before executing the transaction, owner
modified the weight by setWeights()
.
By the time the user transaction is executed, a very small number of shares may be obtained, and the rest of the excess assets are donated by default
For example, suppose there are currently: assets = [A,B,C] weights = [50,50,100] totalWeights = 200
alice calculates the best assetsAmounts
and submit the transaction to memoypool deposit(assetsAmounts = [50,50,100])
, expecting to get shares =200
.
Before alice's transaction is executed, the owner modifies weights
for some reason, setWeights()
is modified to
weights = [20,80,100]
After alice's transaction is executed, alice gets shares=125
, which is the smallest one: 50 * 200 / 80 = 125
.
The rest of the extra assets are donated by default
Note:withdraw() have the same problem
deposit()
can specify mint_shares
or
Instead of taking the minimum value of shares, convertToShares
requires that all the shares calculated by assetsAmounts
must be the same
Avoid being donated by default
Context
#0 - c4-judge
2023-07-11T11:43:13Z
trust1995 marked the issue as duplicate of #281
#1 - c4-judge
2023-07-11T11:43:18Z
trust1995 marked the issue as satisfactory
#2 - c4-judge
2023-07-25T14:04:34Z
trust1995 marked the issue as unsatisfactory: Invalid
#3 - c4-judge
2023-07-28T11:56:27Z
trust1995 marked the issue as satisfactory
592.6743 USDC - $592.67
wrong mint amount of partnerGovernance
When increaseConversionRate()
, because the newly set bHermesRate
is increased, it is necessary to newly mint partnerGovernance
, to avoid that partnerGovernance
is insufficient when the user claims
function increaseConversionRate(uint256 newRate) external onlyOwner { if (newRate < bHermesRate) revert InvalidRate(); if (newRate > (address(bHermesToken).balanceOf(address(this)) / totalSupply)) { revert InsufficientBacking(); } bHermesRate = newRate; partnerGovernance.mint( @> address(this), totalSupply * newRate - address(partnerGovernance).balanceOf(address(this)) ); bHermesToken.claimOutstanding(); }
The above code, the number of supplements needed, using the formula: totalSupply * newRate - address(partnerGovernance).balanceOf(address(this))
This is a problem, the wrong use of address(partnerGovernance).balanceOf(address(this)
, should use partnerGovernance.totalSupply
Because it is possible that part of partnerGovernance
has already been cliams by the user and it is not in the contract, this part should be counted and no need new mint
function increaseConversionRate(uint256 newRate) external onlyOwner { if (newRate < bHermesRate) revert InvalidRate(); if (newRate > (address(bHermesToken).balanceOf(address(this)) / totalSupply)) { revert InsufficientBacking(); } bHermesRate = newRate; partnerGovernance.mint( - address(this), totalSupply * newRate - address(partnerGovernance).balanceOf(address(this)) + address(this), totalSupply * newRate - IERC20(partnerGovernance).totalSupply()); ); bHermesToken.claimOutstanding(); }
Context
#0 - c4-judge
2023-07-11T11:29:54Z
trust1995 marked the issue as duplicate of #473
#1 - c4-judge
2023-07-11T11:29:59Z
trust1995 marked the issue as satisfactory
#2 - c4-judge
2023-07-25T20:25:17Z
trust1995 marked the issue as not a duplicate
#3 - c4-judge
2023-07-25T20:25:46Z
trust1995 marked the issue as primary issue
#4 - c4-judge
2023-07-25T20:26:18Z
trust1995 marked issue #191 as primary and marked this issue as a duplicate of 191
π Selected for report: 0xSmartContract
Also found by: 3kus-iosiro, Audit_Avengers, ByteBandits, IllIllI, Kamil-Chmielewski, Madalad, RED-LOTUS-REACH, Rolezn, Sathish9098, Stormreckson, Udsen, bin2chen, brgltd, ihtishamsudo, kaveyjoe, kodyvim, lukejohn, matrix_0wl, mgf15, nadin
74.2737 USDC - $74.27
L-01: decrementGaugeBoost() It is recommended to call updateUserBoost()
after modification in order to synchronize getUserBoost[user]
function decrementGaugeBoost(address gauge, uint256 boost) public { GaugeState storage gaugeState = getUserGaugeBoost[msg.sender][gauge]; if (boost >= gaugeState.userGaugeBoost) { _userGauges[msg.sender].remove(gauge); delete getUserGaugeBoost[msg.sender][gauge]; emit Detach(msg.sender, gauge); } else { gaugeState.userGaugeBoost -= boost.toUint128(); emit DecrementUserGaugeBoost(msg.sender, gauge, gaugeState.userGaugeBoost); } + updateUserBoost(msg.sender)? }
L-02: decrementGaugesBoostIndexed() Wrong loop condition judgment
function decrementGaugesBoostIndexed(uint256 boost, uint256 offset, uint256 num) public { address[] memory gaugeList = _userGauges[msg.sender].values(); uint256 length = gaugeList.length; - for (uint256 i = 0; i < num && i < length;) { + for (uint256 i = 0; i < num && i + offset < length;) { address gauge = gaugeList[offset + i]; GaugeState storage gaugeState = getUserGaugeBoost[msg.sender][gauge]; if (_deprecatedGauges.contains(gauge) || boost >= gaugeState.userGaugeBoost) { require(_userGauges[msg.sender].remove(gauge)); // Remove from set. Should never fail. delete getUserGaugeBoost[msg.sender][gauge]; emit Detach(msg.sender, gauge); } else { gaugeState.userGaugeBoost -= boost.toUint128(); emit DecrementUserGaugeBoost(msg.sender, gauge, gaugeState.userGaugeBoost); } unchecked { i++; } } }
L-03: notAttached() It is recommended to execute updateUserBoost
before judging and accurate
abstract contract ERC20Boost is ERC20, Ownable, IERC20Boost { ... modifier notAttached(address user, uint256 amount) { + updateUserBoost(user); if (freeGaugeBoost(user) < amount) revert AttachedBoost(); _; }
L-04: ERC4626MultiToken.constructor Suggest adding a judgment that the asset cannot be duplicated
abstract contract ERC4626MultiToken is ERC20, ReentrancyGuard, IERC4626MultiToken { ... constructor(address[] memory _assets, uint256[] memory _weights, string memory _name, string memory _symbol) ERC20(_name, _symbol, 18) { ... for (uint256 i = 0; i < length;) { + if (assetId[_assets[i]] != 0) revert AssetAlreadyAdded(); require(ERC20(_assets[i]).decimals() == 18); require(_weights[i] > 0); _totalWeights += _weights[i]; assetId[_assets[i]] = i + 1; emit AssetAdded(_assets[i], _weights[i]); unchecked { i++; } } totalWeights = _totalWeights; }
L-05: TalosStrategyStaked.transfer/transferFrom recommend call _earnFees() to refresh rewards before accrue()
function transfer(address _to, uint256 _amount) public override returns (bool) { + _earnFees(tokenId); flywheel.accrue(ERC20(address(this)), msg.sender, _to); + _stake(tokenId); return super.transfer(_to, _amount); } function transferFrom(address _from, address _to, uint256 _amount) public override returns (bool) { + _earnFees(tokenId); flywheel.accrue(ERC20(address(this)), _from, _to); + _stake(tokenId); return super.transferFrom(_from, _to, _amount); }
#0 - c4-judge
2023-07-11T13:56:05Z
trust1995 marked the issue as grade-b