Core Contracts

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

Incorrect `ZENO` decimals disrupts protocol accounting

Description

In the RAAC protocol, ZENO tokens are purchased at a discount during auctions using a corresponding ERC20 token, in this case, USDC. Upon maturity, users can redeem their ZENO for USDC at a 1:1 ratio, as shown in the redeem() function of the ZENO.sol contract:

function redeem(uint 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);
USDC.safeTransfer(msg.sender, amount);
}

This implies (as confirmed by the RAAC team) that the ZENO token must have the same number of decimals as the ERC20 token used in the auctions. However, ZENO is always created with 18 decimals, resulting in inaccurate accounting both when purchasing and redeeming ZENO tokens.

Context

Impact

High. This mismatch in token decimals completely disrupts the protocol's accounting during both the purchase and redemption of ZENO tokens.

Likelihood

High. This issue occurs in every ZENO token purchase and redemption.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test, console} from "../../lib/forge-std/src/Test.sol";
import {AuctionFactory} from "../../contracts/zeno/AuctionFactory.sol";
import {Auction} from "../../contracts/zeno/Auction.sol";
import {ZENOFactory} from "../../contracts/zeno/ZENOFactory.sol";
import {ZENO} from "../../contracts/zeno/ZENO.sol";
import {ERC20Mock} from "./mocks/ERC20Mock.sol";
contract ZENOTest is Test {
// Contracts
AuctionFactory auctionFactory;
ZENOFactory zenoFactory;
Auction auction;
ERC20Mock usdc;
ZENO zeno;
// Actors
address BUSINESS = makeAddr("business");
address OWNER = makeAddr("owner");
address USER = makeAddr("user");
function setUp() public {
vm.startPrank(OWNER);
// Deploy USDC token
usdc = new ERC20Mock("USD Coin", "USDC", 6);
// Deploy factories
auctionFactory = new AuctionFactory(OWNER);
zenoFactory = new ZENOFactory(OWNER);
// Deploy ZENO token
zenoFactory.createZENOContract(address(usdc), block.timestamp + 365 days);
zeno = zenoFactory.getZENO(0);
// Deploy Auction
auctionFactory.createAuction(
address(zeno), address(usdc), BUSINESS, block.timestamp, block.timestamp + 1 days, 1e6, 0.5e6, 10e6
);
auction = auctionFactory.getAuction(0);
// Transfer ZENO ownership to auction contract
zenoFactory.transferZenoOwnership(0, address(auction));
vm.stopPrank();
}
function test_wrongZenoDecimals() public {
// Assert that zeno does not have the same decimals than USDC
assertNotEq(zeno.decimals(), usdc.decimals());
console.log("ZENO DECIMALS :", zeno.decimals()); // 18
console.log("USDC DECIMALS :", usdc.decimals()); // 6
}
}

Logs

Ran 1 test for test/foundry/ZENOTest.t.sol:ZENOTest
[PASS] test_wrongZenoDecimals() (gas: 21174)
Logs:
ZENO DECIMALS : 18
USDC DECIMALS : 6
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.17ms (77.98µs CPU time)

Instructions

First, integrate Foundry by running the following commands in your terminal, in the project's root directory:

# Create required directories
mkdir out lib
# Add `forge-std` module to `lib`
git submodule add https://github.com/foundry-rs/forge-std lib/forge-std
# Create foundry.toml
touch foundry.toml

Next, configure Foundry by adding the following settings to foundry.toml:

[profile.default]
src = "contracts"
out = "out"
lib = "lib"

After that, create a foundry/ directory inside the test/ directory. Inside foundry/, create the following files:

  • ZENOTest.t.sol

  • mocks/ERC20Mock.sol

Then, add the following code to mocks/ERC20Mock.sol:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract ERC20Mock is ERC20 {
uint8 private _decimals;
constructor(string memory _tokenName, string memory _tokenSymbol, uint8 _tokenDecimals)
ERC20(_tokenName, _tokenSymbol)
{
_decimals = _tokenDecimals;
}
function mint(address account, uint256 amount) external {
_mint(account, amount);
}
function burn(address account, uint256 amount) external {
_burn(account, amount);
}
function decimals() public view override returns (uint8) {
return _decimals;
}
}

Finally, paste the provided (PoC) into ZENOTest.t.sol and run:

forge test --mt test_wrongZenoDecimals -vvv

Recommendation

Consider setting the number of decimals in the ZENO constructor and overriding the decimals() function:

contract ZENO is IZENO, ERC20, Ownable, ReentrancyGuard {
...
+ uint8 private immutable DECIMALS;
constructor(
address _usdc,
uint256 _maturityDate,
string memory _name,
string memory _symbol,
+ uint8 decimals_,
address _initialOwner
) Ownable(_initialOwner) ERC20(_name, _symbol) {
USDC = IERC20(_usdc);
MATURITY_DATE = _maturityDate;
+ DECIMALS = decimals_;
}
...
+ function decimals() public view override returns (uint8) {
+ return DECIMALS;
+ }
}

Therefore, update the createZENOContract function in ZENOFactory.sol to pass the correct decimals when creating the ZENO token:

function createZENOContract(
address _usdcAddress,
uint256 _maturityDate
) external onlyOwner {
string memory id = Strings.toString(zenos.length + 1);
string memory name = string(abi.encodePacked("ZENO Bond ", id));
string memory symbol = string(abi.encodePacked("ZENO", id));
ZENO newZENO = new ZENO(
_usdcAddress,
_maturityDate,
name,
symbol,
+ ERC20(_usdcAddress).decimals(),
address(this)
);
zenos.push(newZENO);
isZENO[address(newZENO)] = true;
emit ZENOCreated(address(newZENO), _maturityDate);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Decimal precision mismatch between ZENO token (18 decimals) and USDC (6 decimals) not accounted for in redemption, causing calculation errors and incorrect payments

Auction.sol's buy() function multiplies ZENO amount (18 decimals) by price (6 decimals) without normalization, causing users to pay 1 trillion times the intended USDC amount

Support

FAQs

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