Core Contracts

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

Lack of decimal adjustment in `Auction` leads to excessive `ZENO` costs

Description

In the buy() function of the Auction.sol contract, the total cost is calculated by multiplying the current price by the amount to be purchased, as shown below:

function buy(uint256 amount) external whenActive {
...
uint256 cost = price * amount;
...
}

Both price and amount use the same number of decimals - 6 in this case, as USDC is used. However, the cost value does not properly scale back to 6 decimals, causing it to be significantly higher than expected. As a result, users end up paying substantially more than intended.

Context

Impact

High. Users pay substantially more for ZENO tokens than intended.

Likelihood

High. This issue occurs in every ZENO token purchase.

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_wrongZenoCost() public {
// Deal the necessary USDC to the user to buy 1 ZENO bond
uint256 amount = 1e6;
uint256 cost = amount * auction.getPrice();
deal(address(usdc), USER, cost);
// Store user's initial USDC balance
uint256 userInitialUsdcBalance = usdc.balanceOf(USER);
// Skip timestamp by 1 second to activate the auction
skip(1);
// Buy 1 ZENO bond
vm.startPrank(USER);
usdc.approve(address(auction), cost);
auction.buy(amount);
vm.stopPrank();
// Store user's final USDC balance
uint256 userFinalUsdcBalance = usdc.balanceOf(USER);
// Assert that the actual cost is significantly higher than the starting price of ZENO
assertGt(userInitialUsdcBalance - userFinalUsdcBalance, auction.getDetails().startingPrice);
console.log("COST OF BUYING 1 ZENO :", userInitialUsdcBalance - userFinalUsdcBalance); // 999,995.000000 USDC
console.log("ZENO STARTING PRICE :", auction.getDetails().startingPrice); // 1.000000 USDC
}
}

Logs:

Ran 1 test for test/foundry/ZENOTest.t.sol:ZENOTest
[PASS] test_wrongZenoCost() (gas: 383362)
Logs:
COST OF BUYING 1 ZENO : 999995000000
ZENO STARTING PRICE : 1000000
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 14.95ms (1.40ms 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_wrongZenoCost -vvv

Recommendation

To correctly scale the cost value, adjust it according to the token decimals. Additionally, introduce a require statement after computing cost to prevent users from paying nothing when purchasing very small amounts of ZENO tokens.

...
+import "@openzeppelin/contracts/utils/math/Math.sol";
...
contract Auction is IAuction, Ownable {
+ using Math for uint256;
...
function buy(uint256 amount) external whenActive {
require(amount <= state.totalRemaining, "Not enough ZENO remaining");
uint256 price = getPrice();
- uint256 cost = price * amount;
+ uint256 cost = price.mulDiv(amount, 10 ** zeno.decimals(), Math.Rounding.Ceil);
+ require(cost != 0, "Invalid Amount");
require(usdc.transferFrom(msg.sender, businessAddress, cost), "Transfer failed");
bidAmounts[msg.sender] += amount;
state.totalRemaining -= amount;
state.lastBidTime = block.timestamp;
state.lastBidder = msg.sender;
zeno.mint(msg.sender, amount);
emit ZENOPurchased(msg.sender, amount, price);
}
...
}
Updates

Lead Judging Commences

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

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.