Summary
BaseGauge functions stake and withdraw do not have the access control modifier whenNotPaused.
Vulnerability Details
The access control modifier whenNotPaused enables to the project admins to block access to the contracts for security reasons. This is useful when it is necessary to protect user funds. Applied to the stake function, this can prevent unwary users from depositing more funds that would put them at risk. However, the modifier is currently missing:
* @notice Stakes tokens in the gauge
* @param amount Amount to stake
*/
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);
}
Similarly, the withdraw function does not have the modifier whenNotPaused:
* @notice Withdraws staked tokens
* @param amount Amount to withdraw
*/
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);
}
Add this POC to test/unit/core/governance/gauges/BaseGauge.test.js that shows that the stake functon can still be called when the contract has been paused by admin:
describe("Emergency Actions", () => {
beforeEach(async () => {
const paused = true;
await baseGauge.connect(owner).setEmergencyPaused(paused);
expect(await baseGauge.paused()).to.equal(paused);
});
it("should stake", async () => {
await rewardToken.mint(user1.address, ethers.parseEther("1000"));
await rewardToken.connect(user1).approve(await baseGauge.getAddress(), ethers.parseEther("1000"));
await baseGauge.connect(user1).stake(ethers.parseEther("1000"));
});
});
Add this POC to test/unit/core/governance/gauges/BaseGauge.test.js that shows that the withdraw functon can still be called when the contract has been paused by admin
describe("Emergency Actions", () => {
beforeEach(async () => {
const paused = true;
await baseGauge.connect(owner).setEmergencyPaused(paused);
expect(await baseGauge.paused()).to.equal(paused);
});
it("should unstake", async () => {
await rewardToken.mint(user1.address, ethers.parseEther("1000"));
await rewardToken.connect(user1).approve(await baseGauge.getAddress(), ethers.parseEther("1000"));
await baseGauge.connect(user1).stake(ethers.parseEther("1000"));
await baseGauge.connect(user1).withdraw(ethers.parseEther("1000"));
});
});
Impact
Token deposit and withdraw functions that do not have the modifier whenNotPaused prevent project admins from protecting users assets incase of emergency. While it is true that the emergencyWithdraw function exists, this might not be sufficient incase users were in the process of depositing, admin might need to call emergencyWithdraw again which can be front-run by attackers.
Tools Used
Manual review
Recommendations
Add the access control modifier whenNotPaused to the functions stake and withdraw.