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);
}
Create Initial Lock: A user locks a certain amount of RAAC tokens for a specified duration.
Create New Lock: The same user creates a new lock with a different amount and/or duration.
Observe Token Loss: The user's previous lock is overwritten, and the previously locked tokens are lost.
The impact of this vulnerability is significant as it causes users to lose their previously locked tokens when they create a new lock. This can lead to substantial financial losses for users and erode trust in the protocol.
Solidity: The programming language used to write the smart contracts.
Manual Code Review: Reviewing the code to identify potential issues.
Foundry Tests: For POC
To fix this vulnerability, the lock function should be modified to account for existing locks and preserve the previously locked tokens. Here are some recommendations:
Check for Existing Locks: Before creating a new lock, check if the user already has an existing lock and handle it appropriately.
Combine Locks: If the user has an existing lock, combine the new lock with the existing lock to preserve the previously locked tokens.
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/contracts/core/tokens/RAACToken.sol";
import "../src/contracts/core/tokens/veRAACToken.sol";
import "../src/contracts/core/collectors/FeeCollector.sol";
contract FeeCollectorTest is Test {
RAACToken raacToken;
veRAACToken veRaacToken;
FeeCollector feeCollector;
address owner;
address user1;
address user2;
address treasury;
address newTreasury;
address repairFund;
address emergencyAdmin;
uint256 constant BASIS_POINTS = 10000;
uint256 constant WEEK = 7 * 24 * 3600;
uint256 constant ONE_YEAR = 365 * 24 * 3600;
uint256 constant INITIAL_MINT = 10000;
uint256 constant SWAP_TAX_RATE = 100;
uint256 constant BURN_TAX_RATE = 50;
struct FeeType {
uint256 veRAACShare;
uint256 burnShare;
uint256 repairShare;
uint256 treasuryShare;
}
function setUp() public {
owner = address(this);
user1 = address(1);
user2 = address(2);
treasury = address(3);
newTreasury = address(4);
repairFund = address(5);
emergencyAdmin = address(6);
raacToken = new RAACToken(owner, SWAP_TAX_RATE, BURN_TAX_RATE);
veRaacToken = new veRAACToken(address(raacToken));
feeCollector = new FeeCollector(address(raacToken), address(veRaacToken), treasury, repairFund, owner);
raacToken.setFeeCollector(address(feeCollector));
raacToken.manageWhitelist(address(feeCollector), true);
raacToken.manageWhitelist(address(veRaacToken), true);
raacToken.setMinter(owner);
veRaacToken.setMinter(owner);
feeCollector.grantRole(feeCollector.FEE_MANAGER_ROLE(), owner);
feeCollector.grantRole(feeCollector.EMERGENCY_ROLE(), emergencyAdmin);
feeCollector.grantRole(feeCollector.DISTRIBUTOR_ROLE(), owner);
raacToken.mint(owner, INITIAL_MINT * 100);
raacToken.approve(address(feeCollector), INITIAL_MINT * 100);
raacToken.mint(user1, INITIAL_MINT);
raacToken.mint(user2, INITIAL_MINT);
raacToken.mint(address(feeCollector), INITIAL_MINT * 100);
vm.prank(user1);
raacToken.approve(address(feeCollector), type(uint256).max);
vm.prank(user2);
raacToken.approve(address(feeCollector), type(uint256).max);
for (uint8 i = 0; i < 8; i++) {
feeCollector.updateFeeType(i, IFeeCollector.FeeType(5000, 1000, 1000, 3000));
}
vm.prank(user1);
raacToken.approve(address(veRaacToken), 10000);
}
function testOverwriteLock() public {
uint256 startingRaacBalance = raacToken.balanceOf(user1);
console.log("Starting RAAC balance:", startingRaacBalance);
vm.prank(user1);
raacToken.approve(address(veRaacToken), 100);
vm.prank(user1);
veRaacToken.lock(100, 365 days);
uint256 initialVeRaacBalance = veRaacToken.balanceOf(user1);
console.log("Initial veRAAC balance after first lock:", initialVeRaacBalance);
vm.prank(user1);
raacToken.approve(address(veRaacToken), 50);
vm.prank(user1);
veRaacToken.lock(50, 365 days);
uint256 finalVeRaacBalance = veRaacToken.balanceOf(user1);
console.log("Final veRAAC balance after overwriting lock:", finalVeRaacBalance);
forwardTime(365 days);
vm.prank(user1);
veRaacToken.withdraw();
uint256 raacBalanceAfterWithdrawal = raacToken.balanceOf(user1);
console.log("RAAC balance after withdrawal:", raacBalanceAfterWithdrawal);
uint256 raacBalanceDiff =startingRaacBalance - raacBalanceAfterWithdrawal;
console.log("Difference in RAAC balance after withdrawal:", raacBalanceDiff);
uint256 totalLost = (100 + 50) - raacBalanceDiff;
console.log("Total lost amount:", totalLost);
}
uint256 currentTime = block.timestamp;
function forwardTime(uint256 addTime) internal {
currentTime += addTime;
vm.warp(currentTime);
}