Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: high
Invalid

Incorrect Token Accounting in `veRAACToken::lock` and `veRAACToken::increase` Due to Fee-On-Transfer RAACToken

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();
// @audit-issue: Does not account for fee-on-transfer tokens
@> 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 {
// Increase lock using LockManager
_lockState.increaseLock(msg.sender, amount);
_updateBoostState(msg.sender, locks[msg.sender].amount);
// Update voting power
LockManager.Lock memory userLock = _lockState.locks[msg.sender];
(int128 newBias, int128 newSlope) = _votingState.calculateAndUpdatePower(
msg.sender,
userLock.amount + amount,
userLock.end
);
// Update checkpoints
uint256 newPower = uint256(uint128(newBias));
_checkpointState.writeCheckpoint(msg.sender, newPower);
// @audit-issue: Does not account for fee-on-transfer tokens
@> 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); // User receives reduced amount
}

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

  1. User attempts to lock or increase lock by 100e18 RAAC tokens.

  2. Due to fee-on-transfer, only 99e18 tokens (assuming 1% tax) arrive in the contract.

  3. 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);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

[INVALID] FoT RAAC breaks veRAACToken

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.