Skip to content
This repository has been archived by the owner on Jan 12, 2025. It is now read-only.

dany.armstrong90 - The lockMultiplier can be decreased unexpectedly by renewLockPosition. #357

Closed
sherlock-admin4 opened this issue Jul 15, 2024 · 0 comments
Labels
Duplicate A valid issue that is a duplicate of an issue with `Has Duplicates` label Medium A Medium severity issue. Reward A payout will be made for this issue

Comments

@sherlock-admin4
Copy link

sherlock-admin4 commented Jul 15, 2024

dany.armstrong90

Medium

The lockMultiplier can be decreased unexpectedly by renewLockPosition.

Summary

After MlumStaking.sol#createPosition(), the lockMultiplier is not decreased when addToPosition(), renewLockPosition() and extendLockPosition() are called individually unless multiplier settings are not changed.
And in addToPosition(), the lockMultiplier is not decreased but lockDuration can be decreased.
And in renewLockPosition(), lockMultiplier is calculated from lockDuration.
So if a user calls through addToPosition() => renewLockPosition(), lockMultiplier can be decreased unexpectedly so amountWithMultiplier is decreased and less rewards are distributed to the user.

Vulnerability Detail

MlumStaking.sol#createPosition() function is as follows.

    function createPosition(uint256 amount, uint256 lockDuration) external override nonReentrant {
        // no new lock can be set if the pool has been unlocked
        if (isUnlocked()) {
            require(lockDuration == 0, "locks disabled");
        }

        _updatePool();

        // handle tokens with transfer tax
        amount = _transferSupportingFeeOnTransfer(stakedToken, msg.sender, amount);
        require(amount != 0, "zero amount"); // createPosition: amount cannot be null

        // mint NFT position token
        uint256 currentTokenId = _mintNextTokenId(msg.sender);

        // calculate bonuses
370     uint256 lockMultiplier = getMultiplierByLockDuration(lockDuration);
371     uint256 amountWithMultiplier = amount * (lockMultiplier + 1e4) / 1e4;

        // create position
        _stakingPositions[currentTokenId] = StakingPosition({
            initialLockDuration: lockDuration,
            amount: amount,
            rewardDebt: amountWithMultiplier * (_accRewardsPerShare) / (PRECISION_FACTOR),
            lockDuration: lockDuration,
            startLockTime: _currentBlockTimestamp(),
380         lockMultiplier: lockMultiplier,
381         amountWithMultiplier: amountWithMultiplier,
            totalMultiplier: lockMultiplier
        });

        // update total lp supply
        _stakedSupply = _stakedSupply + amount;
        _stakedSupplyWithMultiplier = _stakedSupplyWithMultiplier + amountWithMultiplier;

        emit CreatePosition(currentTokenId, amount, lockDuration);
    }

Here, lockMultiplier is calculated from lockDuration.
And MlumStaking.sol#addToPosition() function is as follows.

    function addToPosition(uint256 tokenId, uint256 amountToAdd) external override nonReentrant {
        ...

        StakingPosition storage position = _stakingPositions[tokenId];

        // we calculate the avg lock time:
        // lock_duration = (remainin_lock_time * staked_amount + amount_to_add * inital_lock_duration) / (staked_amount + amount_to_add)
        uint256 remainingLockTime = _remainingLockTime(position);
 410    uint256 avgDuration = (remainingLockTime * position.amount + amountToAdd * position.initialLockDuration)
            / (position.amount + amountToAdd);

        position.startLockTime = _currentBlockTimestamp();
414     position.lockDuration = avgDuration;

        // lock multiplier stays the same
417     position.lockMultiplier = getMultiplierByLockDuration(position.initialLockDuration);

        ...
    }

As we can see on L410, avgDuration < position.initialLockDuration so lockDuration is decreased.
But on L417, lockMultiplier is calculated from position.initialLockDuration so it is not decreased.
And MlumStaking.sol#renewLockPosition(), _lockPosition() functions are as follows.

    function renewLockPosition(uint256 tokenId) external nonReentrant {
        _requireOnlyApprovedOrOwnerOf(tokenId);

        _updatePool();
@>     _lockPosition(tokenId, _stakingPositions[tokenId].lockDuration, false);
    }
    function _lockPosition(uint256 tokenId, uint256 lockDuration, bool resetInitial) internal {
        require(!isUnlocked(), "locks disabled");

        StakingPosition storage position = _stakingPositions[tokenId];

        // for renew only, check if new lockDuration is at least = to the remaining active duration
        uint256 endTime = position.startLockTime + position.lockDuration;
        uint256 currentBlockTimestamp = _currentBlockTimestamp();
        if (endTime > currentBlockTimestamp) {
            require(lockDuration >= (endTime - currentBlockTimestamp) && lockDuration > 0, "invalid");
        }

        // for extend lock postion we reset the initial lock duration
        // we have to check that the lock duration is greater then the current
        if (resetInitial) {
            require(lockDuration > position.initialLockDuration, "invalid");
            position.initialLockDuration = lockDuration;
        }

        _harvestPosition(tokenId, msg.sender);

        // update position and total lp supply
        position.lockDuration = lockDuration;
714     position.lockMultiplier = getMultiplierByLockDuration(lockDuration);
        position.startLockTime = currentBlockTimestamp;
        _updateBoostMultiplierInfoAndRewardDebt(position);

        emit LockPosition(tokenId, lockDuration);
    }

As you can see on L714, the lockMultiplier is calculated from lockDuration.

If a user calls renewLockPosition() => _lockPosition() after addToPosition() by some reason, the lockMultiplier is decreased.
This results in distribution of less rewards to the user.

Impact

If a user calls renewLockPosition() after addToPosition(), the lockMultiplier is decreased so the amountWithMultiplier is decreased and the user receives less rewards than normal unexpectedly.

Code Snippet

https://github.com/sherlock-audit/2024-06-magicsea/blob/main/magicsea-staking/src/MlumStaking.sol#L714

Tool used

Manual Review

Recommendation

MlumStaking.sol#_lockPosition() function has to be modified as follows.

    function _lockPosition(uint256 tokenId, uint256 lockDuration, bool resetInitial) internal {
        require(!isUnlocked(), "locks disabled");

        StakingPosition storage position = _stakingPositions[tokenId];

        // for renew only, check if new lockDuration is at least = to the remaining active duration
        uint256 endTime = position.startLockTime + position.lockDuration;
        uint256 currentBlockTimestamp = _currentBlockTimestamp();
        if (endTime > currentBlockTimestamp) {
            require(lockDuration >= (endTime - currentBlockTimestamp) && lockDuration > 0, "invalid");
        }

        // for extend lock postion we reset the initial lock duration
        // we have to check that the lock duration is greater then the current
        if (resetInitial) {
            require(lockDuration > position.initialLockDuration, "invalid");
            position.initialLockDuration = lockDuration;
        }

        _harvestPosition(tokenId, msg.sender);

        // update position and total lp supply
        position.lockDuration = lockDuration;
-       position.lockMultiplier = getMultiplierByLockDuration(lockDuration);
+       position.lockMultiplier = getMultiplierByLockDuration(position.initialLockDuration);
        position.startLockTime = currentBlockTimestamp;
        _updateBoostMultiplierInfoAndRewardDebt(position);

        emit LockPosition(tokenId, lockDuration);
    }

Duplicate of #138

@github-actions github-actions bot added the Excluded Excluded by the judge without consulting the protocol or the senior label Jul 21, 2024
@0xSmartContract 0xSmartContract added Medium A Medium severity issue. Duplicate A valid issue that is a duplicate of an issue with `Has Duplicates` label and removed Excluded Excluded by the judge without consulting the protocol or the senior labels Jul 28, 2024
@sherlock-admin4 sherlock-admin4 changed the title Petite Rouge Huskie - The lockMultiplier can be decreased unexpectedly by renewLockPosition. dany.armstrong90 - The lockMultiplier can be decreased unexpectedly by renewLockPosition. Jul 29, 2024
@sherlock-admin4 sherlock-admin4 added the Reward A payout will be made for this issue label Jul 29, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Duplicate A valid issue that is a duplicate of an issue with `Has Duplicates` label Medium A Medium severity issue. Reward A payout will be made for this issue
Projects
None yet
Development

No branches or pull requests

2 participants