code-423n4 / 2023-07-moonwell-findings

1 stars 0 forks source link

Any user can claim rewards infinitely from the market without respecting the accrued rewards time #370

Closed code423n4 closed 10 months ago

code423n4 commented 11 months ago

Lines of code

https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/Comptroller.sol#L998-L1000 https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/Comptroller.sol#L1015-L1019 https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/Comptroller.sol#L1028-L1053

Vulnerability details

Impact

Proof of Concept

Line 998-1000

File: src/core/Comptroller.sol
Line 998-1000:
    function claimReward() public {
        claimReward(msg.sender, allMarkets);
    }

Line 1015-1019

File: src/core/Comptroller.sol
Line 1015-1019:
    function claimReward(address holder, MToken[] memory mTokens) public {
        address[] memory holders = new address[](1);
        holders[0] = holder;
        claimReward(holders, mTokens, true, true);
    }

Line 1028-1053

File: src/core/Comptroller.sol
Line 1028-1053:
    function claimReward(address[] memory holders, MToken[] memory mTokens, bool borrowers, bool suppliers) public {
        require(address(rewardDistributor) != address(0), "No reward distributor configured!");

        for (uint i = 0; i < mTokens.length; i++) {

            // Safety check that the supplied mTokens are active/listed
            MToken mToken = mTokens[i];
            require(markets[address(mToken)].isListed, "market must be listed");

            // Disburse supply side
            if (suppliers == true) {
                rewardDistributor.updateMarketSupplyIndex(mToken);
                for (uint holderIndex = 0; holderIndex < holders.length; holderIndex++) {
                    rewardDistributor.disburseSupplierRewards(mToken, holders[holderIndex], true);
                }
            }

            // Disburse borrow side
            if (borrowers == true) {
                rewardDistributor.updateMarketBorrowIndex(mToken);
                for (uint holderIndex = 0; holderIndex < holders.length; holderIndex++) {
                    rewardDistributor.disburseBorrowerRewards(mToken, holders[holderIndex], true);
                }
            }
        }
    }
  1. The test is copied from testRewards() function in Comptroller.t.sol file & modified to explain the vulnerability in details.

  2. The FaucetToken.sol helper test contract is modified by adding mint() function to the StandardToken contract:

contract StandardToken is ERC20 {
    uint8 _decimals;

    constructor(
        uint256 _initialAmount,
        string memory _tokenName,
        uint8 _decimalUnits,
        string memory _tokenSymbol
    ) ERC20(_tokenName, _tokenSymbol) {
        _mint(msg.sender, _initialAmount);
        _decimals = _decimalUnits;
    }

    function decimals() public view virtual override returns (uint8) {
        return _decimals;
    }

+   function mint(address account, uint256 amount) public returns (bool) {
+       _mint(account, amount);
+       return true;
+   }
}
  1. Add this test to the test/unit/Comptroller.t.sol file; where the following scenario is set: two users, originalDepositor & nonDepositor, are set; the originalDepositor is the one who depositing in the market, then claiming rewards,then transferring his mTokens to the nonDepositor to claim rewards again.
  function testRewardsWrecked() public {
        comptroller._setCollateralFactor(mToken, 0.5e18);

        uint time = 1678430000;
        vm.warp(time);
        // market emission configuration set by the admin of the comptroller contract (which is address(this))
        distributor._addEmissionConfig(
            mToken,
            address(this), //_owner
            address(faucetToken), //_emissionToken (reward token)
            0.5e18, //_supplyEmissionPerSec (set very high for the sake of seeing the difference)
            0, // _borrowEmissionsPerSec
            time + 86400 //_endTime
        );
        faucetToken.allocateTo(address(distributor), 100000e18);

        //--------------Setting up users--------------//
        address originlDepositor = address(0x1);
        address nonDepositor = address(0x2);
        uint256 depositedAmount = 1e18;

        uint256 distributorFaucetBalanceBefore = faucetToken.balanceOf(
            address(distributor)
        );

        //1. originlDepositor depositor mints mToken by directly depositing the ynderlying faucetToken in the mToken market:
        vm.startPrank(originlDepositor);
        faucetToken.mint(originlDepositor, depositedAmount);
        faucetToken.approve(address(mToken), type(uint256).max);
        mToken.mint(depositedAmount);
        assertEq(mToken.balanceOf(originlDepositor), depositedAmount);

        //2. originlDepositor depositor waits sometime (10 seconds) before claiming rewards (given in faucetToken):
        vm.warp(time + 10);
        comptroller.claimReward();
        assertEq(mToken.balanceOf(originlDepositor), depositedAmount);

        //3. originlDepositor got an extra 5 tokens (which is 10 (refers to the seconds) * 0.5 (emission/second) ):
        assertEq(faucetToken.balanceOf(originlDepositor), 5e18);
        assertEq(
            faucetToken.balanceOf(address(distributor)),
            distributorFaucetBalanceBefore - 5e18
        );

        //4. now originlDepositor transfers his mTokens to the nonDepositor:
        mToken.transfer(nonDepositor, mToken.balanceOf(originlDepositor));
        vm.stopPrank();
        assertEq(mToken.balanceOf(originlDepositor), 0);

        //5. the nonDepositor waits sometime (another 10 seconds) before claiming rewards (given in faucetToken):
        vm.startPrank(nonDepositor);
        vm.warp(time + 20);
        comptroller.claimReward();
        assertEq(faucetToken.balanceOf(nonDepositor), 5e18);
        assertEq(
            faucetToken.balanceOf(address(distributor)),
            distributorFaucetBalanceBefore - 10e18
        );

        //6. the nonDepositor returns back mTokens to the originalDepositor:
        mToken.transfer(originlDepositor, mToken.balanceOf(nonDepositor));
        vm.stopPrank();
        assertEq(mToken.balanceOf(nonDepositor), 0);
        assertEq(mToken.balanceOf(originlDepositor), depositedAmount);

        // so by the end of this operation: the nonDepositor was able to get rewards from tha market without being a real depositor;this can be exploited by any user by creating multi accounts and transferring the mTokens between to drain the MultiRewardDistributor contract from reward tokens without being elligble to!
    }
  1. Test result:
$ forge test --match-test testRewardsWrecked
Running 1 test for test/unit/Comptroller.t.sol:ComptrollerUnitTest
[PASS] testRewardsWrecked() (gas: 816089)
Test result: ok. 1 passed; 0 failed; finished in 5.22ms

Tools Used

Manual Testing & Foundry.

Recommended Mitigation Steps

Add a mechanism in the Comptroller contract to allow only original depositors & borrowers from claiming rewards.

Assessed type

Context

c4-pre-sort commented 11 months ago

0xSorryNotSorry marked the issue as primary issue

ElliotFriedman commented 11 months ago

sorry, but this isn't an issue and is expected behavior. rewards go to holders, not initial depositors.

PoC:

` function testRewardsNotWreckedAndFunctionProperly() public { comptroller._setCollateralFactor(mToken, 0.5e18);

    uint time = 1678430000;
    vm.warp(time);
    // market emission configuration set by the admin of the comptroller contract (which is address(this))
    distributor._addEmissionConfig(
        mToken,
        address(this), //_owner
        address(faucetToken), //_emissionToken (reward token)
        0.5e18, //_supplyEmissionPerSec (set very high for the sake of seeing the difference)
        0, // _borrowEmissionsPerSec
        time + 86400 //_endTime
    );
    faucetToken.allocateTo(address(distributor), 100000e18);

    //--------------Setting up users--------------//
    address originlDepositor = address(0x1);
    address nonDepositor = address(0x2);
    uint256 depositedAmount = 1e18;

    uint256 distributorFaucetBalanceBefore = faucetToken.balanceOf(
        address(distributor)
    );

    //1. originlDepositor depositor mints mToken by directly depositing the ynderlying faucetToken in the mToken market:
    vm.startPrank(originlDepositor);
    faucetToken.allocateTo(originlDepositor, depositedAmount);
    faucetToken.approve(address(mToken), type(uint256).max);
    mToken.mint(depositedAmount);
    assertEq(mToken.balanceOf(originlDepositor), depositedAmount);

    //2. originlDepositor depositor waits sometime (10 seconds) before claiming rewards (given in faucetToken):
    vm.warp(time + 10);
    comptroller.claimReward();
    assertEq(mToken.balanceOf(originlDepositor), depositedAmount);

    //3. originlDepositor got an extra 5 tokens (which is 10 (refers to the seconds) * 0.5 (emission/second) ):
    assertEq(faucetToken.balanceOf(originlDepositor), 5e18);
    assertEq(
        faucetToken.balanceOf(address(distributor)),
        distributorFaucetBalanceBefore - 5e18
    );

    //4. now originlDepositor transfers his mTokens to the nonDepositor:
    mToken.transfer(nonDepositor, mToken.balanceOf(originlDepositor));
    vm.stopPrank();
    assertEq(mToken.balanceOf(originlDepositor), 0);

    //5. the nonDepositor waits sometime (another 10 seconds) before claiming rewards (given in faucetToken):
    vm.startPrank(nonDepositor);
    vm.warp(time + 20);
    comptroller.claimReward();
    assertEq(faucetToken.balanceOf(nonDepositor), 5e18);
    assertEq(
        faucetToken.balanceOf(address(distributor)),
        distributorFaucetBalanceBefore - 10e18
    );

    uint256 startingRewardBalance = faucetToken.balanceOf(originlDepositor);
    comptroller.claimReward(originlDepositor);
    assertEq(startingRewardBalance, faucetToken.balanceOf(originlDepositor));

    //6. the nonDepositor returns back mTokens to the originalDepositor:
    mToken.transfer(originlDepositor, mToken.balanceOf(nonDepositor));
    vm.stopPrank();
    assertEq(mToken.balanceOf(nonDepositor), 0);
    assertEq(mToken.balanceOf(originlDepositor), depositedAmount);
}

`

c4-sponsor commented 11 months ago

ElliotFriedman marked the issue as sponsor disputed

alcueca commented 10 months ago

Agree with sponsor.

c4-judge commented 10 months ago

alcueca marked the issue as unsatisfactory: Invalid