Lucene search

K
code423n4Code4renaCODE423N4:2022-04-MIMO-FINDINGS-ISSUES-115
HistoryMay 02, 2022 - 12:00 a.m.

Fund theft In PARMinerV2 with depositing in VotingEscrow and calling updateBoost() to update user.stakeWithBoost without updating accAmountPerShare and accParAmountPerShare. and then collecting more rewards

2022-05-0200:00:00
Code4rena
github.com
5
parminerv2
votingescrow
fund theft

Lines of code

Vulnerability details

Impact

Attacker can generate more PAR and MIMO reward for himself and steal others rewards by staking in VotingEscrow then calling updateBoost() (which updates user.stakeWithBoost based on user boost multiplier (which is based on user VotingEscrow balance) without updating accAmountPerShare and accParAmountPerShare) then calling releaseRewards() to get more rewards.

Proof of Concept

To exploit this, attacker will do this steps:

  1. First deposit fund to PARMinerV2 so miner start tracking attacker’s rewards.

  2. Then attacker will wait for some time until _accMimoAmountPerShare and _accParAmountPerShare values gets high enough in contract and attack become more profitable(higher change in those values will make attacker profit more) (every time someone deposits or withdraws those values get updated in _refresh() and _refreshPAR() function).

    function _refresh() internal {
    if (_totalStake == 0) {
    return;
    }
    uint256 currentMimoBalance = _a.mimo().balanceOf(address(this));
    uint256 mimoReward = currentMimoBalance.sub(_mimoBalanceTracker);
    _mimoBalanceTracker = currentMimoBalance;
    _accMimoAmountPerShare = _accMimoAmountPerShare.add(mimoReward.rayDiv(_totalStakeWithBoost));
    }

    function _refreshPAR(uint256 newTotalStake) internal {
    if (_totalStake == 0) {
    return;
    }
    uint256 currentParBalance = _par.balanceOf(address(this)).sub(newTotalStake);
    uint256 parReward = currentParBalance.sub(_parBalanceTracker);

    _parBalanceTracker = currentParBalance;
    _accParAmountPerShare = _accParAmountPerShare.add(parReward.rayDiv(_totalStakeWithBoost));
    }

  3. Then attacker will deposit high amount of token to VotingEscrow so his balance in that contract become very high.

  4. In next step attacker will call updateBoost() of miner, and this function will update attacker stakeWithBoost based on attacker’s VotingEscrow balance(because of higher boost multipler, so userInfo.stakeWithBoost will be more higher) but userInfo.accAmountPerShare and userInfo.accParAmountPerShare will not be updated by this call. (in _getBoostMultiplier contract will get attacker balance in VotingEscrow to calculate boost multiplier)
    This is updateBoost() and _updateBoost() and _getBoostMultiplier code which shows how this happens:

    function updateBoost(address _user) public {
    UserInfo memory userInfo = _users[_user];
    _updateBoost(_user, userInfo);
    }

    function _updateBoost(address _user, UserInfo memory _userInfo) internal {
    // if user had a boost already, first remove it from the totalStakeWithBoost
    if (_userInfo.stakeWithBoost > 0) {
    _totalStakeWithBoost = _totalStakeWithBoost.sub(_userInfo.stakeWithBoost);
    }
    uint256 multiplier = _getBoostMultiplier(_user);
    _userInfo.stakeWithBoost = _userInfo.stake.wadMul(multiplier);
    _totalStakeWithBoost = _totalStakeWithBoost.add(_userInfo.stakeWithBoost);
    _users[_user] = _userInfo;
    }

    function _getBoostMultiplier(address _user) internal view returns (uint256) {
    uint256 veMIMO = _a.votingEscrow().balanceOf(_user);

    if (veMIMO == 0) return 1e18;

    // Convert boostConfig variables to signed 64.64-bit fixed point numbers
    int128 a = ABDKMath64x64.fromUInt(_boostConfig.a);
    int128 b = ABDKMath64x64.fromUInt(_boostConfig.b);
    int128 c = ABDKMath64x64.fromUInt(_boostConfig.c);
    int128 e = ABDKMath64x64.fromUInt(_boostConfig.e);
    int128 DECIMALS = ABDKMath64x64.fromUInt(1e18);

    int128 e1 = veMIMO.divu(_boostConfig.d); // x/25000
    int128 e2 = e1.sub(e); // x/25000 - 6
    int128 e3 = e2.neg(); // -(x/25000 - 6)
    int128 e4 = e3.exp(); // e^-(x/25000 - 6)
    int128 e5 = e4.add©; // 1 + e^-(x/25000 - 6)
    int128 e6 = b.div(e5).add(a); // 1 + 3/(1 + e^-(x/25000 - 6))
    uint64 e7 = e6.mul(DECIMALS).toUInt(); // convert back to uint64
    uint256 multiplier = uint256(e7); // convert to uint256

    require(multiplier >= 1e18 && multiplier <= _boostConfig.maxBoost, “LM103”);

    return multiplier;
    }

  5. Now attacker has higher amount for userInfo.stakeWithBoost (without updating userInfo.accAmountPerShare and userInfo.accParAmountPerShare ) so he will call releaseRewards() and this call will calculate attacker’s _pendingMIMO and _pendingPAR with unsync userInfo.stakeWithBoost and userInfo.accAmountPerShare values (becasue in step 4 contract didn’t update all of them and only increased userInfo.stakeWithBoost ) which will result in more reward for attacker, and contract will transfer those rewards to attacker(attacker will steal others rewards that are currently in contract address).
    These codes shows step 5 process:

    function releaseRewards(address _user) public override {
    UserInfo memory _userInfo = _users[_user];
    _releaseRewards(_user, _userInfo, _totalStake, false);
    _userInfo.accAmountPerShare = _accMimoAmountPerShare;
    _userInfo.accParAmountPerShare = _accParAmountPerShare;
    _updateBoost(_user, _userInfo);
    }

    function _releaseRewards(
    address _user,
    UserInfo memory _userInfo,
    uint256 _newTotalStake,
    bool _restakePAR
    ) internal {
    uint256 pendingMIMO = _pendingMIMO(_userInfo.stakeWithBoost, _userInfo.accAmountPerShare);
    _refresh();
    _refreshPAR(_newTotalStake);
    uint256 pendingPAR = _pendingPAR(_accParAmountPerShare, _userInfo.stakeWithBoost, _userInfo.accParAmountPerShare);
    if (_userInfo.stakeWithBoost > 0) {
    _mimoBalanceTracker = _mimoBalanceTracker.sub(pendingMIMO);
    _parBalanceTracker = _parBalanceTracker.sub(pendingPAR);
    }

    if (pendingPAR > 0 && !_restakePAR) {
    require(_par.transfer(_user, pendingPAR), “LM100”);
    }
    if (pendingMIMO > 0) {
    require(_a.mimo().transfer(_user, pendingMIMO), “LM100”);
    }
    }

    function _pendingMIMO(uint256 _userStakeWithBoost, uint256 _userAccAmountPerShare) internal view returns (uint256) {
    if (_totalStakeWithBoost == 0) {
    return 0;
    }
    uint256 currentBalance = _a.mimo().balanceOf(address(this));
    uint256 reward = currentBalance.sub(_mimoBalanceTracker);
    uint256 accMimoAmountPerShare = _accMimoAmountPerShare.add(reward.rayDiv(_totalStakeWithBoost));
    return _userStakeWithBoost.rayMul(accMimoAmountPerShare.sub(_userAccAmountPerShare));
    }

    /**
    Returns the number of PAR tokens the user has earned as a reward
    @return number of PAR tokens that will be sent automatically when staking/unstaking
    */
    function _pendingPAR(
    uint256 accParAmountPerShare,
    uint256 _userStakeWithBoost,
    uint256 _userAccParAmountPerShare
    ) internal view returns (uint256) {
    if (_totalStakeWithBoost == 0) {
    return 0;
    }
    return _userStakeWithBoost.rayMul(accParAmountPerShare.sub(_userAccParAmountPerShare));
    }

Attacker can do step 3,4,5 with a smart contract.

Tools Used

VIM

Recommended Mitigation Steps

before user’s stake amounts get updated, contract should first calculated rewards for that user. so updateBoost() most be like this:

  function updateBoost(address _user) public {
    releaseRewards(_user);
    UserInfo memory userInfo = _users[_user];
    _updateBoost(_user, userInfo);
  }

The text was updated successfully, but these errors were encountered:

All reactions