Here are issues in mock contracts:
I, Access Control issues:
1, StETHMock contract:
function mint()
is publicly accessible, anyone can mint sthETHMock token:
function mint(address _account, uint256 _amount) external {
require(_amount <= 1000 * (10 ** decimals()), "StETHMock: amount is too big");
uint256 sharesAmount = getSharesByPooledEth(_amount);
_mintShares(_account, sharesAmount);
totalPooledEther += _amount;
}
Attacker can mint any share and DoS further minting share by making totalPooledEther
== 2^256-1, making further mint revert, even when owner reset by calling setTotalPooledEther
, attacker can do it again
2, WStETHMock contract:
function mint()
is publicly accessible, anyone can mint token for free:
function mint(address account_, uint256 amount_) external {
_mint(account_, amount_);
}
3, DistributionV2 contract:
function createPool()
is publicly accessible, compare to Distribution
contract, pool only can be created by owner:
function createPool(IDistribution.Pool calldata pool_) public {
pools.push(pool_);
}
Recommendations: these functions should have checking condition to restrict role
II, Function do not work as it should:
1, GatewayRouterMock contract:
Function outBoundTransfer()
is intended used to bridge token, but it only transfer to another address in the same chain:
function outboundTransfer(
address _token,
address _to,
uint256 _amount,
uint256 _maxGas,
uint256 _gasPriceBid,
bytes calldata _data
) external payable returns (bytes memory) {
IERC20(_token).transferFrom(msg.sender, _to, _amount);
return abi.encode(_token, _to, _amount, _maxGas, _gasPriceBid, _data);
}
Moreover, native eth transfered to this function, it is not used anywhere and stuck in the contract, since there is no way to withdraw them.
2, NonfungiblePositionManagerMock contract
Function increaseLiquidity()
is used to increase liquidity, but it do not do anything actually:
function increaseLiquidity(
INonfungiblePositionManager.IncreaseLiquidityParams calldata params
) external payable returns (uint128 liquidity, uint256 amount0, uint256 amount1) {}
Similar with positions()
function:
function positions(
uint256 tokenId
)
external
view
returns (
uint96 nonce,
address operator,
address token0,
address token1,
uint24 fee,
int24 tickLower,
int24 tickUpper,
uint128 liquidity,
uint256 feeGrowthInside0LastX128,
uint256 feeGrowthInside1LastX128,
uint128 tokensOwed0,
uint128 tokensOwed1
)
{}
Both of them is used in L2TokenReceiver.increaseLiquidityCurrentRange()
function, which make this function do not work as intended:
function increaseLiquidityCurrentRange(
uint256 tokenId_,
uint256 depositTokenAmountAdd_,
uint256 rewardTokenAmountAdd_,
uint256 depositTokenAmountMin_,
uint256 rewardTokenAmountMin_
) external onlyOwner returns (uint128 liquidity_, uint256 amount0_, uint256 amount1_) {
uint256 amountAdd0_;
uint256 amountAdd1_;
uint256 amountMin0_;
uint256 amountMin1_;
(, , address token0_, , , , , , , , , ) = INonfungiblePositionManager(nonfungiblePositionManager).positions( //<---
tokenId_
);
if (token0_ == params.tokenIn) {
amountAdd0_ = depositTokenAmountAdd_;
amountAdd1_ = rewardTokenAmountAdd_;
amountMin0_ = depositTokenAmountMin_;
amountMin1_ = rewardTokenAmountMin_;
} else {
amountAdd0_ = rewardTokenAmountAdd_;
amountAdd1_ = depositTokenAmountAdd_;
amountMin0_ = rewardTokenAmountMin_;
amountMin1_ = depositTokenAmountMin_;
}
INonfungiblePositionManager.IncreaseLiquidityParams memory params_ = INonfungiblePositionManager
.IncreaseLiquidityParams({
tokenId: tokenId_,
amount0Desired: amountAdd0_,
amount1Desired: amountAdd1_,
amount0Min: amountMin0_,
amount1Min: amountMin1_,
deadline: block.timestamp
});
(liquidity_, amount0_, amount1_) = INonfungiblePositionManager(nonfungiblePositionManager).increaseLiquidity( //<---
params_
);
emit LiquidityIncreased(tokenId_, amount0_, amount1_, liquidity_, amountMin0_, amountMin1_);
}
3, SwapRouterMock contract:
Function exactInputSingle()
is used to swap:
function swap(uint256 amountIn_, uint256 amountOutMinimum_) external onlyOwner returns (uint256) {
SwapParams memory params_ = params;
ISwapRouter.ExactInputSingleParams memory swapParams_ = ISwapRouter.ExactInputSingleParams({
tokenIn: params_.tokenIn,
tokenOut: params_.tokenOut,
fee: params_.fee,
recipient: address(this),
deadline: block.timestamp,
amountIn: amountIn_,
amountOutMinimum: amountOutMinimum_,
sqrtPriceLimitX96: params_.sqrtPriceLimitX96
});
uint256 amountOut_ = ISwapRouter(router).exactInputSingle(swapParams_); //<----
emit TokensSwapped(params_.tokenIn, params_.tokenOut, amountIn_, amountOut_, amountOutMinimum_);
return amountOut_;
}
But it just directly transfer extact amountIn
of tokenOut to recipient from that contract and not swapping anything:
function exactInputSingle(ISwapRouter.ExactInputSingleParams calldata params_) external returns (uint256) {
IERC20(params_.tokenIn).transferFrom(msg.sender, address(this), params_.amountIn);
IERC20(params_.tokenOut).transfer(params_.recipient, params_.amountIn);
return params_.amountIn;
}
For remediation, these function should be updated to be able to work when protocol is deployed
III, Function does not exist
In L1Sender
contract, it can be seen that GateWayRouterMock
contract should have 2 functions: outboundTransfer()
and send()
:
function sendDepositToken(
uint256 gasLimit_,
uint256 maxFeePerGas_,
uint256 maxSubmissionCost_
) external payable onlyDistribution returns (bytes memory) {
DepositTokenConfig storage config = depositTokenConfig;
// Get current stETH balance
uint256 amountUnwrappedToken_ = IERC20(unwrappedDepositToken).balanceOf(address(this));
// Wrap all stETH to wstETH
uint256 amount_ = IWStETH(config.token).wrap(amountUnwrappedToken_);
bytes memory data_ = abi.encode(maxSubmissionCost_, "");
return
IGatewayRouter(config.gateway).outboundTransfer{value: msg.value}( //<----
config.token,
config.receiver,
amount_,
gasLimit_,
maxFeePerGas_,
data_
);
}
function sendMintMessage(address user_, uint256 amount_, address refundTo_) external payable onlyDistribution {
RewardTokenConfig storage config = rewardTokenConfig;
bytes memory receiverAndSenderAddresses_ = abi.encodePacked(config.receiver, address(this));
bytes memory payload_ = abi.encode(user_, amount_);
ILayerZeroEndpoint(config.gateway).send{value: msg.value}( //<----
config.receiverChainId, // communicator LayerZero chainId
receiverAndSenderAddresses_, // send to this address to the communicator
payload_, // bytes payload
payable(refundTo_), // refund address
address(0x0), // future parameter
bytes("") // adapterParams (see "Advanced Features")
);
}
But only outBoundRouter
function avaiable in the contract:
contract GatewayRouterMock {
function outboundTransfer(
address _token,
address _to,
uint256 _amount,
uint256 _maxGas,
uint256 _gasPriceBid,
bytes calldata _data
) external payable returns (bytes memory) {
IERC20(_token).transferFrom(msg.sender, _to, _amount);
return abi.encode(_token, _to, _amount, _maxGas, _gasPriceBid, _data);
}
function getGateway(address) external view returns (address) {
return address(this);
}
}
For remediation, function send()
should be implemented in the contract
Manual review
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.