Core Contracts

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

Create a lock amount can override the previous locked amount.

Summary

A user can override their locked amount in the veRAACToken if they lock another amount without being refunded.

Vulnerability Details

In the lock function, the createLock function is used to create a lock for the user. However, the issue is that the function does not check whether a lock already exists, as we can see here:

function createLock(LockState storage state, address user, uint256 amount, uint256 duration)
internal
returns (uint256 end)
{
// Validation logic remains the same
if (state.minLockDuration != 0 && state.maxLockDuration != 0) {
if (duration < state.minLockDuration || duration > state.maxLockDuration) {
revert InvalidLockDuration();
}
}
if (amount == 0) revert InvalidLockAmount();
end = block.timestamp + duration;
state.locks[user] = Lock({amount: amount, end: end, exists: true});
state.totalLocked += amount;
emit LockCreated(user, amount, end);
return end;
}

The lock in the lock state wil be overrided by the new lock.

In order to run this coded POC you will have to add foundry to the project to do so you will have to follow 4 steps :

run npm install --save-dev @nomicfoundation/hardhat-foundry

add require("@nomicfoundation/hardhat-foundry"); to the hardhat.config file

run npx hardhat init-foundry

remove The ReserveLibraryMock from the test folder because this file countain errors that will make the compilation imposible for foundry.

you can now copy paste this code in a solidity file in the test folder and run forge test --mt test_lockTwoTimePOC -vv.

// SPDX-License-Identifier: GPL-2.0
pragma solidity ^0.8.0;
import {Test, console} from "forge-std/Test.sol";
import {crvUSDToken} from "contracts/mocks/core/tokens/crvUSDToken.sol";
import {RAACToken} from "contracts/core/tokens/RAACToken.sol";
import {veRAACToken} from "contracts/core/tokens/veRAACToken.sol";
import {RAACReleaseOrchestrator} from "contracts/core/minters/RAACReleaseOrchestrator/RAACReleaseOrchestrator.sol";
import {RAACHousePrices} from "contracts/core/primitives/RAACHousePrices.sol";
import {RAACNFT} from "contracts/core/tokens/RAACNFT.sol";
import {RToken} from "contracts/core/tokens/RToken.sol";
import {DebtToken} from "contracts/core/tokens/DebtToken.sol";
import {LendingPool, ILendingPool} from "contracts/core/pools/LendingPool/LendingPool.sol";
import {Treasury} from "contracts/core/collectors/Treasury.sol";
import {FeeCollector} from "contracts/core/collectors/FeeCollector.sol";
import {DEToken} from "contracts/core/tokens/DEToken.sol";
import {StabilityPool} from "contracts/core/pools/StabilityPool/StabilityPool.sol";
import {RAACMinter} from "contracts/core/minters/RAACMinter/RAACMinter.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC4626} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
import {Auction} from "contracts/zeno/Auction.sol";
import {ZENO} from "contracts/zeno/ZENO.sol";
import {ERC20Mock} from "contracts/mocks/core/tokens/ERC20Mock.sol";
import {TimelockController} from "contracts/core/governance/proposals//TimelockController.sol";
import {Governance} from "contracts/core/governance/proposals//Governance.sol";
import {RAACGauge} from "contracts/core/governance/gauges/RAACGauge.sol";
import {RWAGauge} from "contracts/core/governance/gauges/RWAGauge.sol";
import {BoostController} from "contracts/core/governance/boost/BoostController.sol";
import {GaugeController} from "contracts/core/governance/gauges/GaugeController.sol";
import {IGaugeController} from "contracts/interfaces/core/governance/gauges/IGaugeController.sol";
import {IGovernance} from "contracts/interfaces/core/governance/proposals/IGovernance.sol";
import {ReserveLibrary} from "contracts/libraries/pools/ReserveLibrary.sol";
import {LockManager} from "contracts/libraries/governance/LockManager.sol";
import {VotingPowerLib} from "contracts/libraries/governance/VotingPowerLib.sol";
import {TimeWeightedAverage} from "contracts/libraries/math/TimeWeightedAverage.sol";
import "contracts/libraries/math/WadRayMath.sol";
contract CodedPOC is Test{
address constant BOB = address(0x10000);
address constant ALICE = address(0x20000);
address constant CHARLIE = address(0x30000);
address[] internal users;
address owner = makeAddr("Owner");
address[] public proposers = [makeAddr("Proposers")];
address[] public executors = [makeAddr("Executors")];
// protocol contracts
crvUSDToken crvUSD;
RAACToken raac;
veRAACToken veRAAC;
RAACReleaseOrchestrator raacReleaseOrchestrator;
RAACHousePrices raacHousePrices;
RAACNFT raacNFT;
RToken rToken;
DebtToken debtToken;
LendingPool lendingPool;
Treasury treasury;
FeeCollector feeCollector;
DEToken deToken;
StabilityPool stabilityPool;
RAACMinter raacMinter;
Treasury repairFund;
CurveMock curveMock;
ERC20Mock usdc;
ERC20Mock rewardToken;
ZENO currentZeno;
ZENO[] zenos;
Auction currentAuction;
Auction[] auctions;
TimelockController timelock;
Governance governance;
GaugeController gaugeController;
RAACGauge raacGauge;
RWAGauge rwaGauge;
BoostController boostController;
uint256[] proposalIds;
address[] gauges;
uint256 timestampBefore;
function setUp() public {
vm.warp(1524785992);
vm.roll(4370000);
users = [BOB, ALICE, CHARLIE];
vm.label(BOB, "Bob");
vm.label(ALICE, "Alice");
vm.label(CHARLIE, "Charlie");
vm.label(owner, "Owner");
vm.label(address(this), "Test");
crvUSD = new crvUSDToken(owner);
curveMock = new CurveMock(address(crvUSD));
vm.label(address(curveMock), "CurveMock");
vm.prank(owner);
crvUSD.setMinter(address(this));
raac = new RAACToken(owner, 200, 50);
vm.label(address(raac), "RAAC");
veRAAC = new veRAACToken(address(raac));
vm.label(address(veRAAC), "veRAAC");
raacReleaseOrchestrator = new RAACReleaseOrchestrator(owner);
vm.label(address(raacReleaseOrchestrator), "RAACReleaseOrchestrator");
raacHousePrices = new RAACHousePrices(owner);
raacNFT = new RAACNFT(address(crvUSD), address(raacHousePrices), owner);
rToken = new RToken("RToken", "RT", owner, address(crvUSD));
debtToken = new DebtToken("DebtToken", "DT", owner);
lendingPool = new LendingPool(
address(crvUSD), address(rToken), address(debtToken), address(raacNFT), address(raacHousePrices), 0.1e27
);
lendingPool.setPrimeRateOracle(address(owner));
//lendingPool.setCurveVault(address(curveMock));
treasury = new Treasury(address(owner));
repairFund = new Treasury(address(owner));
feeCollector =
new FeeCollector(address(raac), address(veRAAC), address(treasury), address(repairFund), address(owner));
deToken = new DEToken("DEToken", "DET", owner, address(rToken));
stabilityPool = new StabilityPool(address(owner));
raacMinter = new RAACMinter(address(raac), address(stabilityPool), address(lendingPool), address(treasury));
vm.prank(owner);
raac.setFeeCollector(address(feeCollector));
vm.prank(owner);
raac.manageWhitelist(address(feeCollector), true);
vm.prank(owner);
raac.manageWhitelist(address(veRAAC), true);
vm.prank(owner);
raac.manageWhitelist(address(owner), true);
vm.prank(owner);
raac.setMinter(owner);
// raac.mint(BOB, 1_000_000e18);
// raac.mint(ALICE, 1_000_000e18);
// raac.mint(CHARLIE, 1_000_000e18);
usdc = new ERC20Mock("USDC", "USDC");
vm.label(address(usdc), "USDC");
vm.prank(owner);
raac.setMinter(address(raacMinter));
bytes32 role = feeCollector.FEE_MANAGER_ROLE();
vm.prank(owner);
feeCollector.grantRole(role, owner);
role = feeCollector.EMERGENCY_ROLE();
vm.prank(owner);
feeCollector.grantRole(role, owner);
role = feeCollector.DISTRIBUTOR_ROLE();
vm.prank(owner);
feeCollector.grantRole(role, owner);
vm.prank(owner);
rToken.setReservePool(address(lendingPool));
vm.prank(owner);
debtToken.setReservePool(address(lendingPool));
vm.prank(owner);
deToken.setStabilityPool(address(stabilityPool));
vm.prank(owner);
// Set up minter configuration
raac.transferOwnership(address(raacMinter));
vm.prank(owner);
rToken.transferOwnership(address(lendingPool));
vm.prank(owner);
debtToken.transferOwnership(address(lendingPool));
vm.prank(owner);
stabilityPool.initialize(
address(rToken), address(deToken), address(raac), address(raacMinter), address(crvUSD), address(lendingPool)
);
// Set up lending pool configuration
lendingPool.setStabilityPool(address(stabilityPool));
vm.prank(owner);
raacHousePrices.setOracle(owner);
vm.prank(owner);
stabilityPool.setRAACMinter(address(raacMinter));
_createAuction();
rewardToken = new ERC20Mock("RewardToken", "RT");
timelock = new TimelockController(2 days, proposers, executors, owner);
governance = new Governance(address(veRAAC), address(timelock));
role = timelock.PROPOSER_ROLE();
vm.prank(owner);
timelock.grantRole(role, address(governance));
role = timelock.EXECUTOR_ROLE();
vm.prank(owner);
timelock.grantRole(role, address(governance));
role = timelock.CANCELLER_ROLE();
vm.prank(owner);
timelock.grantRole(role, address(governance));
gaugeController = new GaugeController(address(veRAAC));
raacGauge = new RAACGauge(address(rewardToken), address(raac), address(gaugeController));
vm.label(address(raacGauge), "RAACGauge");
gaugeController.addGauge(address(raacGauge), IGaugeController.GaugeType.RAAC, 10000);
raacGauge.grantRole(raacGauge.CONTROLLER_ROLE(), owner);
raacGauge.grantRole(raacGauge.FEE_ADMIN(), address(owner));
raacGauge.grantRole(raacGauge.EMERGENCY_ADMIN(), address(owner));
vm.prank(owner);
raacGauge.setBoostParameters(25000, 10000, 1 weeks);
vm.prank(owner);
raacGauge.setDistributionCap(10000000e18);
vm.prank(owner);
// Set initial weight after time alignment
raacGauge.setInitialWeight(5000); // 50% weight
rwaGauge = new RWAGauge(address(rewardToken), address(raac), address(gaugeController));
vm.label(address(rwaGauge), "RWAGauge");
gaugeController.addGauge(address(rwaGauge), IGaugeController.GaugeType.RWA, 10000);
rwaGauge.grantRole(rwaGauge.CONTROLLER_ROLE(), address(owner));
rwaGauge.grantRole(rwaGauge.FEE_ADMIN(), address(owner));
rwaGauge.grantRole(rwaGauge.EMERGENCY_ADMIN(), address(owner));
// Initialize boost parameters
vm.prank(owner);
rwaGauge.setBoostParameters(
25000, // maxBoost (2.5x = 25000 basis points)
10000, // minBoost (1x = 10000 basis points)
7 * 24 * 3600 // boostWindow (7 days)
);
vm.prank(owner);
// Set distribution cap
rwaGauge.setDistributionCap(10000000e18);
vm.prank(owner);
// Set monthly emission
rwaGauge.setMonthlyEmission(100000e18);
boostController = new BoostController(address(veRAAC));
boostController.modifySupportedPool(address(lendingPool), true);
gauges = [address(raacGauge), address(rwaGauge)];
for (uint256 i; i < users.length; i++) {
crvUSD.mint(users[i], 1_000_000_000e18);
vm.prank(users[i]);
crvUSD.approve(address(raacNFT), type(uint256).max);
vm.prank(users[i]);
crvUSD.approve(address(lendingPool), type(uint256).max);
vm.prank(users[i]);
debtToken.approve(address(lendingPool), type(uint256).max);
vm.prank(users[i]);
rToken.approve(address(stabilityPool), type(uint256).max);
vm.prank(users[i]);
raac.approve(address(veRAAC), type(uint256).max);
usdc.mint(users[i], 1_000_000_000e18);
vm.prank(users[i]);
raac.approve(address(raacGauge), type(uint256).max);
vm.prank(users[i]);
raac.approve(address(rwaGauge), type(uint256).max);
}
timestampBefore = block.timestamp;
}
function _createAuction() internal {
currentZeno = new ZENO(address(usdc), 365 days, "ZENO", "ZN", owner);
currentAuction = new Auction(
address(currentZeno),
address(usdc),
address(currentZeno),
block.timestamp + 1 days,
block.timestamp + 11 days,
500,
10,
1000e18,
owner
);
vm.prank(owner);
currentZeno.transferOwnership(address(currentAuction));
for (uint256 i; i < users.length; i++) {
vm.prank(users[i]);
usdc.approve(address(currentAuction), type(uint256).max);
}
auctions.push(currentAuction);
zenos.push(currentZeno);
}
function test_lockTwoTimePOC() public {
vm.startPrank(CHARLIE);
//charlie deposit in the lending and stability pool
lendingPool.deposit(10_000e18);
stabilityPool.deposit(rToken.balanceOf(CHARLIE));
//we let some time pass
vm.warp(block.timestamp+ 6003);
vm.roll(block.number + uint256(6003)/15);
//Charlie withdraw his de tokens to get the raac tokens
stabilityPool.withdraw(deToken.balanceOf(CHARLIE));
veRAAC.lock(raac.balanceOf(CHARLIE)/2,31554267);
uint256 balanceOfCharlie = veRAAC.balanceOf(CHARLIE);
uint256 charlieVotingPower = veRAAC.getVotingPower(CHARLIE);
assertEq(balanceOfCharlie, charlieVotingPower);
veRAAC.lock(raac.balanceOf(CHARLIE)/2,31537156);
vm.stopPrank();
balanceOfCharlie = veRAAC.balanceOf(CHARLIE);
charlieVotingPower = veRAAC.getVotingPower(CHARLIE);
assertEq(balanceOfCharlie, charlieVotingPower);
}
}
contract CurveMock is ERC4626 {
IERC20 curveToken;
constructor(address token) ERC4626(IERC20(token)) ERC20("curve vault", "CV") {}
function withdraw(uint256 assets, address receiver, address owner, uint256 maxLoss, address[] calldata strategies)
external
returns (uint256 shares) {
withdraw(assets, receiver, owner);
}
}

You should have this output :

Failing tests:
Encountered 1 failing test in test/CodedPOC.t.sol:CodedPOC
[FAIL: assertion failed: 9652281251156086133 != 3216263726045894628] test_lockTwoTimePOC() (gas: 1423909)

Impact

The user will lost his previous locked amount and his voting power moreover he will still have his veRAACTokens and the total locked will be updated breaking therefore the protocol calculation.

Tools Used

medusa

Recommendations

A check should be added to revert the transaction if a user lock a second time.

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

veRAACToken::lock called multiple times, by the same user, leads to loss of funds

Support

FAQs

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