Core Contracts

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

Stake functionality in RWAGauge and RAACGauge always reverts

Summary

The RWAGauge and RAACGauge contracts allow staking of veRAAC tokens so that users can earn rewards. However, veRAAC token transfers are disabled, and the stake/withdraw functions will always revert because of that.

Vulnerability Details

Both RWAGauge and RAACGauge inherit from BaseGauge.

In BaseGauge.sol:L261 and L273, the functions that use the staking token are implemented. Based on the documents and the existing tests, the staking token will be veRAAC.

function stake(uint256 amount) external nonReentrant updateReward(msg.sender) {
if (amount == 0) revert InvalidAmount();
_totalSupply += amount;
_balances[msg.sender] += amount;
stakingToken.safeTransferFrom(msg.sender, address(this), amount);
emit Staked(msg.sender, amount);
}
function withdraw(uint256 amount) external nonReentrant updateReward(msg.sender) {
if (amount == 0) revert InvalidAmount();
if (_balances[msg.sender] < amount) revert InsufficientBalance();
_totalSupply -= amount;
_balances[msg.sender] -= amount;
stakingToken.safeTransfer(msg.sender, amount);
emit Withdrawn(msg.sender, amount);
}

However, veRAAC.sol:L544 implements an override for the _update function to prevent transfers:

/**
* @notice Internal function to handle token transfers
* @dev Overrides ERC20 _update to prevent regular transfers
* @param from The sender address
* @param to The recipient address
* @param amount The amount to transfer
*/
function _update(
address from,
address to,
uint256 amount
) internal virtual override {
if (from == address(0) || to == address(0)) {
// Allow minting and burning operations
super._update(from, to, amount);
return;
}
// Prevent all other transfers of veRAAC tokens
revert TransferNotAllowed();
}

Which in turn will cause a revert in the above mentioned staking functionality.

Impact

The RWAGauge and RAACGauge contracts will always revert on stake/withdraw.

Tools Used and Proof of Concept

The following PoC can be executed to see that the stake function will revert.

import { time } from "@nomicfoundation/hardhat-network-helpers";
import { expect } from "chai";
import hre from "hardhat";
const { ethers } = hre;
describe("GenericTest", () => {
let snapshotId;
// tokens
let raacToken;
let veRAACToken;
let rewardToken;
// random
let rwaGauge;
let gaugeController;
let owner;
let user1;
let user2;
let emergencyAdmin;
const MONTH = 30 * 24 * 3600;
const WEIGHT_PRECISION = 10000;
const DAY = 24 * 3600;
beforeEach(async () => {
snapshotId = await network.provider.send('evm_snapshot');
[owner, user1, user2, emergencyAdmin] = await ethers.getSigners();
// Deploy RAACToken
const RAACToken = await ethers.getContractFactory("RAACToken");
raacToken = await RAACToken.deploy(owner, 0, 0);
await raacToken.waitForDeployment();
await raacToken.connect(owner).setMinter(owner);
await raacToken.mint(owner.address, ethers.parseEther("100"));
await raacToken.connect(owner).transfer(user1.address, ethers.parseEther("100"));
// Deploy veRAACToken
const VeRAACToken = await ethers.getContractFactory("veRAACToken");
veRAACToken = await VeRAACToken.deploy(await raacToken.getAddress());
await veRAACToken.waitForDeployment();
// Deploy mock tokens
const MockToken = await ethers.getContractFactory("MockToken");
rewardToken = await MockToken.deploy("Reward Token", "RWD", 18);
// Deploy controller
const GaugeController = await ethers.getContractFactory("GaugeController");
gaugeController = await GaugeController.deploy(await veRAACToken.getAddress());
// Get current time and ensure we're moving to a future period
const currentTime = BigInt(await time.latest());
const nextMonthStart = ((currentTime / BigInt(MONTH)) * BigInt(MONTH)) + BigInt(MONTH);
// Move to next month start
await time.setNextBlockTimestamp(nextMonthStart);
await network.provider.send("evm_mine");
// Deploy RWAGauge
const RWAGauge = await ethers.getContractFactory("RWAGauge");
rwaGauge = await RWAGauge.deploy(
await rewardToken.getAddress(),
await veRAACToken.getAddress(),
await gaugeController.getAddress()
);
await rewardToken.mint(owner.address, ethers.parseEther("1000000"));
await rewardToken.connect(owner).transfer(rwaGauge.getAddress(), ethers.parseEther("100000"));
await rwaGauge.grantRole(await rwaGauge.CONTROLLER_ROLE(), owner.address);
await rwaGauge.grantRole(await rwaGauge.FEE_ADMIN(), owner.address);
await rwaGauge.grantRole(await rwaGauge.EMERGENCY_ADMIN(), owner.address);
await gaugeController.grantRole(await gaugeController.GAUGE_ADMIN(), owner.address);
await gaugeController.addGauge(await rwaGauge.getAddress(), 0, WEIGHT_PRECISION);
await rwaGauge.setInitialWeight(5000); // Set initial weight to 50%
await rwaGauge.setBoostParameters(25000, 10000, 7 * 24 * 3600);
await rwaGauge.setDistributionCap(ethers.parseEther("1000000"));
await rwaGauge.setMonthlyEmission(ethers.parseEther("100000"));
});
afterEach(async () => {
await network.provider.send('evm_revert', [snapshotId]);
});
describe("Stake", () => {
it("Should revert stake transaction", async () => {
await veRAACToken.connect(user1).approve(rwaGauge.getAddress(), ethers.parseEther("50"));
await expect(rwaGauge.connect(user1).stake(ethers.parseEther("50"))).to.be.revertedWithCustomError(veRAACToken, "TransferNotAllowed");
});
});
});

Recommendations

Implement a whitelist so that specific contracts can transfer veRAAC tokens.

Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

Users cannot stake into BaseGauge because veRAACToken is non-transferrable, breaking core gauge staking functionality

According to the sponsor `The required stakingToken is not the veToken just the incitivised token being staked into the contract.`

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

Users cannot stake into BaseGauge because veRAACToken is non-transferrable, breaking core gauge staking functionality

According to the sponsor `The required stakingToken is not the veToken just the incitivised token being staked into the contract.`

Support

FAQs

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

Give us feedback!