Munchables - 0xHash's results

A web3 point farming game in which Keepers nurture creatures to help them evolve, deploying strategies to earn them rewards in competition with other players.

General Information

Platform: Code4rena

Start Date: 22/05/2024

Pot Size: $20,000 USDC

Total HM: 6

Participants: 126

Period: 5 days

Judge: 0xsomeone

Total Solo HM: 1

Id: 379

League: ETH

Munchables

Findings Distribution

Researcher Performance

Rank: 62/126

Findings: 1

Award: $0.01

🌟 Selected for report: 0

🚀 Solo Findings: 0

Lines of code

https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L401-L427 https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L275-L294 https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L311-L398

Vulnerability details

Impact

Due to lack of validation of the _onBehalfOf address in the LockManager.sol#lockOnBehalf() function, an attacker can access arbitrary players. Therefore, by capturing the player's unlock() function, the attacker can use the lockOnBehalf() function to lock a small amount of ETH (1wei) and revert the player's unlock() function.

Proof of Concept

In the LockManager.sol#lockOnBehalf() function, the _onBehalfOf address can be entered arbitrarily, so the attacker can directly access any player.

    function lockOnBehalf(
        address _tokenContract,
        uint256 _quantity,
        address _onBehalfOf
    )
        external
        payable
        notPaused
        onlyActiveToken(_tokenContract)
        onlyConfiguredToken(_tokenContract)
        nonReentrant
    {
        address tokenOwner = msg.sender;
        address lockRecipient = msg.sender;
289:    if (_onBehalfOf != address(0)) {
290:        lockRecipient = _onBehalfOf;
291:    }

293:    _lock(_tokenContract, _quantity, tokenOwner, lockRecipient);
    }

Additionally, the LockManager.sol#_lock() function updates lockedToken.quantity and then updates lockedToken.unlockTime based on the current timestamp (block.timestamp).

    function _lock(
        address _tokenContract,
        uint256 _quantity,
        address _tokenOwner,
        address _lockRecipient
    ) private {
        (
            address _mainAccount,
            MunchablesCommonLib.Player memory _player
        ) = accountManager.getPlayer(_lockRecipient);
        if (_mainAccount != _lockRecipient) revert SubAccountCannotLockError();
        if (_player.registrationDate == 0) revert AccountNotRegisteredError();
        // check approvals and value of tx matches
        if (_tokenContract == address(0)) {
            if (msg.value != _quantity) revert ETHValueIncorrectError();
        } else {
            if (msg.value != 0) revert InvalidMessageValueError();
            IERC20 token = IERC20(_tokenContract);
            uint256 allowance = token.allowance(_tokenOwner, address(this));
            if (allowance < _quantity) revert InsufficientAllowanceError();
        }

        LockedToken storage lockedToken = lockedTokens[_lockRecipient][
            _tokenContract
        ];
        ... ...

        lockedToken.remainder = remainder;
380:    lockedToken.quantity += _quantity;
381:    lockedToken.lastLockTime = uint32(block.timestamp);
382:    lockedToken.unlockTime =
383:        uint32(block.timestamp) +
384:        uint32(_lockDuration);

        // set their lock duration in playerSettings
387:    playerSettings[_lockRecipient].lockDuration = _lockDuration;

        ... ...
    }

Meanwhile, the LockManager.sol#unlock() function checks whether the asset can be unlocked.

    function unlock(
        address _tokenContract,
        uint256 _quantity
    ) external notPaused nonReentrant {
        LockedToken storage lockedToken = lockedTokens[msg.sender][
            _tokenContract
        ];
        if (lockedToken.quantity < _quantity)
            revert InsufficientLockAmountError();
410:    if (lockedToken.unlockTime > uint32(block.timestamp))
411:        revert TokenStillLockedError();

        ... ...
    }

Therefore, when the attacker catches the player's LockManager.sol#unlock() function in mempool, he first executes the lockOnBehalf() function with 1wei's assets, thereby extending the player's lockedToken.unlockTime, Thereby the attacker can revert the unlock() function and then the player will not be able to unlock his assets.

Tools Used

Manual Review

It is recommended to add a validation function for the _onBehalfOf address of the LockManager.sol#lockOnBehalf() function.

Assessed type

Invalid Validation

#0 - c4-judge

2024-06-05T12:57:43Z

alex-ppg marked the issue as satisfactory

#1 - c4-judge

2024-06-05T13:00:01Z

alex-ppg changed the severity to 3 (High Risk)

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter