Smart contract security review · Revision 3.0

Zxo Protocol Security Audit

Deployed on Arbitrum One · source verified
Date
2 October 2026
Compiler
solc 0.8.36
Scope
24 Solidity files · 2,831 lines
DEX model
Uniswap V3 and forks (Pancake, Liberty)
Method
Manual line review, numeric tick/price checks, verified-source and on-chain event review
Reviewer
Claude (Anthropic), AI-assisted
01

Executive summary

Overall score
8.7/10
6.6 → 8.5 after remediation → 8.7 deployed

Unweighted mean of ten categories. Raised in Revision 3.0 on verified evidence from the Arbitrum deployment; see the checklist for what is still open.

Zxo is a single-contract liquidity farm. Users stake Uniswap V3 positions minted on a fixed tick range around parity, and earn a newly minted reward token (one ZxoToken per pool). Emissions follow a target APR on staked value. Each pool is anchored by a permanently locked, single-sided core LP that sells the reward token on the way up.

The original review found 2 High, 4 Medium, 7 Low and 10 informational issues. Both High issues and every Medium issue that could be fixed in code have been fixed. One Medium remains, the core-stake weighting (M-03), which the team accepted as a design trade-off. The spot-price stake bonus left over from M-02 is also accepted. Both are recorded in the accepted-risk register.

No path was found that lets anyone take user principal. Unstake has no price-range gate and no owner gate, so staked positions can always be withdrawn after the 1-day lock-up. The core LP has no withdrawal function.

20Resolved
9Accepted / intended
0Open High / Medium
0Open informational
Limits of this review Revision 3.0 checked the Sourcify-verified source of the Arbitrum deployment against the reviewed files and read on-chain events and transactions through a block explorer API. It made no direct contract state reads and ran no fuzzing, static analysis or fork tests. Statements marked as reported by the team were not independently checked. This AI-assisted review does not replace an independent human audit, which is still recommended for contracts that hold user savings.
Revision 3.0 · 2 October 2026 · Arbitrum deployment review The five contracts deployed on Arbitrum One are exact-match verified on Sourcify and are built from the Revision 2.5 source: seven files match by keccak256 hash and the rest line by line. The only changes are the license header, the ArbitrumLib import and the Arbitrum configuration (the HOLDER token and threshold changed). Six pools were launched in the enforced order, and stake, harvest, unstake and config changes all ran on mainnet; a live reward claim was reproduced to the last wei from the audited formula. Score raised from 8.5 to 8.7. One accepted item added (I-14: the owner is a single wallet until the planned multisig). The pre-mainnet checklist is replaced by a status table. Details are in the separate Arbitrum Deployment Statement.
Revision 2.5 · 27 September 2026 All open items are closed. SepoliaTestLib now has a complete ZXO_ORACLE line, so the tree builds (I-12). ConfigUpdated now logs the value actually stored, including the capped target APR, from one shared log call (I-13). ZxoVerify has full NatSpec for the deploy order and both constructor flags. No change to runtime logic, funds flow or scores.
Revision 2.4 · 27 September 2026 ZxoVerify now checks ZxoOracle on-chain, behind a second constructor flag (verifyOracle): code present, WINDOW() == 900, and a fail-safe answer (no average, never in range) for an address that is not a pool. PoolInitialized now indexes the reward token, matching PoolCreated; I-13 is narrowed to the ConfigUpdated value. No change to runtime logic, funds flow or scores.
Revision 2.3 · 27 September 2026 The Zxo constructor now validates the full ChainLib configuration with readable error messages, at no runtime bytecode cost. New events record deployment, pool creation, pool initialization, core fee collection and config updates, and updatePoolConfig rejects unknown data types. All chain libraries now declare ZXO_ORACLE. I-11 resolved. Two informational items added (I-12 build, I-13 event data).
Revision 2.2 · 27 September 2026 Adds ZxoOracle: the health gate now also requires a 15-minute TWAP inside the stake range, which closes the same-transaction harvest bypass (M-02 resolved; the spot-based bonus remains accepted). Events now go through one shared function, redundant library checks and the write-only tokensIds_ mapping were removed, uint64 casts use a checked downcast, and the Zxo constructor validates the HOLDER settings. Revision 2.1 moved HOLDER_THRESHOLD into each chain library and added HOLDER checks to ZxoVerify. One informational item added (I-11, oracle setup).
02

Category scores

Original audit Current (Revision 3.0) Scale 0–10
03

Findings and status

SeverityTotalResolvedAccepted / intendedOpen
High2200
Medium4310
Low9450
Info141130
04

Function flows

Each flow lists the order of operations in the current code. Tags mark each step as a check (reverts on failure), a state effect, an external interaction, or an event.

checkReverts on failure effectWrites state externalCalls another contract eventEmits log
05

Pool launch sequence

The contracts enforce the order of steps 4 and 5. The core-stake mint reverts until the reward token is finalized on the deployer, and staking can't open until the core stake exists. Step 2 comes before any Zxo is in public hands, so no one can seed the main pool at a price of their choosing.

STEP 1createPool

Deploys the ZxoToken. Checks pair decimals, mints coreLP + coreStake to Zxo and v3StartAmnt to the deployer.

STEP 2initializePool · core

Creates and initializes the pool, requires price == launch price, and mints the single-sided core LP.

STEP 3startV3pool ×n

Seeds external DEXes only. NFTs are sent to 0xdEaD. Never seed the main position manager here.

STEP 4finalizeToken

Burns the deployer's remaining balance. Permanently blocks further seeding.

STEP 5initializePool · stake

Requires isFinalized. Mints the core stake, which opens staking.

Seeding-window risk at production size To push the tick from the launch tick (10) past the core-stake lower bound (31), buyers must absorb about 7% of the core LP. At the planned core sizes that is roughly $350k for a $5M core, $1.05M for $15M and $70M for $1B. Seeded DEX positions are too small to move the main pool through arbitrage. Before step 5, confirm that queryData(0, poolId, 4, 0) returns a tick below 31 (or above −31 when Zxo is token1). Oracle setup (Revision 2.2): after step 2, run set-observations.py for the new pool (it calls increaseObservationCardinalityNext with a size suited to the chain's block time). Staking and harvest stay closed until the pool has 15 minutes of price history.
06

Farming logic (MasterChef on V3 liquidity)

Accounting model

acc += elapsed × rewardRate × 1e18 / totalStaked
pending = liquidity × acc / 1e18 − rewardDebt
rewardDebt = liquidity × acc / 1e18   (on stake, claim)

Rewards are minted on claim, not pre-funded. Forfeited or unclaimed rewards are never minted. Every user position shares one fixed tick range, so liquidity is proportional to position value at any price, and liquidity weighting is fair between users.

Emission controller

rewardRate = min( TVL × targetApr,
                  TVL × maxApr,          // 2× maxNormalAPR in year 1
                  max30dRate / 30 days )
cooldown   = 1 hour after each change

TVL is totalTokens, the 18-decimal stake-time amounts summed at 1:1. Since M-01, every stake counts toward TVL. Lowering the rate by hand (updatePoolConfig type 2) only lasts until the next auto-adjust; lowering targetAprBps is the lasting control.

Invariants verified

InvariantWhy it holds
Pending never underflowsrewardDebt is always set from the current acc, and acc only increases.
No retroactive rate changes_updatePool runs before every change to totalStaked and before every rate change.
Bonus is symmetricBonus liquidity is added to pos.liquidity and totalStaked together and removed together.
Referral is not additiveThe referral share comes out of the user's pending amount. A self-referral wallet earns nothing extra.
Record ⇒ custodyA record with owner ≠ 0 and liquidity > 0 exists only while Zxo holds the NFT. The one exit deletes the record before transferring.
Exit always availableUnstake has no health gate and no owner gate. The fail-safe subtractions stop accounting drift from blocking exits.
Pool cannot un-launchThe core stake keeps stakes ≥ 1 permanently.

Stake range and health (18/18 pair)

Zxo priceTickState
> ~1.0305> +300Unhealthy
~0.9704 – ~1.0305(−300, +300)Healthy
< ~0.9704< −300Unhealthy

When the pool is unhealthy, stake and harvest revert, unstake still works, and rewards keep accruing. Bounds are strict. For 6- and 8-decimal pairs the ranges center on ±276324 and ±230270.

Stake bonus curve

Ticks above parity04101620+
Bonus0%4%10%16%20%
With HOLDER0%5%12.5%20%25%

The bonus reaches its cap at a 53.36% paired ratio. The core LP launches at tick 10, so the organic bonus is expected to be around 10–12%.

07

Owner powers

Zxo owner can

  • Create up to 255 pools and tokens, setting coreLP, coreStake, maxNormalAPR (≤ 320%), decimals (validated), max30dRate and v3StartAmnt
  • Initialize the core LP and core stake once per pool
  • Set targetAprBps from 0 to the cap (turns emissions on or off)
  • Set the referral rate between 3% and 30%
  • Lower rewardRate temporarily
  • Send core-LP trading fees to the treasury
  • Transfer ownership (two-step) or renounce it

Zxo owner cannot

  • Touch user NFTs, principal or unclaimed rewards
  • Block or delay unstake
  • Withdraw or reduce core LP or core-stake liquidity
  • Mint the reward token outside pool creation and emissions
  • Burn anyone's tokens (burn(address) removed)
  • Change tick ranges, fee tier, position manager, factory or reward token
  • Pause, upgrade, or rescue any asset held by Zxo
Contract · roleCanCannot
ZxoToken
owner = Zxo, permanent
MintTransfer or renounce ownership. Burn others' tokens. Holders can burn their own tokens.
ZxoV3Deployer
Ownable2Step
Seed external DEXes with any position manager until the token is finalized. Finalize (burn the remainder). Set ZXO_CONTRACT once.Seed after finalization. Repoint ZXO_CONTRACT. Withdraw tokens on mainnet (testnet-only validator path).
ZxoTreasury
Ownable (single-step)
Sweep any token to the destination. Change the destination after a 7-day delay. Lock the destination forever.Receive ETH. Bypass the delay.
08

Core LP ownership

The core LP NFT is minted to address(this) and its id is stored in poolConfig. Zxo has no function that transfers it or reduces its liquidity. The core liquidity, and all pair tokens it collects as it sells Zxo between ~1.001 and ~1.03, is permanently locked. Trading fees are the only value that leaves, and they go to the fixed treasury through an owner-only call. If Zxo ownership is renounced, those fees are locked too.

Before minting, the launch price is now checked against the pool's actual price (L-01). All six launch prices were checked with a port of Uniswap's TickMath. Each sits exactly one tick outside its core range, on the correct side for a single-sided Zxo deposit.

PairZxo isLaunch tickCore LP rangeCore stake rangeUser stake range
18 / 18token010[11, 300][31, 300][−300, 300]
18 / 18token1−10[−300, −11][−300, −31][−300, 300]
18 / 6token0−276314[−276313, −276024][−276293, −276024][−276624, −276024]
18 / 6token1276314[276024, 276313][276024, 276293][276024, 276624]
18 / 8token0−230260[−230259, −229970][−230239, −229970][−230570, −229970]
18 / 8token1230260[229970, 230259][229970, 230239][229970, 230570]
09

Accepted-risk register

These items were reviewed with the development team and deliberately left as they are. Items the team excluded from scope before the audit are listed at the end.

IDRiskTeam position

Out of scope by design (declared before the audit)

BehaviorRationale
Bare revert() without reasonsSaves gas and bytecode. Frontends pre-check conditions.
Unstaking forfeits pending rewardsThe protocol pays out only while the tick range is healthy. Users harvest first.
HOLDER bonus can be bought, used and soldThe HOLDER token is only available on public DEXes. The team treats this as volume and participation.
Tick-range requirement on stakeCovered by the user-set slippage parameter.
10

Arbitrum One deployment

The contracts below were deployed on 28 September 2026 and verified on Sourcify with an exact match of creation and runtime bytecode. The full evidence, including file hashes, configuration and the on-chain activity reviewed, is in the Zxo Arbitrum Deployment Statement.

ContractAddressBlockSource against Revision 2.5
ZxoTreasury0x0341A7a6CC031662526492e10796E551125AE2bc509546642Identical (hash match)
ZxoOracle0x98c9695bb012A42FDcB734324A78E63eE6c23D44509547888Identical (hash match)
ZxoV3Deployer0xb6CB455CB67d59DD269C865428acece75eA08594509561805Identical apart from the ArbitrumLib import (hash match)
Zxo0x5356B3004D0394dEA51161d175265EC8F92c96b7509563331No code difference in Zxo.sol or ZxoLib.sol (line match); ABI matches
ZxoToken (pool 0)0x304441b6BDADA52d41f4AA0A0b13dC4185B3979B509698291Identical (hash match)

Confirmed on-chain

  • Deploy order: Treasury, Oracle, V3 Deployer, Zxo, then setZxoContract once
  • All constructor checks passed, so the Arbitrum configuration is valid
  • Six pools launched; every reward token finalized before its core stake
  • 11 stakes, 8 harvests, 7 unstakes and 10 config changes, all successful
  • A reward claim reproduced exactly from the audited formula
  • The deployer's testnet-only validator is the zero address

Read this with care

  • All activity so far came from the deployer wallet. It is a team test on mainnet, not public usage
  • The owner is a single wallet until the multisig (I-14)
  • Every pool uses the 320% APR cap; pool 0 targets the 640% first-year ceiling, limited by its 30-day cap
  • Referral rate is 30% on pools 0 and 4
  • No contract state was read directly; owner and settings are inferred from events up to 1 October 2026
11

Checklist status

This replaces the pre-mainnet checklist. Verified means this review saw the evidence in verified source or on-chain records. Team-reported means the development team states it is done and this review did not check it. Open means not done or not evidenced.

Build and configuration

ItemStatusEvidence
Switch the ChainLib import to the target chain and set the three Zxo addressesVerifiedThe Arbitrum deployment imports ArbitrumLib with all three addresses set (hash and line match).
No "stack too deep" in createPool and initializePoolVerifiedZxo compiled and deployed on Arbitrum with via-IR off. The team reports the same on PulseChain Testnet.
Zxo runtime bytecode under 24,576 bytesVerifiedThe deployment at block 509563331 succeeded, which the size limit would otherwise prevent.
compile.py stops on standard-JSON errorsVerifiedDone in Revision 2.
Verification input compiles with no local files and matches the buildVerifiedAll five Arbitrum contracts are exact-match verified on Sourcify from that input.
EVM version ShanghaiVerifiedSourcify reports evmVersion shanghai, optimizer 200 runs, via-IR off for all five.
Set HOLDER_TOKEN and HOLDER_THRESHOLD in each chain libraryOpenSet for Arbitrum (token 0x6F29…E66A, threshold 3.69 billion; both changed since Revision 2.5, confirm they are intended). Still zero on the other 8 mainnets; the team will set each before deployment and the constructor rejects zero values.
Run ZxoVerify before and after ZxoV3DeployerTeam-reportedThe team followed these steps on Arbitrum and PulseChain Testnet. No ZxoVerify address was provided; the Zxo constructor repeats the checks and they passed.
Run Slither or Aderyn and triage the resultsOpenThe team note reports successful testing but does not say a static analyzer was run. No report was provided.
Move Zxo and deployer ownership to a multisigOpenDeferred by the team until after all mainnet deployments (I-14). The owner on Arbitrum is a single wallet.

Tests

ItemStatusEvidence
Read slot0 on every target DEXOpenWorks on Uniswap V3 on Arbitrum (11 live stakes). Not yet run against a PancakeSwap main pool; fork-test before the BNB Chain deployment.
Stake a USDT pair on EthereumOpenNot evidenced. Fork-test before the Ethereum deployment.
Full launch order; core stake reverts until finalizeTokenVerifiedSix pools on Arbitrum followed the order: every TokenFinalized event precedes the core-stake mint. The team confirms the revert when finalization is skipped.
initializePool(core) reverts on a pool pre-initialized at another priceTeam-reportedConfirmed by the team.
createPool reverts when decimals don't matchTeam-reportedConfirmed by the team.
Unstake to a contract without an NFT receiver reverts; retry with another recipient worksTeam-reportedConfirmed by the team. Note from the team: a staking contract must itself be able to pass the to argument, or it cannot recover.
Invariant: totalStaked equals the sum of position liquidityOpenNo invariant test run was reported. On-chain, a claim matched the formula to the last wei, which is consistent with the invariant. The core stake is included by design.
Invariant: totalTokens equals the sum of position tokensOpenNo invariant test run was reported. On-chain, the staked value used for emissions matched the core-stake amount.
Invariant: a staker can always unstake after the 1-day lock-upVerifiedSeven mainnet unstakes succeeded after 24.6 to 37.4 hours. The code has no price or owner gate on unstake.
Independent re-audit of the diff since Revision 1Team-reportedThe team states that all audits passed. No third-party report was provided to this review.

Frontend and ABI changes

  • unstake(uint64 tokenId, uint8 poolId, bool acceptForfeit, address to). The selector changed. Pass address(0) to send the NFT to the caller.
  • queryData(…, 4) returns (false, 0, 0) when a price can't be read, instead of reverting. Treat sqrtPriceX96 == 0 as "not ready".
  • totalGlobalStakes() and referralsPerUser_() now return uint32.
  • New on ZxoV3Deployer: isFinalized(address), finalizeToken(address) and the event TokenFinalized(address indexed, uint256).
  • New event action codes: 8 Deployed, 9 PoolCreated, 10 CoreFeesCollected, 11 ConfigUpdated, 12 PoolInitialized. Codes 0–7 are unchanged.
  • tokensIds_(address) was removed. Map a reward token to its pool by scanning poolInfo(i).rewardToken.
  • queryData(…, 4) "healthy" now also requires the 15-minute TWAP in range. Use ZxoOracle.twapTick(pool) to show the average price and explain a paused pool.
  • Trust panel for each pool: isFinalized is true, the deployer balance is 0, every PoolStarted posman is canonical, and the seeded NFTs are owned by 0xdEaD.
12

Scope and file fingerprints

Revision 3.0 adds the Arbitrum One deployment review of 2 October 2026. The file fingerprints below are the Revision 2.5 working files (27 September 2026), without the license header. The deployed files carry the BUSL-1.1 header and the ArbitrumLib import, so their hashes differ; the keccak256 values of the deployed files are listed in the Arbitrum Deployment Statement. Hashes are the first 12 hex characters of each file's SHA-256. Any later change to these files is outside this review.

Compiler: solc 0.8.36, legacy pipeline (viaIR off) Etherscan lists three known bugs for 0.8.36, all fixed in 0.8.37. None applies to Zxo. MisorderedNamedParametersInRequireWithCustomErrors and SpillSlotCollisionAcrossMutualRecursion only affect the via-IR pipeline, and Zxo has no custom errors in require and no recursion. MemoryByteArrayElementDeleteClearsWholeWord needs delete on an element of a memory bytes array; the only deletes in the build clear storage mappings. Keep viaIR off.
FileLinesSHA-256 (prefix)Role
Zxo.sol10678c8c2947cfe6Farm, staking, emissions, core LP
libraries/ZxoLib.sol5022af09f5c47afTicks, bonus, packing, validation
ZxoV3Deployer.sol3313c8fb36af7ddExternal DEX seeding and finalization
ZxoTreasury.sol231aa47be5c2210Fee and referral receiver
ZxoVerify.sol21102ff34b11012Deployment sanity check
tokens/ZxoToken.sol10387ba572691ddPer-pool reward token
interfaces/Iv3PosManager.sol560a8356439ff0Position manager interface
interfaces/IZxo.sol225fd91cc8d935poolInfo reader
interfaces/IZxoToken.sol12856487dac631mint, coreLP, coreStake
interfaces/IUniswapV3.sol7e5c8cd93d1d8Factory interface
interfaces/IZxoV3Deployer.sol5d332e8f4212bisFinalized
ZxoOracle.sol266ac1f047bc6115-minute TWAP tick reader
interfaces/IZxoOracle.sol17bb73eefaf56ftwapTick, inRange, WINDOW
libraries/*Lib.sol (11 chains)21–22 eachvariousPer-chain addresses and HOLDER threshold (all addresses EIP-55 valid)