Incorrect Period Transition Logic in Reward Distribution
Summary
The updatePeriod
function contains a critical flaw in the logic used to calculate the start of the next reward period. This error can lead to gaps in reward distribution, resulting in users missing out on potential rewards. The issue arises from the incorrect calculation of the next period's start time, which can significantly impact user experience and financial outcomes.
Code Issue
The problematic line of code is as follows:
uint256 nextPeriodStart = ((currentTime / periodDuration) + 2) * periodDuration;
Concrete Example
-
Period Duration: 7 days (604800 seconds)
-
Deployment Time: 1717100000 (Wed May 29 2024 12:13:20 UTC)
-
First Period Start Calculation:
-
Incorrect Calculation:
-
Correct Calculation:
Impact Visualization
[Period 1] [Gap] [Period 2]
|----------|------------------------------|----------|
Start End Start End
Impact
User Experience
Financial Impact
Users could lose out on significant rewards, especially in systems where timely reward claims are critical. The financial implications can be substantial, particularly in high-frequency trading or yield farming scenarios where every moment counts.
Exploitation Risk
PoC
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../contracts/core/governance/gauges/BaseGauge.sol";
import "../contracts/interfaces/core/governance/gauges/IGauge.sol";
import "./mocks/MockERC20.sol";
import "./mocks/MockVeToken.sol";
* @title TestGauge
* @notice Mock implementation of BaseGauge for testing purposes
* @dev Implements required abstract functions with fixed values for testing
*/
contract TestGauge is BaseGauge {
constructor(
address _rewardToken,
address _stakingToken,
address _controller,
uint256 _maxEmission,
uint256 _periodDuration
)
BaseGauge(
_rewardToken,
_stakingToken,
_controller,
_maxEmission,
_periodDuration
)
{}
function _getBaseWeight(address) internal pure override returns (uint256) {
return 1000;
}
}
* @title MockGaugeController
* @notice Mock controller implementation for testing gauge interactions
* @dev Simulates basic controller functionality required for tests
*/
contract MockGaugeController {
MockVeToken public veRAACToken;
constructor(address _veToken) {
veRAACToken = MockVeToken(_veToken);
}
function getGaugeWeight(address) external pure returns (uint256) {
return 1000;
}
function getVotingPower(address user) external view returns (uint256) {
return veRAACToken.getLockedAmount(user);
}
}
* @title BaseGaugeTest
* @notice Comprehensive test suite for BaseGauge contract
* @dev Tests are organized into phases focusing on different aspects of functionality
*/
contract BaseGaugeTest is Test {
TestGauge public gauge;
MockERC20 public rewardToken;
MockERC20 public stakingToken;
MockVeToken public veToken;
MockGaugeController public controller;
address public admin = address(this);
address public user1 = address(0x1);
address public user2 = address(0x2);
uint256 constant PERIOD_DURATION = 7 days;
uint256 constant MAX_EMISSION = 1000e18;
uint256 constant WEEK = 7 days;
uint256 constant BASIS_POINTS = 10000;
event DirectionVoted(
address indexed user,
uint256 direction,
uint256 votingPower
);
event EmissionUpdated(uint256 emission);
event PeriodUpdated(uint256 timestamp, uint256 avgWeight);
function setUp() public {
console.log("Setting up test environment...");
rewardToken = new MockERC20();
stakingToken = new MockERC20();
veToken = new MockVeToken();
controller = new MockGaugeController(address(veToken));
console.log("Deployed mock tokens:");
console.log("- Reward Token:", address(rewardToken));
console.log("- Staking Token:", address(stakingToken));
console.log("- veToken:", address(veToken));
console.log("- Controller:", address(controller));
gauge = new TestGauge(
address(rewardToken),
address(stakingToken),
address(controller),
MAX_EMISSION,
PERIOD_DURATION
);
console.log("Deployed TestGauge:", address(gauge));
rewardToken.mint(address(gauge), 10000e18);
stakingToken.mint(user1, 1000e18);
stakingToken.mint(user2, 1000e18);
veToken.mint(user1, 100e18);
veToken.mint(user2, 100e18);
console.log("Initial token balances set up");
console.log(
"- Gauge reward balance:",
rewardToken.balanceOf(address(gauge))
);
console.log("- User1 staking balance:", stakingToken.balanceOf(user1));
console.log("- User2 staking balance:", stakingToken.balanceOf(user2));
vm.startPrank(user1);
stakingToken.approve(address(gauge), type(uint256).max);
vm.stopPrank();
vm.startPrank(user2);
stakingToken.approve(address(gauge), type(uint256).max);
vm.stopPrank();
vm.startPrank(address(this));
bytes32 adminRole = gauge.DEFAULT_ADMIN_ROLE();
gauge.grantRole(adminRole, address(this));
gauge.grantRole(gauge.CONTROLLER_ROLE(), address(controller));
gauge.grantRole(gauge.CONTROLLER_ROLE(), address(this));
gauge.grantRole(gauge.FEE_ADMIN(), address(this));
vm.stopPrank();
console.log("Setup complete - roles granted and approvals set");
}
function testNextPeriodStartCalculation() public {
uint256 periodDuration = 7 days;
uint256 deploymentTime = 1717100000;
vm.warp(deploymentTime);
gauge.setInitialWeight(1000);
uint256 expectedNextPeriodStart = ((deploymentTime / periodDuration) +
1) * periodDuration;
uint256 actualNextPeriodStart = gauge.getCurrentPeriodStart();
console.log("Expected Next Period Start:", expectedNextPeriodStart);
console.log("Actual Next Period Start:", actualNextPeriodStart);
assertEq(
actualNextPeriodStart,
expectedNextPeriodStart,
"Next period start time is incorrect"
);
}
}
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MockERC20 is ERC20 {
uint8 private _decimals = 18;
constructor() ERC20("Mock Token", "MOCK") {}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
function setDecimals(uint8 decimals_) external {
_decimals = decimals_;
}
function decimals() public view override returns (uint8) {
return _decimals;
}
function approve(address spender, uint256 amount) public virtual override returns (bool) {
_approve(_msgSender(), spender, amount);
return true;
}
}
pragma solidity ^0.8.19;
import "../../contracts/interfaces/core/tokens/IveRAACToken.sol";
* @notice A mock implementation of IveRAACToken for testing
*/
contract MockVeToken is IveRAACToken {
uint256 public totalSupplyVal = 10000e18;
mapping(address => uint256) public balances;
mapping(address => LockPosition) public lockPositions;
mapping(address => mapping(address => uint256)) public allowances;
mapping(address => uint256) private _votingPower;
address public minterAddress;
function setBalance(address user, uint256 amount) external {
balances[user] = amount;
lockPositions[user] = LockPosition({
amount: amount,
end: block.timestamp + 365 days,
power: amount
});
}
function balanceOf(address user) external view returns (uint256) {
return balances[user];
}
function totalSupply() external view returns (uint256) {
return totalSupplyVal;
}
function getVotingPower(address user) external view returns (uint256) {
return lockPositions[user].power;
}
function getVotingPower(address user, uint256) external view returns (uint256) {
return lockPositions[user].power;
}
function getTotalVotingPower() external view returns (uint256) {
return totalSupplyVal;
}
function getTotalVotingPower(uint256) external view returns (uint256) {
return totalSupplyVal;
}
function getLockPosition(address user) external view returns (LockPosition memory) {
return lockPositions[user];
}
function setTotalSupply(uint256 amount) external {
totalSupplyVal = amount;
}
function lock(uint256 amount, uint256 duration) external override {
balances[msg.sender] = amount;
lockPositions[msg.sender] = LockPosition({
amount: amount,
end: block.timestamp + duration,
power: amount
});
}
function extend(uint256 newDuration) external override {
LockPosition storage position = lockPositions[msg.sender];
position.end = block.timestamp + newDuration;
}
function increase(uint256 amount) external override {
LockPosition storage position = lockPositions[msg.sender];
position.amount += amount;
position.power += amount;
balances[msg.sender] += amount;
}
function withdraw() external override {
LockPosition storage position = lockPositions[msg.sender];
require(block.timestamp >= position.end, "Lock not expired");
uint256 amount = position.amount;
position.amount = 0;
position.power = 0;
balances[msg.sender] = 0;
}
function setMinter(address minter) external override {
minterAddress = minter;
}
function calculateVeAmount(uint256 amount, uint256) external pure returns (uint256) {
return amount;
}
function transfer(address to, uint256 amount) external override returns (bool) {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[to] += amount;
return true;
}
function transferFrom(address from, address to, uint256 amount) external override returns (bool) {
require(allowances[from][msg.sender] >= amount, "Insufficient allowance");
require(balances[from] >= amount, "Insufficient balance");
allowances[from][msg.sender] -= amount;
balances[from] -= amount;
balances[to] += amount;
return true;
}
function approve(address spender, uint256 amount) external returns (bool) {
allowances[msg.sender][spender] = amount;
return true;
}
function getLockedAmount(address user) external view returns (uint256) {
return lockPositions[user].amount;
}
function mint(address to, uint256 amount) external {
require(msg.sender == minterAddress || minterAddress == address(0), "Not minter");
balances[to] += amount;
totalSupplyVal += amount;
LockPosition storage position = lockPositions[to];
position.amount += amount;
position.power += amount;
if (position.end == 0) {
position.end = block.timestamp + 365 days;
}
}
function setLockedAmount(address user, uint256 amount) external {
LockPosition storage position = lockPositions[user];
position.amount = amount;
position.power = amount;
if (position.end == 0) {
position.end = block.timestamp + 365 days;
}
}
function setVotingPower(address account, uint256 amount) external {
_votingPower[account] = amount;
}
}
Recommendations
Correct the Period Calculation Logic: Update the calculation of nextPeriodStart
to ensure that it accurately reflects the end of the current period and the start of the next one. The corrected line should be:
uint256 nextPeriodStart = ((currentTime / periodDuration) + 1) * periodDuration;
Thorough Testing: Implement comprehensive tests to cover various scenarios, including edge cases where the current time is close to the end of a period. This will help ensure that the period transition logic works as intended.
User Communication: Inform users about the changes made to the period calculation logic and the importance of timely reward distribution. Transparency can help rebuild trust in the system.
Monitoring and Alerts: Set up monitoring for reward distribution events to quickly identify and address any future discrepancies in reward calculations.