Summary
After reviewing ZENO.sol, I can confirm there is a critical decimal precision mismatch between ZENO tokens (18 decimals) and USDC (6 decimals) that leads to significant economic vulnerabilities.
Vulnerable Code
contract ZENO is IZENO, ERC20, Ownable, ReentrancyGuard {
using SafeERC20 for IERC20;
IERC20 public immutable USDC;
constructor(
address _usdc,
uint256 _maturityDate,
string memory _name,
string memory _symbol,
address _initialOwner
) Ownable(_initialOwner) ERC20(_name, _symbol) {
USDC = IERC20(_usdc);
}
function redeem(uint amount) external nonReentrant {
_burn(msg.sender, amount);
USDC.safeTransfer(msg.sender, amount);
}
}
Impact
Economic Exploitation
function exploitDecimals() {
auction.buy(1e18);
zeno.redeem(1e18);
}
Protocol Insolvency
Initial USDC Balance: 1,000,000 USDC (1e6 * 1e6 units)
User redeems 1 ZENO: Attempts to transfer 1e18 USDC units
Result: Contract depleted, unable to honor redemptions
Proof of Concept
contract DecimalMismatchTest {
ZENO zeno;
IERC20 usdc;
function testDecimalMismatch() public {
usdc.approve(address(zeno), type(uint256).max);
uint256 depositAmount = 1e6;
uint256 zenoAmount = 1e18;
vm.warp(zeno.MATURITY_DATE() + 1);
zeno.redeem(zenoAmount);
uint256 usdcReceived = usdc.balanceOf(address(this));
assertEq(usdcReceived, 1e18);
}
}
Recommended Mitigation
contract ZENO is IZENO, ERC20, Ownable, ReentrancyGuard {
using SafeERC20 for IERC20;
uint8 public constant USDC_DECIMALS = 6;
uint8 public constant ZENO_DECIMALS = 18;
uint256 public constant DECIMAL_ADJUSTMENT = 10**(ZENO_DECIMALS - USDC_DECIMALS);
constructor(
address _usdc,
uint256 _maturityDate,
string memory _name,
string memory _symbol,
address _initialOwner
) Ownable(_initialOwner) ERC20(_name, _symbol) {
USDC = IERC20(_usdc);
require(ERC20(_usdc).decimals() == USDC_DECIMALS, "Invalid USDC decimals");
}
function redeem(uint256 amount) external nonReentrant {
if (!isRedeemable()) revert BondNotRedeemable();
if (amount == 0) revert ZeroAmount();
uint256 totalAmount = balanceOf(msg.sender);
if (amount > totalAmount) revert InsufficientBalance();
totalZENORedeemed += amount;
_burn(msg.sender, amount);
uint256 usdcAmount = amount / DECIMAL_ADJUSTMENT;
USDC.safeTransfer(msg.sender, usdcAmount);
emit Redeemed(msg.sender, amount, usdcAmount);
}
}
Additional Recommendations
Add decimal validation in constructor:
require(decimals() == ZENO_DECIMALS, "Invalid ZENO decimals");
Add events for tracking:
event Redeemed(
address indexed user,
uint256 zenoAmount,
uint256 usdcAmount
);
Update tests to verify decimal handling:
function testCorrectDecimalHandling() public {
uint256 zenoAmount = 1e18;
uint256 expectedUsdc = 1e6;
zeno.redeem(zenoAmount);
assertEq(usdc.balanceOf(address(this)), expectedUsdc);
}