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();
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);
}
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
Use this guide to intergrate foundry into your project: foundry
Create a new file FortisAudits.t.sol
in the test
directory.
Add the following gist code to the file: Gist Code
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.