Summary
The veRAACToken::lock
and veRAACToken::increase
functions do not account for the fact that RAACToken
is a fee-on-transfer token. As a result, the contract may receive fewer tokens than expected when transferring funds, leading to inconsistencies in the lock and lock increase mechanisms.Vulnerability Details
Problem Description
The veRAACToken::lock
and veRAACToken::increase
functions transfer RAAC
tokens (RAACToken
) from the user to the contract using safeTransferFrom
. However, since RAACToken
applies a fee-on-transfer mechanism, the amount received by the contract will be less than the amount specified. This discrepancy is not accounted for, leading to incorrect calculations of locked amounts and voting power.
Affected Code in veRAACToken::lock
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();
@> raacToken.safeTransferFrom(msg.sender, address(this), amount);
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);
emit LockCreated(msg.sender, amount, unlockTime);
}
Code Reference: https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/tokens/veRAACToken.sol#L220
Affected Code in veRAACToken::increase
function increase(uint256 amount) external nonReentrant whenNotPaused {
_lockState.increaseLock(msg.sender, amount);
_updateBoostState(msg.sender, locks[msg.sender].amount);
LockManager.Lock memory userLock = _lockState.locks[msg.sender];
(int128 newBias, int128 newSlope) = _votingState.calculateAndUpdatePower(
msg.sender,
userLock.amount + amount,
userLock.end
);
uint256 newPower = uint256(uint128(newBias));
_checkpointState.writeCheckpoint(msg.sender, newPower);
@> raacToken.safeTransferFrom(msg.sender, address(this), amount);
_mint(msg.sender, newPower - balanceOf(msg.sender));
emit LockIncreased(msg.sender, amount);
}
Code Reference: https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/tokens/veRAACToken.sol#L269
RAACToken
Implementation with Fee-On-Transfer Mechanism
The RAACToken::_update
function deducts fees before completing the transfer:
function _update(
address from,
address to,
uint256 amount
) internal virtual override {
uint256 baseTax = swapTaxRate + burnTaxRate;
if (baseTax == 0 || from == address(0) || to == address(0) || whitelistAddress[from] || whitelistAddress[to] || feeCollector == address(0)) {
super._update(from, to, amount);
return;
}
uint256 totalTax = amount.percentMul(baseTax);
uint256 burnAmount = totalTax * burnTaxRate / baseTax;
super._update(from, feeCollector, totalTax - burnAmount);
super._update(from, address(0), burnAmount);
super._update(from, to, amount - totalTax);
}
Code Reference: https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/tokens/RAACToken.sol#L185
Since the transferred amount is reduced by tax fees, veRAACToken::lock
and veRAACToken::increase
will use the wrong amount for subsequent calculations, leading to inflated voting power and potential economic issues. Although there is a whitelist mechanism, veRAACToken
is not added as a whitelisted member in the protocol's testing suite, further exacerbating the issue.
Steps to Reproduce
User attempts to lock or increase lock by 100e18
RAAC tokens.
Due to fee-on-transfer, only 99e18
tokens (assuming 1% tax) arrive in the contract.
However, the functions assume 100e18
is locked, leading to incorrect voting power calculations and mismatched accounting.
Impact
Incorrect Voting Power Calculation: Since the actual locked amount is lower than expected, users may receive inflated veRAAC voting power.
Protocol Inconsistencies: Future withdrawals or governance mechanisms relying on voting power will be misaligned due to incorrect calculations.
Tools Used
Manual Review
Recommendations
Modify lock to check the actual balance received by the contract after transfer:
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();
+ uint256 balanceBefore = raacToken.balanceOf(address(this));
raacToken.safeTransferFrom(msg.sender, address(this), amount);
+ uint256 balanceAfter = raacToken.balanceOf(address(this));
+ uint256 actualReceived = balanceAfter - balanceBefore;
+ if (actualReceived < amount) {
+ amount = actualReceived; // Adjust lock amount to actual received tokens
+ }
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);
emit LockCreated(msg.sender, amount, unlockTime);
}
function increase(uint256 amount) external nonReentrant whenNotPaused {
uint256 balanceBefore = raacToken.balanceOf(address(this));
raacToken.safeTransferFrom(msg.sender, address(this), amount);
uint256 balanceAfter = raacToken.balanceOf(address(this));
+ uint256 actualReceived = balanceAfter - balanceBefore;
+ if (actualReceived < amount) {
+ amount = actualReceived; // Adjust lock amount to actual received tokens
+ }
_lockState.increaseLock(msg.sender, amount);
_updateBoostState(msg.sender, locks[msg.sender].amount);
_checkpointState.writeCheckpoint(msg.sender, actualReceived);
_mint(msg.sender, actualReceived);
emit LockIncreased(msg.sender, amount);
}