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: 22/101
Findings: 6
Award: $2,666.63
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: shealtielanz
Also found by: 0xStalin, 0xnev, Breeje, RED-LOTUS-REACH, kutugu, xuwinnie
166.6515 USDC - $166.65
https://github.com/code-423n4/2023-05-maia/blob/54a45beb1428d85999da3f721f923cbf36ee3d35/src/ulysses-omnichain/RootBridgeAgent.sol#L674-L681 https://github.com/code-423n4/2023-05-maia/blob/54a45beb1428d85999da3f721f923cbf36ee3d35/src/ulysses-omnichain/RootBridgeAgent.sol#L714-L725
_gasSwapIn and _gasSwapOut using the wrong price. There are a couple of questions here:
First for TWAP price manipulation, here is a good explanation. The proposer can manipulate the TWAP price by manipulating two consecutive blocks, and then use sandwiches, JIT and other means to arbitrage. By the way, since there's no slippage protection, a lot of arbitrage space.
Second, for the swap price, it should be set to zeroForOneOnInflow ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1
, not the current slot0 price. This results in missing out on many low price ranges and losing funds.
Manual review
Set the price to zeroForOneOnInflow ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1
Uniswap
#0 - c4-judge
2023-07-10T10:31:42Z
trust1995 marked the issue as unsatisfactory: Invalid
#1 - KuTuGu
2023-07-27T16:44:14Z
Hi, I raised two questions here:
#2 - trust1995
2023-07-28T10:54:55Z
TWAP price can be manipulated
is a generic statement and without specific in-code explanations, is not enough to be accepted, especially when submitted under High severity which requires high burden of proof.
Warden has identified the slot0 instantaneous price issue, therefore this submission will be dupped to the main entry and given partial scoring.
#3 - c4-judge
2023-07-28T10:55:05Z
trust1995 marked the issue as satisfactory
#4 - c4-judge
2023-07-28T10:55:13Z
trust1995 marked the issue as duplicate of #823
#5 - c4-judge
2023-07-28T10:55:22Z
trust1995 marked the issue as partial-50
#6 - KuTuGu
2023-07-29T02:31:32Z
I wonder if the second question is valid and would like to hear your opinion?
#7 - trust1995
2023-07-29T07:12:01Z
I don't think it's necessarily a good idea, and regardless it is framed as a recommendation. It would have been good to back it up with rationalization.
🌟 Selected for report: peakbolt
Also found by: 0xStalin, 0xTheC0der, BPZ, LokiThe5th, RED-LOTUS-REACH, adeolu, bin2chen, jasonxiale, kodyvim, kutugu, ltyu, ubermensch
23.8445 USDC - $23.84
https://github.com/code-423n4/2023-05-maia/blob/54a45beb1428d85999da3f721f923cbf36ee3d35/src/ulysses-omnichain/BranchBridgeAgent.sol#L1340-L1342 https://github.com/code-423n4/2023-05-maia/blob/54a45beb1428d85999da3f721f923cbf36ee3d35/src/ulysses-omnichain/BranchPort.sol#L388-L390
According to the documentation, Ulysses Omnichain accounting supports tokens with any decimals and converts them to 18 decimals.
However, its normalization and denormalizeDecimals formula are incorrect and incompatible with low-precision tokens.
function _normalizeDecimals(uint256 _amount, uint8 _decimals) internal pure returns (uint256) { return _decimals == 18 ? _amount : _amount * (10 ** _decimals) / 1 ether; }
For USDT, the decimal is 6
, and amounts less than 1e12
are all 0
after _normalizeDecimals
, which obviously prevents most user from interacting with protocol. The same for _denormalizeDecimals
.
Manual review
Use 36 precision scaling
function _normalizeDecimals(uint256 _amount, uint8 _decimals) internal pure returns (uint256) { return _amount * 10 ** (36 - _decimals); }
Decimal
#0 - c4-judge
2023-07-09T15:23:56Z
trust1995 marked the issue as duplicate of #758
#1 - c4-judge
2023-07-09T15:24:00Z
trust1995 marked the issue as partial-50
#2 - c4-judge
2023-07-11T17:12:45Z
trust1995 changed the severity to 3 (High Risk)
#3 - c4-judge
2023-07-28T11:18:34Z
trust1995 marked the issue as partial-25
1975.5809 USDC - $1,975.58
FlywheelAcummulatedRewards/FlywheelBribeRewards gains are instantaneous and can be frontrun.
The user only needs to frontrun the delegate before each incentive is distributed to get the incentive, and there is no way to prevent the user from undelegating after receiving the reward.
The protocal is to mint votes using staking bHermes. For example, the user can staking bHermes to get votes, delegate to receive the incentive, undelegate, and then take out bHermes to do other earnings, and wait until a week later to re-delegate.
function testUndelegateAfterAccrue() external { MockERC20 token = new MockERC20("test token", "TKN", 18); FlywheelCore flywheel = createFlywheel(token); gaugeToken.setMaxDelegates(2); hermes.mint(address(this), 100e18); hermes.approve(address(gaugeToken), 100e18); gaugeToken.mint(address(this), 100e18); gaugeToken.delegate(address(this)); gaugeToken.incrementGauge(address(gauge), 100e18); address other = address(0xdead); hevm.startPrank(other); hermes.mint(other, 100e18); hermes.approve(address(gaugeToken), 100e18); gaugeToken.mint(other, 100e18); gaugeToken.delegate(other); gaugeToken.incrementGauge(address(gauge), 100e18); hevm.stopPrank(); // addBribeFlywheel gauge.addBribeFlywheel(flywheel); token.mint(address(depot), 100 ether); // claimRewards gauge.accrueBribes(address(this)); flywheel.claimRewards(address(this)); gauge.accrueBribes(other); flywheel.claimRewards(other); assert(token.balanceOf(address(this)) == token.balanceOf(other)); assert(token.balanceOf(address(this)) == 50 ether); // Undo delegate gaugeToken.decrementGauge(address(gauge), 100e18); gaugeToken.undelegate(address(this), 100e18); gaugeToken.burn(address(this), 100e18); hevm.warp(WEEK - 3 days); // frontrun to delegate gaugeToken.mint(address(this), 100e18); gaugeToken.delegate(address(this)); gaugeToken.incrementGauge(address(gauge), 100e18); // update bribeFlywheel token.mint(address(depot), 100 ether); // Cycle 1 hevm.warp(WEEK); // claimRewards gauge.accrueBribes(address(this)); flywheel.claimRewards(address(this)); gauge.accrueBribes(other); flywheel.claimRewards(other); assert(token.balanceOf(address(this)) == token.balanceOf(other)); assert(token.balanceOf(address(this)) == 100 ether); // Undo delegate gaugeToken.decrementGauge(address(gauge), 100e18); gaugeToken.undelegate(address(this), 100e18); gaugeToken.burn(address(this), 100e18); hevm.warp(WEEK * 2 - 3 days); // frontrun to delegate gaugeToken.mint(address(this), 100e18); gaugeToken.delegate(address(this)); gaugeToken.incrementGauge(address(gauge), 100e18); // update bribeFlywheel token.mint(address(depot), 100 ether); // Cycle 2 hevm.warp(WEEK * 2); // claimRewards gauge.accrueBribes(address(this)); flywheel.claimRewards(address(this)); gauge.accrueBribes(other); flywheel.claimRewards(other); assert(token.balanceOf(address(this)) == token.balanceOf(other)); assert(token.balanceOf(address(this)) == 150 ether); }
Add the above test to BaseV2GaugeTest.t.sol
, and you will find that the gains from the frontrun and the gains from the continuous delegate are equal.
Foundry
Assign incentives by delegate duration instead of cycle
Context
#0 - c4-judge
2023-07-10T08:35:33Z
trust1995 marked the issue as duplicate of #206
#1 - c4-judge
2023-07-10T08:35:39Z
trust1995 marked the issue as satisfactory
#2 - c4-judge
2023-07-25T08:49:05Z
trust1995 changed the severity to 3 (High Risk)
462.2859 USDC - $462.29
https://github.com/code-423n4/2023-05-maia/blob/54a45beb1428d85999da3f721f923cbf36ee3d35/src/ulysses-omnichain/RootBridgeAgent.sol#L1219-L1222 https://github.com/code-423n4/2023-05-maia/blob/54a45beb1428d85999da3f721f923cbf36ee3d35/src/ulysses-omnichain/RootBridgeAgent.sol#L848-L852
DepositGasAnycallConfig can deposit the gas fee externally, but here should not withdraw the nativeToken. This prevents gas from being deposited.
There are two ways to store gas in RootBridgeAgent:
// deposit GAS function _manageGasOut(uint24 _toChain) internal returns (uint128) { uint256 amountOut; address gasToken; uint256 _initialGas = initialGas; if (_toChain == localChainId) { //Transfer gasToBridgeOut Local Branch Bridge Agent if remote initiated call. if (_initialGas > 0) { address(wrappedNativeToken).safeTransfer(getBranchBridgeAgent[localChainId], userFeeInfo.gasToBridgeOut); } return uint128(userFeeInfo.gasToBridgeOut); } if (_initialGas > 0) { if (userFeeInfo.gasToBridgeOut <= MIN_FALLBACK_RESERVE * tx.gasprice) revert InsufficientGasForFees(); (amountOut, gasToken) = _gasSwapOut(userFeeInfo.gasToBridgeOut, _toChain); } else { if (msg.value <= MIN_FALLBACK_RESERVE * tx.gasprice) revert InsufficientGasForFees(); wrappedNativeToken.deposit{value: msg.value}(); (amountOut, gasToken) = _gasSwapOut(msg.value, _toChain); } IPort(localPortAddress).burn(address(this), gasToken, amountOut, _toChain); return amountOut.toUint128(); } // pay GAS if (localAnyCallExecutorAddress == msg.sender) { //Save initial gas initialGas = _initialGas; } //Zero out gas after use if remote call if (initialGas > 0) { _payExecutionGas(userFeeInfo.depositedGas, userFeeInfo.gasToBridgeOut, _initialGas, fromChainId); }
When localAnyCallExecutorAddress invoke anyExecute, gas fee is stored in nativeToken first, then later withdraw from nativeToken and stored into multichain. That's right
function depositGasAnycallConfig() external payable { //Deposit Gas _replenishGas(msg.value); } function _replenishGas(uint256 _executionGasSpent) internal { //Unwrap Gas wrappedNativeToken.withdraw(_executionGasSpent); IAnycallConfig(IAnycallProxy(localAnyCallAddress).config()).deposit{value: _executionGasSpent}(address(this)); }
But when deposit gas directly from the outside, there is no need to interact with wrappedNativeToken, and the withdraw prevents the deposit.
Manual review
Also add a deposit logic to depositGasAnycallConfig, or remove the withdraw logic
Context
#0 - c4-judge
2023-07-10T11:08:45Z
trust1995 marked the issue as duplicate of #305
#1 - c4-judge
2023-07-10T11:08:49Z
trust1995 marked the issue as satisfactory
#2 - c4-judge
2023-07-25T13:37:35Z
trust1995 marked the issue as selected for report
#3 - c4-sponsor
2023-07-25T18:09:53Z
0xBugsy marked the issue as sponsor confirmed
#4 - c4-sponsor
2023-07-27T19:06:23Z
0xBugsy marked the issue as sponsor acknowledged
#5 - 0xBugsy
2023-07-28T13:17:23Z
We recognize the audit's findings on Anycall Gas Management. These will not be rectified due to the upcoming migration of this section to LayerZero.
#6 - c4-sponsor
2023-07-28T13:19:58Z
0xBugsy marked the issue as sponsor confirmed
🌟 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
restakeToken
is used to restaking an NFT, which can be invoked by anyone when the current period expires according to code comments.
But a problem with the code implementation caused only the owner can invoke restakeToken
even after the period expired, which the team admitted was an implementation error.
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 _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();
When restakeToken
invoke _unstakeToken
, the parameter isNotRestake
should be set to false
, but in the code is true
, which would prevent other users from calling restakeToken
.
Manual review
Set isNotRestake to false
Context
#0 - c4-judge
2023-07-09T11:37:37Z
trust1995 marked the issue as duplicate of #745
#1 - c4-judge
2023-07-09T11:37:42Z
trust1995 marked the issue as satisfactory
🌟 Selected for report: 0xnev
Also found by: 0xSmartContract, Breeje, BugBusters, ByteBandits, IllIllI, Kaiziron, Madalad, MohammedRizwan, SpicyMeatball, T1MOH, kutugu, nadin, peanuts, said, shealtielanz, tsvetanovv
14.356 USDC - $14.36
https://github.com/code-423n4/2023-05-maia/blob/54a45beb1428d85999da3f721f923cbf36ee3d35/src/ulysses-amm/UlyssesRouter.sol#L49 https://github.com/code-423n4/2023-05-maia/blob/54a45beb1428d85999da3f721f923cbf36ee3d35/src/ulysses-amm/UlyssesRouter.sol#L59 https://github.com/code-423n4/2023-05-maia/blob/54a45beb1428d85999da3f721f923cbf36ee3d35/src/ulysses-amm/UlyssesRouter.sol#L73
UlyssesRouter has no expiration time protection, if the token price changes before tx execution, it will affect user earnings.
Manual review
Add expiration time protection for UlyssesRouter
MEV
#0 - c4-judge
2023-07-09T16:10:49Z
trust1995 marked the issue as unsatisfactory: Invalid
#1 - c4-judge
2023-07-25T09:33:19Z
trust1995 marked the issue as duplicate of #171
#2 - c4-judge
2023-07-25T09:44:19Z
trust1995 marked the issue as satisfactory