Core Contracts

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

Missing redemption rate calculation in ZENO.sol

Summary

The RAAC-ZENO bond auction feature is contained within two contracts: ZENO and Auction.

The Auction contract handles the logic behind purchasing bonds; the ZENO contract handles the logic for redemption.

The ZENO contract does not calculate the redemption amounts properly which leads to users always receiving dust.

From the docs:

• Bond prices and redemption rates are dynamically adjusted based on market conditions

Vulnerability Details

function buy(uint256 amount) external whenActive {
require(amount <= state.totalRemaining, "Not enough ZENO remaining");
uint256 price = getPrice();
> uint256 cost = price * amount;
require(usdc.transferFrom(msg.sender, businessAddress, cost), "Transfer failed");
// SNIP
> zeno.mint(msg.sender, amount);
emit ZENOPurchased(msg.sender, amount, price);
}

On the buy side, price is used to determine how much funds need to be pulled from the user.

The cost for a ZENO bond is calculated as price * amount , where amount is the number of ZENO bonds a user is purchasing.

function redeem(uint amount) external nonReentrant {
// SNIP
totalZENORedeemed += amount;
> _burn(msg.sender, amount);
> USDC.safeTransfer(msg.sender, amount);
}
function redeemAll() external nonReentrant {
// SNIP
uint256 amount = balanceOf(msg.sender);
totalZENORedeemed += amount;
> _burn(msg.sender, amount);
> USDC.safeTransfer(msg.sender, amount);
}

The redemption side is missing the calculation that multiplies the number of ZENO bonds to redeem by a redemption rate to obtain the number of reward tokens to dispense.

Instead, the raw input value, amount, is transferred to the user.

There should be an additional calculation in both redeem() and redeemAll() that scales the payout value to the correct decimals of the output token and multiplies by a redemption rate value.

Add the below test into test/Zeno/Integration.test.js

Run with the following command: npm run test:unit:all

it("Lack of redemption rate calculation results in dust payout amounts on redeem", async function () {
let amountToBuy = 10;
// Increase time to auctionStartTime
await ethers.provider.send("evm_increaseTime", [3600*2]);
const auctionStateForPrice = await auction1.state();
const price = await auction1.getPrice();
const cost = parseFloat(price) * amountToBuy;
log(`Cost: ${cost}`); // in 6 dp
const allowance = await usdc.allowance(addr1.address, auction1Address);
if (allowance < cost) {
const approveTx = await usdc
.connect(addr1)
.approve(auction1Address, cost);
await approveTx.wait();
}
const usdcBalance = await usdc.balanceOf(addr1.address);
const zenoBalance1 = await zeno1.balanceOf(addr1.address);
log(`USDC Balance of addr1 before buy(): ${usdcBalance}`);
log(`ZENO Balance of addr1 before buy(): ${zenoBalance1}`);
await auction1.connect(addr1).buy(amountToBuy);
const usdcBalance1 = await usdc.balanceOf(addr1.address);
const zenoBalance2 = await zeno1.balanceOf(addr1.address);
log(`USDC Balance of addr1 after buy(): ${usdcBalance1}`);
log(`ZENO Balance of addr1 after buy(): ${zenoBalance2}`);
// Increase time to maturity date: one year
await ethers.provider.send("evm_increaseTime", [86400 * 365 + 1]);
await ethers.provider.send("evm_mine");
const amountToRedeem = 6;
// send money from business to zeno1
const zenoBalanceInput = await usdc.balanceOf(businessAddress);
await usdc
.connect(businessAccount)
.transfer(zeno1Address, zenoBalanceInput);
// check zeno1 received the money
const zenoBalance = await usdc.balanceOf(zeno1Address);
expect(zenoBalance).to.equal(zenoBalanceInput);
// ======== REDEEM ==========
const beforeRedeemBalance = await usdc.balanceOf(addr1.address);
await zeno1.connect(addr1).redeem(amountToRedeem);
const finalBalance = await usdc.balanceOf(addr1.address);
const zenoBalance3 = await zeno1.balanceOf(addr1.address);
log(`ZENO Balance of addr1 after redeem(): ${zenoBalance3}`);
log(`USDC Balance of addr1 before redeem(): ${beforeRedeemBalance}`);
log(`USDC Balance of addr1 after redeem(): ${finalBalance}`);
log(`Difference: ${finalBalance - beforeRedeemBalance}`);
});
Balance of addr1: 1000000000
Cost: 1000000000
USDC Balance of addr1 before buy(): 1000000000
ZENO Balance of addr1 before buy(): 0
USDC Balance of addr1 after buy(): 33479160
ZENO Balance of addr1 after buy(): 10
ZENO Balance of addr1 after redeem(): 4
USDC Balance of addr1 before redeem(): 33479160
USDC Balance of addr1 after redeem(): 33479166
Difference: 6
✔ Lack of redemption rate calculation results in dust payout amounts on redeem

In the test case, a user purchases 10 ZENO bonds, waits for redemption period to start, then redeems 6 bonds.

As shown in the output, for redeeming 6 ZENO bonds, the user receives 6 USDC tokens in return (including decimals), which is dust.

Impact

All users who participate in purchasing ZENO bonds will receive incorrect amounts on redemption.

Tools Used

Manual Review

Recommendations

Modify redeem() and redeemAll() to include calculations for redemptions, which includes scaling of output amount to correct decimals and usage of a redemption rate value, similar to cost calculation in buy().

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 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.