LOT holders can stake and unstake in the same block, and receive rewards for the tickets bought in the interim. This means they are leeching on the rewards without taking any risk. They can listen in the mempool for purchased tickets, then sandwich them between staking-and-unstaking to get the reward without actually staking. This attack is especially more profitable when the number of stakers are small and the number of tickets purchased is big. For example, somebody buying tickets for his favorite numbers for a whole year ahead.
Proof of Concept
The POC for this is given below:
function testGetRewardsSingleStaker() public {
vm.prank(address(lottery));
uint256 preRewardClaimBalance;
stakingToken.mint(STAKER, 100); //mints user some tokens
emit log_named_decimal_uint("balance", stakingToken.balanceOf(STAKER),18); //starting balance
vm.startPrank(STAKER);
stakingToken.approve(address(staking), 1);
for(uint256 i=0; i< 3; i++) { //3 times.
stakingToken.approve(address(staking), 1);
staking.stake(1); //stakes 1 token
buySameTickets(lottery.currentDraw()+uint128(i), uint120(0x0F), address(0), 4); //someone buys tickets
preRewardClaimBalance = rewardToken.balanceOf(STAKER); //starting balance
assertEq(staking.earned(STAKER), TICKET_FEE * 4); //check user earnings
staking.getReward(); //get the reward
assertEq(rewardToken.balanceOf(STAKER) - preRewardClaimBalance, TICKET_FEE * 4); //make sure his balance increased
staking.withdraw(1); //unstake
vm.stopPrank();
finalizeDraw(0); //finalize the current draw. past forward to next timestamp etc.
vm.startPrank(STAKER);
stakingToken.approve(address(staking), 1); //approve for next stake
emit log_named_decimal_uint("balance", stakingToken.balanceOf(STAKER),18);
emit log_named_decimal_uint("balance", rewardToken.balanceOf(STAKER),18);
}
}
Basically how this works is:
User waits for some rewards to be accumulated in the staking contract.
He stakes some LOT tokens using the stake function.
In the same block, he calls the getReward function. Which sends him rewards as per the current reward ratio.
Again, in the same block he calls the withdraw function and unstakes all his tokens.
He waits some more lottery tickets to be sold so that more rewards are accumulated.
And then repeat steps 1-5.
In this specific POC, user does this 3 times and in between each try 4 tickets are bought. In the end his rewardToken balance was 12000000000000000000.
This happens because the earned function doesnt calculate the rewards correctly.
You can see that the rewards are independent of the amount of time the tokens are held in the contract. Instead it only depends on the current balance.
Tools Used
VS code, Foundry, Manual analysis
Recommended Mitigation Steps
Adjust the formula to make sure that the holding time is accounted in determining the rewards for a user.
Lines of code
https://github.com/code-423n4/2023-03-wenwin/blob/91b89482aaedf8b8feb73c771d11c257eed997e8/src/staking/Staking.sol#L61 https://github.com/code-423n4/2023-03-wenwin/blob/91b89482aaedf8b8feb73c771d11c257eed997e8/src/staking/Staking.sol#L91
Vulnerability details
Impact
LOT holders can stake and unstake in the same block, and receive rewards for the tickets bought in the interim. This means they are leeching on the rewards without taking any risk. They can listen in the mempool for purchased tickets, then sandwich them between staking-and-unstaking to get the reward without actually staking. This attack is especially more profitable when the number of stakers are small and the number of tickets purchased is big. For example, somebody buying tickets for his favorite numbers for a whole year ahead.
Proof of Concept
The POC for this is given below:
Basically how this works is:
stake
function.getReward
function. Which sends him rewards as per the current reward ratio.withdraw
function and unstakes all his tokens.In this specific POC, user does this 3 times and in between each try 4 tickets are bought. In the end his
rewardToken
balance was12000000000000000000
.This happens because the earned function doesnt calculate the rewards correctly.
You can see that the rewards are independent of the amount of time the tokens are held in the contract. Instead it only depends on the current balance.
Tools Used
VS code, Foundry, Manual analysis
Recommended Mitigation Steps
Adjust the formula to make sure that the holding time is accounted in determining the rewards for a user.