Core Contracts

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

Incorrect reward distributions and unfair share allocations

Summary

https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/governance/gauges/BaseGauge.sol#L261C4-L280C6

Incorrect reward distributions and unfair share allocations as can be seen below

Vulnerability Details

https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/governance/gauges/BaseGauge.sol#L261C4-L280C6

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);
}
/**
* @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);
}
// State variables
contract BaseGauge {
uint256 private _totalSupply; // Global total supply tracker
mapping(address => uint256) private _balances; // Individual balances
// Staking Implementation
function stake(uint256 amount) external nonReentrant updateReward(msg.sender) {
if (amount == 0) revert InvalidAmount();
// ISSUE: Non-atomic updates to related state
_totalSupply += amount; // Global state update
_balances[msg.sender] += amount; // Individual state update
stakingToken.safeTransferFrom(msg.sender, address(this), amount);
emit Staked(msg.sender, amount);
}
// Withdrawal Implementation
function withdraw(uint256 amount) external nonReentrant updateReward(msg.sender) {
if (amount == 0) revert InvalidAmount();
if (_balances[msg.sender] < amount) revert InsufficientBalance();
// ISSUE: Same non-atomic update problem
_totalSupply -= amount; // Global state update
_balances[msg.sender] -= amount; // Individual state update
stakingToken.safeTransfer(msg.sender, amount);
emit Withdrawn(msg.sender, amount);
}
}
// 1. Reentrancy Attack (even with nonReentrant modifier)
contract SupplyAttacker {
BaseGauge gauge;
bool attacked = false;
// Attack through a malicious token that calls back on transfer
function attack() external {
// Initial stake to get some balance
gauge.stake(1000);
// Trigger malicious withdrawal
gauge.withdraw(500); // This will trigger the callback
}
// Callback function from malicious token
function tokenCallback() external {
if (!attacked) {
attacked = true;
// State is inconsistent here:
// - _totalSupply has been reduced
// - _balances not yet reduced
// - Can perform operations with inconsistent state
gauge.stake(1000); // Uses incorrect totalSupply value
}
}
}
// 2. Emergency Function Issue
contract BaseGauge {
function emergencyWithdraw(address token, uint256 amount) external onlyRole(DEFAULT_ADMIN_ROLE) {
// ISSUE: Bypasses supply tracking
IERC20(token).safeTransfer(msg.sender, amount);
// _totalSupply and _balances not updated
}
}
// 3. Direct Transfer Problems
contract BaseGauge {
// No handling of direct token transfers
// Tokens can be sent directly to contract, breaking supply invariant
}
// DEMONSTRATION OF BREAKS
// Scenario 1: Emergency Withdrawal
function demonstrateEmergencyBreak() {
// Initial state
assert(_totalSupply == 1000);
assert(_balances[user] == 1000);
// Emergency withdrawal
emergencyWithdraw(stakingToken, 500);
// Broken state
assert(_totalSupply == 1000); // Still shows 1000
assert(_balances[user] == 1000); // Still shows 1000
// But actual token balance is 500
}
// Scenario 2: Direct Transfer
function demonstrateDirectTransferBreak() {
// Initial state
assert(_totalSupply == 1000);
// Direct token transfer
stakingToken.transfer(address(this), 500);
// Broken state
assert(_totalSupply == 1000); // Doesn't reflect extra 500
// Contract has 1500 tokens but supply shows 1000
}

Impact

Incorrect reward distributions and unfair share allocations

Tools Used

Foundry

Recommendations

// 1. Atomic State Updates
struct SupplyUpdate {
uint256 oldTotalSupply;
uint256 newTotalSupply;
uint256 oldBalance;
uint256 newBalance;
address account;
}
function updateSupplyAtomic(SupplyUpdate memory update) internal {
// Validate state transition
require(update.newTotalSupply >= update.oldTotalSupply - update.oldBalance, "Invalid supply change");
require(update.newBalance <= update.oldBalance + update.newTotalSupply - update.oldTotalSupply, "Invalid balance change");
// Atomic update
_totalSupply = update.newTotalSupply;
_balances[update.account] = update.newBalance;
emit SupplyUpdated(update.oldTotalSupply, update.newTotalSupply);
}
// 2. Supply Verification
function verifySupply() internal view returns (bool) {
uint256 calculatedSupply = 0;
address[] memory holders = getHolders(); // Need to maintain holder list
for (uint i = 0; i < holders.length; i++) {
calculatedSupply += _balances[holders[i]];
}
return calculatedSupply == _totalSupply;
}
// 3. Safe Operations with Supply Check
function safeStake(uint256 amount) external nonReentrant updateReward(msg.sender) {
require(amount > 0, "Invalid amount");
// Prepare atomic update
SupplyUpdate memory update = SupplyUpdate({
oldTotalSupply: _totalSupply,
newTotalSupply: _totalSupply + amount,
oldBalance: _balances[msg.sender],
newBalance: _balances[msg.sender] + amount,
account: msg.sender
});
// Execute transfer first
stakingToken.safeTransferFrom(msg.sender, address(this), amount);
// Update state atomically
updateSupplyAtomic(update);
// Verify invariant
assert(verifySupply());
}
// 4. Recovery Mechanism
function recoverSupplyState() internal {
uint256 actualSupply = 0;
address[] memory holders = getHolders();
for (uint i = 0; i < holders.length; i++) {
actualSupply += _balances[holders[i]];
}
if (actualSupply != _totalSupply) {
emit SupplyDiscrepancy(_totalSupply, actualSupply);
_totalSupply = actualSupply; // Align with real state
}
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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

Give us feedback!