Summary
The treasury manages protocol treasury funds with role-based access control. However, SafeERC20 is not utilized in the Treasury.sol. As a result, the treasury will not work well with some wired tokens.
Vulnerability Details
There is a check for totalSupply limit in the veRAACToken.lock() function.
https://github.com/Cyfrin/2025-02-raac/blob/main/contracts/core/collectors/Treasury.sol#L216
if (totalSupply() + amount > MAX_TOTAL_SUPPLY) revert TotalSupplyLimitExceeded();
However, the added amount is a locked amount, not the minted amount.
https://github.com/Cyfrin/2025-02-raac/blob/main/contracts/core/collectors/Treasury.sol#L223-L241
@> uint256 unlockTime = block.timestamp + duration;
_lockState.createLock(msg.sender, amount, duration);
_updateBoostState(msg.sender, amount);
(int128 bias, int128 slope) = _votingState.calculateAndUpdatePower(
msg.sender,
@> amount,
@> unlockTime
);
@> uint256 newPower = uint256(uint128(bias));
_checkpointState.writeCheckpoint(msg.sender, newPower);
@> _mint(msg.sender, newPower);
When the duration is shorter than MAX_LOCK_DURATION, the minted amount is less than the locked amount.
https://github.com/Cyfrin/2025-02-raac/blob/main/contracts/libraries/governance/VotingPowerLib.sol#L75
@> uint256 initialPower = (amount * duration) / MAX_LOCK_DURATION;
@> bias = int128(int256(initialPower));
As a result, totalSupply() + amount will be smaller than the totalSupply() after locking, which could result in an unreasonable revert of locking.
Impact
Unreasonable reverts due to an improper max supply check.
Recommendations
function lock(uint256 amount, uint256 duration) external nonReentrant whenNotPaused {
if (amount == 0) revert InvalidAmount();
if (amount > MAX_LOCK_AMOUNT) revert AmountExceedsLimit();
- if (totalSupply() + amount > MAX_TOTAL_SUPPLY) revert TotalSupplyLimitExceeded();
if (duration < MIN_LOCK_DURATION || duration > MAX_LOCK_DURATION)
revert InvalidLockDuration();
// Do the transfer first - this will revert with ERC20InsufficientBalance if user doesn't have enough tokens
raacToken.safeTransferFrom(msg.sender, address(this), amount);
// Calculate unlock time
uint256 unlockTime = block.timestamp + duration;
// Create lock position
_lockState.createLock(msg.sender, amount, duration);
_updateBoostState(msg.sender, amount);
// Calculate initial voting power
(int128 bias, int128 slope) = _votingState.calculateAndUpdatePower(
msg.sender,
amount,
unlockTime
);
// Update checkpoints
uint256 newPower = uint256(uint128(bias));
_checkpointState.writeCheckpoint(msg.sender, newPower);
+ if (totalSupply() + newPower > MAX_TOTAL_SUPPLY) revert TotalSupplyLimitExceeded();
// Mint veTokens
_mint(msg.sender, newPower);
emit LockCreated(msg.sender, amount, unlockTime);
}