Core Contracts

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

Fee-on-Transfer Mechanism Causes Insufficient Funds During Withdrawals in `veRAAC` Token Contract

Summary

The veRAACToken contract allows users to lock RAACToken tokens and later withdraw them. However, since RAACToken is a fee-on-transfer token with a combined swap and burn rate of 1.2%, the contract does not account for the reduced amount received during the lock function.

Vulnerability Details

In fee-on-transfer tokens like RAACToken, a portion of the tokens is deducted during each transfer as a fee. In this case, a 1.2% fee is applied, meaning that for every 100 tokens transferred, only 98.8 tokens are received by the recipient. The lock function in the veRAACToken contract transfers the full amount from the user to the contract but does not adjust the user's locked balance to reflect the 1.2% fee deduction.

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
// @audit : Total fees is not deducted from amount .
@-> _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);
// Mint veTokens
_mint(msg.sender, newPower);
emit LockCreated(msg.sender, amount, unlockTime);
}
  • As a result, the contract records a higher balance than it actually holds. When multiple users lock tokens, the cumulative discrepancy can lead to a situation where the contract lacks sufficient tokens to fulfill withdrawal requests, causing failures for users attempting to withdraw their locked tokens.

Impact

Users who lock their RAACToken tokens in the veRAACToken contract may encounter failures during withdrawal attempts due to the contract's insufficient token balance. This issue is particularly problematic for the last user attempting to withdraw, as the contract may have already depleted its token reserves due to the unaccounted fee deductions during the locking process.

Proof Of Concept (POC)

  • Mahi initiates a lock of 100 tokens into the contract.

  • Due to the 1.2% fee, 1.2 tokens are deducted, and the contract receives 98.8 tokens.

  • Similarly, Bluedragon locks 100 tokens.

  • After the 1.2% fee deduction, the contract receives another 98.8 tokens.

  • The contract now holds 197.6 tokens (98.8 tokens from Mahi + 98.8 tokens from Bluedragon).

  • Mahi requests to withdraw her original 100 tokens.

  • The contract, aiming to return the full amount Mahi deposited, transfers 100 tokens to her.

  • The contract now has 97.6 tokens remaining (197.6 initial balance - 100 tokens withdrawn by Mahi).

  • Bluedragon then attempts to withdraw his 100 tokens.

  • The contract only has 97.6 tokens remaining, insufficient to fulfill Bluedragon's withdrawal request.

  • Bluedragon's withdrawal transaction reverts due to the contract's inadequate balance.

Proof Of Code

  1. Use this guide to intergrate foundry into your project: foundry

  2. Create a new file FortisAudits.t.sol in the test directory.

  3. Add the following gist code to the file: Gist Code

  4. Run the test using forge test --mt test_Withdraw_HaveNotEnoughFundsForLastWithdrawer -vvvv.

function test_Withdraw_HaveNotEnoughFundsForLastWithdrawer() public {
address mahi = makeAddr("mahi");
address bluedragon = makeAddr("bluedragon");
address minter = makeAddr("minter");
uint256 amount = 100e6;
uint256 duration = 31536000 seconds ;
address gauge = makeAddr("gauge");
vm.startPrank(initialOwner);
raacToken.setMinter(minter);
vm.stopPrank();
vm.startPrank(minter);
raacToken.mint(mahi , amount);
raacToken.mint(bluedragon , amount);
vm.stopPrank();
vm.startPrank(mahi);
raacToken.approve(address(veraacToken), amount);
veraacToken.lock(amount, duration);
console.log("balance of mahi is", veraacToken.balanceOf(mahi));
vm.stopPrank();
vm.startPrank(bluedragon);
raacToken.approve(address(veraacToken), amount);
veraacToken.lock(amount, duration);
console.log("balance of bluedragon is", veraacToken.balanceOf(bluedragon));
vm.stopPrank();
vm.warp(duration + 1);
vm.startPrank(mahi);
veraacToken.withdraw();
vm.stopPrank();
vm.startPrank(bluedragon);
vm.expectRevert();
veraacToken.withdraw();
vm.stopPrank();
}

Logs :

Deposit Logs :

VM::startPrank(mahi: [0xE078F390B9AA9550c5a3fB332083B0B0E828c118])
│ └─ ← [Return]
├─ [24802] RAACToken::approve(veRAACToken: [0xcEc0514B3d15432092B21dd854c0B5Df66CE85f9], 100000000 [1e8])
│ ├─ emit Approval(owner: mahi: [0xE078F390B9AA9550c5a3fB332083B0B0E828c118], spender: veRAACToken: [0xcEc0514B3d15432092B21dd854c0B5Df66CE85f9], value: 100000000 [1e8])
│ └─ ← [Return] true
├─ [480052] veRAACToken::lock(100000000 [1e8], 31536000 [3.153e7])
│ ├─ [60942] RAACToken::transferFrom(mahi: [0xE078F390B9AA9550c5a3fB332083B0B0E828c118], veRAACToken: [0xcEc0514B3d15432092B21dd854c0B5Df66CE85f9], 100000000 [1e8])
│ │ ├─ emit Transfer(from: mahi: [0xE078F390B9AA9550c5a3fB332083B0B0E828c118], to: owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], value: 600000 [6e5])
│ │ ├─ emit Transfer(from: mahi: [0xE078F390B9AA9550c5a3fB332083B0B0E828c118], to: 0x0000000000000000000000000000000000000000, value: 600000 [6e5]) │ │ ├─ emit Transfer(from: mahi: [0xE078F390B9AA9550c5a3fB332083B0B0E828c118], to: veRAACToken: [0xcEc0514B3d15432092B21dd854c0B5Df66CE85f9], value: 98800000 [9.88e7])
│ │ └─ ← [Return] true
│ ├─ emit LockCreated(user: mahi: [0xE078F390B9AA9550c5a3fB332083B0B0E828c118], amount: 100000000 [1e8], unlockTime: 31536001 [3.153e7])
VM::startPrank(bluedragon: [0x95021a5059c26139a8d38E67D8b046B90f13982A])
│ └─ ← [Return]
├─ [24802] RAACToken::approve(veRAACToken: [0xcEc0514B3d15432092B21dd854c0B5Df66CE85f9], 100000000 [1e8])
│ ├─ emit Approval(owner: bluedragon: [0x95021a5059c26139a8d38E67D8b046B90f13982A], spender: veRAACToken: [0xcEc0514B3d15432092B21dd854c0B5Df66CE85f9], value: 100000000 [1e8])
│ └─ ← [Return] true
├─ [230013] veRAACToken::lock(100000000 [1e8], 31536000 [3.153e7])
│ ├─ [13142] RAACToken::transferFrom(bluedragon: [0x95021a5059c26139a8d38E67D8b046B90f13982A], veRAACToken: [0xcEc0514B3d15432092B21dd854c0B5Df66CE85f9], 100000000 [1e8])
│ │ ├─ emit Transfer(from: bluedragon: [0x95021a5059c26139a8d38E67D8b046B90f13982A], to: owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], value: 600000 [6e5])
│ │ ├─ emit Transfer(from: bluedragon: [0x95021a5059c26139a8d38E67D8b046B90f13982A], to: 0x0000000000000000000000000000000000000000, value: 600000
[6e5])
│ │ ├─ emit Transfer(from: bluedragon: [0x95021a5059c26139a8d38E67D8b046B90f13982A], to: veRAACToken: [0xcEc0514B3d15432092B21dd854c0B5Df66CE85f9], value: 98800000 [9.88e7])
│ │ └─ ← [Return] true
│ ├─ emit LockCreated(user: bluedragon: [0x95021a5059c26139a8d38E67D8b046B90f13982A], amount: 100000000 [1e8], unlockTime: 31536001 [3.153e7])

Withdraw Logs :

VM::startPrank(mahi: [0xE078F390B9AA9550c5a3fB332083B0B0E828c118])
│ └─ ← [Return]
├─ [64547] veRAACToken::withdraw()
│ ├─ emit CheckpointCreated(user: mahi: [0xE078F390B9AA9550c5a3fB332083B0B0E828c118], blockNumber: 1, power: 0)
│ ├─ emit Transfer(from: mahi: [0xE078F390B9AA9550c5a3fB332083B0B0E828c118], to: 0x0000000000000000000000000000000000000000, value: 25000000 [2.5e7]) │ ├─ [30188] RAACToken::transfer(mahi: [0xE078F390B9AA9550c5a3fB332083B0B0E828c118], 100000000 [1e8])
│ │ ├─ emit Transfer(from: veRAACToken: [0xcEc0514B3d15432092B21dd854c0B5Df66CE85f9], to: owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], value: 600000 [6e5])
│ │ ├─ emit Transfer(from: veRAACToken: [0xcEc0514B3d15432092B21dd854c0B5Df66CE85f9], to: 0x0000000000000000000000000000000000000000, value: 600000 [6e5])
│ │ ├─ emit Transfer(from: veRAACToken: [0xcEc0514B3d15432092B21dd854c0B5Df66CE85f9], to: mahi: [0xE078F390B9AA9550c5a3fB332083B0B0E828c118], value: 98800000 [9.88e7])
│ │ └─ ← [Return] true
│ ├─ emit Withdrawn(user: mahi: [0xE078F390B9AA9550c5a3fB332083B0B0E828c118], amount: 100000000 [1e8])
VM::startPrank(bluedragon: [0x95021a5059c26139a8d38E67D8b046B90f13982A])
│ └─ ← [Return]
├─ [0] VM::expectRevert(custom error 0xf4844814)
│ └─ ← [Return]
├─ [40602] veRAACToken::withdraw()
│ ├─ emit CheckpointCreated(user: bluedragon: [0x95021a5059c26139a8d38E67D8b046B90f13982A], blockNumber: 1, power: 0)
│ ├─ emit Transfer(from: bluedragon: [0x95021a5059c26139a8d38E67D8b046B90f13982A], to: 0x0000000000000000000000000000000000000000, value: 25000000 [2.5e7])
│ ├─ [7878] RAACToken::transfer(bluedragon: [0x95021a5059c26139a8d38E67D8b046B90f13982A], 100000000 [1e8])
│ │ ├─ emit Transfer(from: veRAACToken: [0xcEc0514B3d15432092B21dd854c0B5Df66CE85f9], to: owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], value: 600000 [6e5])
│ │ ├─ emit Transfer(from: veRAACToken: [0xcEc0514B3d15432092B21dd854c0B5Df66CE85f9], to: 0x0000000000000000000000000000000000000000, value: 600000 [6e5])
│ │ └─ ← [Revert] ERC20InsufficientBalance(0xcEc0514B3d15432092B21dd854c0B5Df66CE85f9, 96400000 [9.64e7], 98800000 [9.88e7])
│ └─ ← [Revert] ERC20InsufficientBalance(0xcEc0514B3d15432092B21dd854c0B5Df66CE85f9, 96400000 [9.64e7], 98800000 [9.88e7])
├─ [0] VM::stopPrank()

Tools Used

Manual Review

Recommendation

To address this issue, the lock function should be modified to account for the 1.2% fee-on-transfer deduction. After transferring the tokens from the user to the contract, the actual amount received by the contract should be calculated and used to update the user's locked balance.

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 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.