Core Contracts

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

loss of funds for users when redeeming zeno Token

Summary

Buyers will loose funds

Vulnerability Details

In Auction.sol.buy(), the cost for
calculating amount of zeno to be minted is defined as :

uint256 cost = price * amount;

function buy(uint256 amount) external whenActive {
require(amount <= state.totalRemaining, "Not enough ZENO remaining");
//get price and calculate the cost
@>1 uint256 price = getPrice();
uint256 cost = price * amount;
// transfer the cost from the user.
@>2 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);
}

During redemption in ZENO.sol.redeem(), the function directly transfers the redeem amount to the user without considering the price of zeno.

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();
}
// burn zeno and transfer the amount of usdc to the user
totalZENORedeemed += amount;
_burn(msg.sender, amount);
@> USDC.safeTransfer(msg.sender, amount);
}

This results in a significant loss for the user effectively, the funds they spent on purchasing ZENO are not fully returned when they redeem.

Proof Of Concept

See how to integrate foundry to hardhat project
. Create a new file POC.t.sol in project /test/ folder . Paste the poc and run forge test --mt test_POC

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "contracts/zeno/AuctionFactory.sol";
import "contracts/zeno/ZENOFactory.sol";
import "contracts/zeno/Auction.sol";
import "contracts/zeno/ZENO.sol";
contract MockUSDC is ERC20 {
constructor() ERC20("USDC", "USDC") {}
function decimals() public pure override returns (uint8) {
return 6;
}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}
contract IntegrationTest is Test {
MockUSDC public usdc;
address owner;
address user;
address businessAccount;
AuctionFactory public auctionFactory;
ZENOFactory public zenoFactory;
Auction public auction;
ZENO public zeno;
function setUp() public {
owner = address(this);
user = makeAddr("addr1");
businessAccount = makeAddr("business");
// Deploy USDC with initial supply
usdc = new MockUSDC();
// Deploy Factories
auctionFactory = new AuctionFactory(owner);
zenoFactory = new ZENOFactory(owner);
// Create ZENO contract
// Setup Auction parameters
uint256 auctionStartTime = block.timestamp ;
uint256 auctionEndTime = auctionStartTime + 1 days;
uint256 startingPrice = 100 * 1e6; // 100 USDC
uint256 reservePrice = 10 * 1e6; // 10 USDC
uint256 totalZENOAllocated = 100 ether; // 10 Bonds
uint256 maturityDate = block.timestamp + 365 days;
zenoFactory.createZENOContract(address(usdc), maturityDate);
zeno = zenoFactory.getZENO(0);
// Create Auction
auctionFactory.createAuction(
address(zeno),
address(usdc),
businessAccount,
auctionStartTime,
auctionEndTime,
startingPrice,
reservePrice,
totalZENOAllocated
);
auction = auctionFactory.getAuction(0);
// Transfer ZENO ownership
zenoFactory.transferZenoOwnership(0, address(auction));
}
function test_POC() public {
uint256 price = auction.getPrice();
uint256 amount = 100 ;
uint256 cost = amount * price;
assertEq(price, 100000000);
assertEq(cost, 10000000000);
// mint the cost of zeno tokens to the user
usdc.mint(user, cost);
usdc.mint(address(zeno), 1 ether);
// user buys zeno with `cost` amount of usdc
vm.startPrank(user);
assertEq(usdc.balanceOf(user), cost);
usdc.approve(address(auction), cost);
vm.warp(block.timestamp + 1);
auction.buy(amount);
assertEq(usdc.balanceOf(user), 104100);
assertEq(zeno.balanceOf(address(user)), amount);
// time pass and user redeems
vm.warp(block.timestamp + 365 days);
zeno.redeem(amount);
// user gets less the amount
assertEq(usdc.balanceOf(user), 104200);
vm.stopPrank();
}
}

Impact

Loss of user usdc funds

Tools Used

Manual Review

Recommendations

Before transferring usdc to user, get the price of the redeeming amount of zeno and transfer the cost to the user.

Updates

Lead Judging Commences

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

ZENO.sol implements fixed 1:1 redemption with USDC regardless of auction purchase price, breaking zero-coupon bond economics and causing user funds to be permanently lost

Support

FAQs

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

Give us feedback!