MorpheusAI

MorpheusAI
Foundry
22,500 USDC
View results
Submission Details
Severity: low
Valid

Mock issues

Summary

Here are issues in mock contracts:

Vulnerability Details

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

Tools Used

Manual review

Updates

Lead Judging Commences

inallhonesty Lead Judge
over 1 year ago
inallhonesty Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

Lack of access control in `StETHMock:mint` and `WStETHMock::mint`

`createPool` from DistributionV2.sol misses all the checks and access control available in Distribution.sol

greatlake Submitter
over 1 year ago
greatlake Submitter
over 1 year ago
greatlake Submitter
over 1 year ago
inallhonesty Lead Judge
over 1 year ago
greatlake Submitter
over 1 year ago
inallhonesty Lead Judge
over 1 year ago
inallhonesty Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

Lack of access control in `StETHMock:mint` and `WStETHMock::mint`

`createPool` from `DistributionV2.sol` misses all the checks and access control available in `Distribution.sol`

SwapRouterMock/NonfungiblePositionManagerMock doesn't take into account prices or token pairs or any traditional protection mechanisms of Uniswap

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.