Core Contracts

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

Decimal Precision Vulnerability in Boost Calculations

Summary

The BoostController.sol contract's boost calculation mechanism is vulnerable to decimal precision issues when handling tokens with different decimal places. This can result in significantly different boost effects for tokens with varying decimals, potentially leading to economic losses and system manipulation.

Vulnerability Detail

The boost calculation in BoostController.sol does not properly account for tokens with different decimal places. The issue arises because the contract:

  1. Uses raw token amounts without normalizing for decimals

  2. Applies the same boost calculation logic regardless of token decimals

  3. Does not scale the boost based on token precision

This leads to inconsistent boost effects where tokens with different decimals receive disproportionate boosts despite having the same underlying value.

For example, given the same veToken balance:

  • USDC (6 decimals): 1 USDC gets boosted to 2 USDC

  • ETH (18 decimals): 1 ETH gets boosted to 2 ETH

  • BTC (8 decimals): 1 BTC gets boosted to 2 BTC

While the multiplier appears the same (2x), the economic impact varies significantly due to the different token values.

Impact

The vulnerability can be exploited in several ways:

  1. Economic Exploitation:

    • Users can maximize their boost benefits by using tokens with higher underlying value

    • For example, boosting 100 BTC provides significantly more value than boosting an equivalent amount in USDC

  2. System Manipulation:

    • Attackers can strategically choose tokens to maximize their boost benefits

    • The protocol's intended boost mechanism becomes imbalanced across different token types

  3. Financial Impact:
    Given current market prices:

    • Boosting 1M USDC yields $1.5M in extra value

    • Boosting 1K ETH yields $3M in extra value

    • Boosting 100 BTC yields $6M in extra value

This demonstrates how the same veToken balance can result in vastly different economic outcomes based solely on token decimals.

Proof of Concept

Running the test with forge test --match-contract DecimalExploitTest -vv demonstrates the full impact.

POC file: `test/DecimalExploitPoC.sol`:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test} from "forge-std/Test.sol";
import {console2} from "forge-std/console2.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "../contracts/interfaces/core/tokens/IveRAACToken.sol";
import "../contracts/core/governance/boost/BoostController.sol";
/**
* @title MockERC20
* @notice Mock ERC20 token with configurable decimals
*/
contract MockERC20 is ERC20 {
uint8 private immutable _decimals;
constructor(string memory name_, string memory symbol_, uint8 decimals_) ERC20(name_, symbol_) {
_decimals = decimals_;
}
function decimals() public view virtual override returns (uint8) {
return _decimals;
}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}
/**
* @title MockVeToken
* @notice Mock veToken for testing boost calculations
*/
contract MockVeToken is ERC20, IveRAACToken {
mapping(address => IveRAACToken.LockPosition) public lockPositions;
uint256 public totalVotingPower;
constructor() ERC20("Mock veToken", "veTOKEN") {}
function getLockPosition(address account) external view override returns (IveRAACToken.LockPosition memory) {
return lockPositions[account];
}
function getVotingPower(address account, uint256) external view override returns (uint256) {
return balanceOf(account);
}
function getVotingPower(address account) external view override returns (uint256) {
return balanceOf(account);
}
function getTotalVotingPower() external view override returns (uint256) {
return totalSupply();
}
function setLockPosition(address user, uint256 amount, uint256 end) external {
lockPositions[user] = IveRAACToken.LockPosition({
amount: amount,
end: end,
power: amount // For testing, we'll set power equal to amount
});
}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
// Override transfer functions to satisfy interface
function transfer(address to, uint256 amount) public virtual override(ERC20, IveRAACToken) returns (bool) {
return super.transfer(to, amount);
}
function transferFrom(address from, address to, uint256 amount) public virtual override(ERC20, IveRAACToken) returns (bool) {
return super.transferFrom(from, to, amount);
}
// Unused interface functions
function lock(uint256 amount, uint256 duration) external override {}
function extend(uint256 duration) external override {}
function withdraw() external override {}
function increase(uint256 amount) external override {}
function getUnlockTime(address) external pure returns (uint256) { return 0; }
function getVotes(address) external pure returns (uint256) { return 0; }
function delegates(address) external pure returns (address) { return address(0); }
function delegate(address) external {}
function delegateBySig(address, uint256, uint256, uint8, bytes32, bytes32) external {}
function getPastVotes(address, uint256) external pure returns (uint256) { return 0; }
function getPastTotalSupply(uint256) external pure returns (uint256) { return 0; }
function getPastVotingPower(address, uint256) external pure returns (uint256) { return 0; }
function getPastTotalVotingPower(uint256) external pure returns (uint256) { return 0; }
function calculateVeAmount(uint256, uint256) external pure override returns (uint256) { return 0; }
function setMinter(address) external override {}
}
/**
* @title DecimalExploitTest
* @notice Demonstrates the vulnerability in boost calculations when handling tokens with different decimals
* @dev Shows how the same veToken balance can result in different effective boosts due to decimal precision
*/
contract DecimalExploitTest is Test {
// Test contracts
BoostController public boostController;
MockVeToken public veToken;
MockERC20 public usdcToken; // 6 decimals
MockERC20 public ethToken; // 18 decimals
MockERC20 public btcToken; // 8 decimals
// Test accounts
address public user = address(0x1);
address public admin = address(0x2);
// Constants
bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE");
function setUp() public {
// Deploy tokens with different decimals
usdcToken = new MockERC20("USDC", "USDC", 6);
ethToken = new MockERC20("ETH", "ETH", 18);
btcToken = new MockERC20("BTC", "BTC", 8);
veToken = new MockVeToken();
// Deploy boost controller
vm.prank(admin);
boostController = new BoostController(address(veToken));
// Setup roles and permissions
vm.startPrank(admin);
boostController.grantRole(MANAGER_ROLE, admin);
boostController.modifySupportedPool(address(usdcToken), true);
boostController.modifySupportedPool(address(ethToken), true);
boostController.modifySupportedPool(address(btcToken), true);
vm.stopPrank();
// Setup user with veToken balance and lock position
veToken.mint(user, 1000e18); // 1000 veTokens
veToken.setLockPosition(address(boostController), 1000e18, block.timestamp + 365 days);
}
function test_DecimalExploitDemo() public {
vm.startPrank(user);
// Test amounts representing 1 token each
uint256 oneUSDC = 1e6; // 1 USDC
uint256 oneETH = 1e18; // 1 ETH
uint256 oneBTC = 1e8; // 1 BTC
console2.log("=== Decimal Vulnerability Demonstration ===");
console2.log("User veToken balance: %s", veToken.balanceOf(user) / 1e18);
console2.log("");
// Calculate boosts for each token
(uint256 usdcBoost, uint256 usdcBoostedAmount) = boostController.calculateBoost(user, address(usdcToken), oneUSDC);
(uint256 ethBoost, uint256 ethBoostedAmount) = boostController.calculateBoost(user, address(ethToken), oneETH);
(uint256 btcBoost, uint256 btcBoostedAmount) = boostController.calculateBoost(user, address(btcToken), oneBTC);
// Calculate effective multipliers
uint256 usdcMultiplier = (usdcBoostedAmount * 10000) / oneUSDC;
uint256 ethMultiplier = (ethBoostedAmount * 10000) / oneETH;
uint256 btcMultiplier = (btcBoostedAmount * 10000) / oneBTC;
console2.log("=== USDC (6 decimals) ===");
console2.log("Input amount: 1 USDC");
console2.log("Boost basis points: %s", usdcBoost);
console2.log("Boosted amount: %s USDC", usdcBoostedAmount / 1e6);
console2.log("Effective multiplier: %sx", usdcMultiplier / 10000);
console2.log("");
console2.log("=== ETH (18 decimals) ===");
console2.log("Input amount: 1 ETH");
console2.log("Boost basis points: %s", ethBoost);
console2.log("Boosted amount: %s ETH", ethBoostedAmount / 1e18);
console2.log("Effective multiplier: %sx", ethMultiplier / 10000);
console2.log("");
console2.log("=== BTC (8 decimals) ===");
console2.log("Input amount: 1 BTC");
console2.log("Boost basis points: %s", btcBoost);
console2.log("Boosted amount: %s BTC", btcBoostedAmount / 1e8);
console2.log("Effective multiplier: %sx", btcMultiplier / 10000);
console2.log("");
// Demonstrate the impact with larger amounts
uint256 largeUSDC = 1_000_000e6; // 1M USDC
uint256 largeETH = 1_000e18; // 1K ETH
uint256 largeBTC = 100e8; // 100 BTC
(uint256 largeUsdcBoost, uint256 largeUsdcBoostedAmount) = boostController.calculateBoost(user, address(usdcToken), largeUSDC);
(uint256 largeEthBoost, uint256 largeEthBoostedAmount) = boostController.calculateBoost(user, address(ethToken), largeETH);
(uint256 largeBtcBoost, uint256 largeBtcBoostedAmount) = boostController.calculateBoost(user, address(btcToken), largeBTC);
console2.log("=== Impact with Larger Amounts ===");
console2.log("1M USDC boost: %s (effective: %sx)", largeUsdcBoost, (largeUsdcBoostedAmount * 10000 / largeUSDC) / 10000);
console2.log("1K ETH boost: %s (effective: %sx)", largeEthBoost, (largeEthBoostedAmount * 10000 / largeETH) / 10000);
console2.log("100 BTC boost: %s (effective: %sx)", largeBtcBoost, (largeBtcBoostedAmount * 10000 / largeBTC) / 10000);
// Show potential economic impact (assuming 1 ETH = $2000, 1 BTC = $40000)
uint256 usdcValue = largeUsdcBoostedAmount - largeUSDC; // Value gained in USDC (6 decimals)
uint256 ethValue = ((largeEthBoostedAmount - largeETH) * 2000) / 1e18; // Value gained in USD
uint256 btcValue = ((largeBtcBoostedAmount - largeBTC) * 40000) / 1e8; // Value gained in USD
console2.log("");
console2.log("=== Economic Impact (Extra Value from Boost) ===");
console2.log("USDC profit: $%s", usdcValue / 1e6);
console2.log("ETH profit: $%s", ethValue);
console2.log("BTC profit: $%s", btcValue);
console2.log("");
console2.log("This demonstrates how the same veToken balance can result in significantly");
console2.log("different boost effects due to improper decimal handling.");
vm.stopPrank();
}
}

Code Snippet

From BoostController.sol:

function calculateBoost(
address user,
address pool,
uint256 amount
) external view returns (uint256 boostBasisPoints, uint256 boostedAmount) {
// No decimal normalization performed
uint256 votingPower = veToken.getVotingPower(user);
uint256 totalVotingPower = veToken.getTotalVotingPower();
// Direct calculation without considering token decimals
boostBasisPoints = _calculateBoostBasisPoints(votingPower, totalVotingPower);
boostedAmount = amount + ((amount * boostBasisPoints) / BASIS_POINTS);
}

Tool Used

  • Manual Review

  • Custom Foundry Test Suite

Recommendation

Implement proper decimal handling in boost calculations:

  1. Normalize token amounts to a standard decimal precision (e.g., 18 decimals):

function calculateBoost(
address user,
address pool,
uint256 amount
) external view returns (uint256 boostBasisPoints, uint256 boostedAmount) {
uint256 decimals = IERC20Metadata(pool).decimals();
uint256 normalizedAmount = amount * (10 ** (18 - decimals));
uint256 votingPower = veToken.getVotingPower(user);
uint256 totalVotingPower = veToken.getTotalVotingPower();
boostBasisPoints = _calculateBoostBasisPoints(votingPower, totalVotingPower);
uint256 normalizedBoostedAmount = normalizedAmount + ((normalizedAmount * boostBasisPoints) / BASIS_POINTS);
boostedAmount = normalizedBoostedAmount / (10 ** (18 - decimals));
}
  1. Consider implementing value-based boost calculations using price oracles to ensure fair boost distribution across different token types.

  2. Add validation checks for token decimals to prevent extreme cases.

Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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