Summary
The wrong scaling of the cupon bond price makes Zeno buyers to pay more USDC than they should.
Vulnerability Details
Zeno it's a standard ERC20 token with 18 decimals.
Users can buy Zeno tokens using the Auction::buy function:
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");
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);
}
The Dutch auction price decreases linearly between startingPrice and reservePrice.
The cost, paid in usdc tokens is calculated as price * amount with no Zeno decimals scaling down.
For example, for a cupon bond price of 100e6 usdc, the buyer will pay 100e6 * 1e18 = 1e20 * 1e6 usdc.
Impact
Buyers will pay much more for each cupon bond.
Tools Used
Recommendations
Scale down the cost by the decimals of Zeno token. In this way the cost will be in usdc precision. To avoid leaking dust value, the buyer may only be allowed to buy whole zeno bonds amounts.
function buy(uint256 amount) external whenActive {
require(amount <= state.totalRemaining, "Not enough ZENO remaining");
uint256 price = getPrice();
+ amount = amount / 1e18 * 1e18; // no fractional amounts
- uint256 cost = price * amount;
+ uint256 cost = price * amount / 1e18;
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);
}