Summary
BaseGauge is a smart contract that allows users stake veRAACToken and get rewards on boosted emission rate. However, veRRACToken is not transferrable. So users cannot send veRAACToken to RAACGauge and RWAGauge. Thus, staking feature will be useless.
Vulnerability Details
In order to stake to gauge, you need to transfer staking token to gauge contract:
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);
}
According to RAACGauge.test.js
and RWAGauge.test.js
, staking token is veRAACToken:
const GaugeController = await ethers.getContractFactory("GaugeController");
@> gaugeController = await GaugeController.deploy(await veRAACToken.getAddress());
const currentTime = BigInt(await time.latest());
const nextWeekStart = ((currentTime / BigInt(WEEK)) + 3n) * BigInt(WEEK);
await time.setNextBlockTimestamp(Number(nextWeekStart));
await network.provider.send("evm_mine");
const RAACGauge = await ethers.getContractFactory("RAACGauge");
raacGauge = await RAACGauge.deploy(
await rewardToken.getAddress(),
@> await veRAACToken.getAddress(),
await gaugeController.getAddress()
);
However, veRAACToken is non-transferrable:
function _update(
address from,
address to,
uint256 amount
) internal virtual override {
if (from == address(0) || to == address(0)) {
super._update(from, to, amount);
return;
}
@> revert TransferNotAllowed();
}
Thus, all staking attempt will revert with TransferNotAllowed
error.
Note: Test script works because MockToken is used for veRAACToken.
POC
You'll need to integrate foundry to the project before running the following poc:
pragma solidity ^0.8.19;
import "../lib/forge-std/src/Test.sol";
import {RAACGauge} from "../contracts/core/governance/gauges/RAACGauge.sol";
import {veRAACToken} from "../contracts/core/tokens/veRAACToken.sol";
import {RAACToken} from "../contracts/core/tokens/RAACToken.sol";
import {MockToken} from "../contracts/mocks/core/tokens/MockToken.sol";
import {GaugeController} from "../contracts/core/governance/gauges/GaugeController.sol";
import {IGaugeController} from "../contracts/interfaces/core/governance/gauges/IGaugeController.sol";
contract GaugeTest is Test {
RAACGauge raacGauge;
RAACToken raacToken;
veRAACToken veToken;
MockToken rewardToken;
GaugeController gaugeController;
address alice = makeAddr("alice");
address bob = makeAddr("bob");
uint256 userAssetAmount = 10000e18;
function setUp() external {
raacToken = new RAACToken(address(this), 0, 0);
raacToken.setMinter(address(this));
veToken = new veRAACToken(address(raacToken));
veToken.setMinter(address(this));
raacToken.manageWhitelist(address(veToken), true);
rewardToken = new MockToken("Reward Token", "RWD", 18);
gaugeController = new GaugeController(address(veToken));
raacGauge = new RAACGauge(address(rewardToken), address(veToken), address(gaugeController));
raacGauge.grantRole(keccak256("CONTROLLER_ROLE"), address(this));
raacGauge.setBoostParameters(25000, 10000, 7 days);
gaugeController.addGauge(address(raacGauge), IGaugeController.GaugeType.RAAC, 10000);
}
function testStake() external {
_dealVeToken(alice, userAssetAmount);
vm.startPrank(alice);
veToken.approve(address(raacGauge), userAssetAmount);
vm.expectRevert(abi.encodeWithSignature("TransferNotAllowed()"));
raacGauge.stake(userAssetAmount);
vm.stopPrank();
}
function _dealVeToken(address account, uint256 amount) internal {
if (amount == 0) {
return;
}
deal(address(raacToken), account, amount);
vm.startPrank(account);
raacToken.approve(address(veToken), amount);
veToken.lock(amount, veToken.MAX_LOCK_DURATION());
vm.stopPrank();
}
}
Impact
Core functionality of gauge is broken.
Tools Used
Manual Review
Recommendations
Implement a whitelist mechanism for veRAACToken transfer.