Executive summary
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.
Category scores
Findings and status
| Severity | Total | Resolved | Accepted / intended | Open |
|---|---|---|---|---|
| High | 2 | 2 | 0 | 0 |
| Medium | 4 | 3 | 1 | 0 |
| Low | 9 | 4 | 5 | 0 |
| Info | 14 | 11 | 3 | 0 |
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.
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.
Deploys the ZxoToken. Checks pair decimals, mints coreLP + coreStake to Zxo and v3StartAmnt to the deployer.
Creates and initializes the pool, requires price == launch price, and mints the single-sided core LP.
Seeds external DEXes only. NFTs are sent to 0xdEaD. Never seed the main position manager here.
Burns the deployer's remaining balance. Permanently blocks further seeding.
Requires isFinalized. Mints the core stake, which opens staking.
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.
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
| Invariant | Why it holds |
|---|---|
| Pending never underflows | rewardDebt 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 symmetric | Bonus liquidity is added to pos.liquidity and totalStaked together and removed together. |
| Referral is not additive | The referral share comes out of the user's pending amount. A self-referral wallet earns nothing extra. |
| Record ⇒ custody | A record with owner ≠ 0 and liquidity > 0 exists only while Zxo holds the NFT. The one exit deletes the record before transferring. |
| Exit always available | Unstake has no health gate and no owner gate. The fail-safe subtractions stop accounting drift from blocking exits. |
| Pool cannot un-launch | The core stake keeps stakes ≥ 1 permanently. |
Stake range and health (18/18 pair)
| Zxo price | Tick | State |
|---|---|---|
| > ~1.0305 | > +300 | Unhealthy |
| ~0.9704 – ~1.0305 | (−300, +300) | Healthy |
| < ~0.9704 | < −300 | Unhealthy |
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 parity | 0 | 4 | 10 | 16 | 20+ |
|---|---|---|---|---|---|
| Bonus | 0% | 4% | 10% | 16% | 20% |
| With HOLDER | 0% | 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%.
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 · role | Can | Cannot |
|---|---|---|
| ZxoToken owner = Zxo, permanent | Mint | Transfer 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. |
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.
| Pair | Zxo is | Launch tick | Core LP range | Core stake range | User stake range |
|---|---|---|---|---|---|
| 18 / 18 | token0 | 10 | [11, 300] | [31, 300] | [−300, 300] |
| 18 / 18 | token1 | −10 | [−300, −11] | [−300, −31] | [−300, 300] |
| 18 / 6 | token0 | −276314 | [−276313, −276024] | [−276293, −276024] | [−276624, −276024] |
| 18 / 6 | token1 | 276314 | [276024, 276313] | [276024, 276293] | [276024, 276624] |
| 18 / 8 | token0 | −230260 | [−230259, −229970] | [−230239, −229970] | [−230570, −229970] |
| 18 / 8 | token1 | 230260 | [229970, 230259] | [229970, 230239] | [229970, 230570] |
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.
| ID | Risk | Team position |
|---|
Out of scope by design (declared before the audit)
| Behavior | Rationale |
|---|---|
Bare revert() without reasons | Saves gas and bytecode. Frontends pre-check conditions. |
| Unstaking forfeits pending rewards | The protocol pays out only while the tick range is healthy. Users harvest first. |
| HOLDER bonus can be bought, used and sold | The HOLDER token is only available on public DEXes. The team treats this as volume and participation. |
| Tick-range requirement on stake | Covered by the user-set slippage parameter. |
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.
| Contract | Address | Block | Source against Revision 2.5 |
|---|---|---|---|
| ZxoTreasury | 0x0341A7a6CC031662526492e10796E551125AE2bc | 509546642 | Identical (hash match) |
| ZxoOracle | 0x98c9695bb012A42FDcB734324A78E63eE6c23D44 | 509547888 | Identical (hash match) |
| ZxoV3Deployer | 0xb6CB455CB67d59DD269C865428acece75eA08594 | 509561805 | Identical apart from the ArbitrumLib import (hash match) |
| Zxo | 0x5356B3004D0394dEA51161d175265EC8F92c96b7 | 509563331 | No code difference in Zxo.sol or ZxoLib.sol (line match); ABI matches |
| ZxoToken (pool 0) | 0x304441b6BDADA52d41f4AA0A0b13dC4185B3979B | 509698291 | Identical (hash match) |
Confirmed on-chain
- Deploy order: Treasury, Oracle, V3 Deployer, Zxo, then
setZxoContractonce - 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
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
| Item | Status | Evidence |
|---|---|---|
| Switch the ChainLib import to the target chain and set the three Zxo addresses | Verified | The Arbitrum deployment imports ArbitrumLib with all three addresses set (hash and line match). |
| No "stack too deep" in createPool and initializePool | Verified | Zxo compiled and deployed on Arbitrum with via-IR off. The team reports the same on PulseChain Testnet. |
| Zxo runtime bytecode under 24,576 bytes | Verified | The deployment at block 509563331 succeeded, which the size limit would otherwise prevent. |
| compile.py stops on standard-JSON errors | Verified | Done in Revision 2. |
| Verification input compiles with no local files and matches the build | Verified | All five Arbitrum contracts are exact-match verified on Sourcify from that input. |
| EVM version Shanghai | Verified | Sourcify reports evmVersion shanghai, optimizer 200 runs, via-IR off for all five. |
| Set HOLDER_TOKEN and HOLDER_THRESHOLD in each chain library | Open | Set 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 ZxoV3Deployer | Team-reported | The 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 results | Open | The 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 multisig | Open | Deferred by the team until after all mainnet deployments (I-14). The owner on Arbitrum is a single wallet. |
Tests
| Item | Status | Evidence |
|---|---|---|
| Read slot0 on every target DEX | Open | Works 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 Ethereum | Open | Not evidenced. Fork-test before the Ethereum deployment. |
| Full launch order; core stake reverts until finalizeToken | Verified | Six 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 price | Team-reported | Confirmed by the team. |
| createPool reverts when decimals don't match | Team-reported | Confirmed by the team. |
| Unstake to a contract without an NFT receiver reverts; retry with another recipient works | Team-reported | Confirmed 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 liquidity | Open | No 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 tokens | Open | No 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-up | Verified | Seven 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 1 | Team-reported | The 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. Passaddress(0)to send the NFT to the caller.queryData(…, 4)returns(false, 0, 0)when a price can't be read, instead of reverting. TreatsqrtPriceX96 == 0as "not ready".totalGlobalStakes()andreferralsPerUser_()now returnuint32.- New on ZxoV3Deployer:
isFinalized(address),finalizeToken(address)and the eventTokenFinalized(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 scanningpoolInfo(i).rewardToken.queryData(…, 4)"healthy" now also requires the 15-minute TWAP in range. UseZxoOracle.twapTick(pool)to show the average price and explain a paused pool.- Trust panel for each pool:
isFinalizedis true, the deployer balance is 0, everyPoolStartedposman is canonical, and the seeded NFTs are owned by 0xdEaD.
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.
delete on an element of a memory bytes array; the only deletes in the build clear storage mappings. Keep viaIR off.
| File | Lines | SHA-256 (prefix) | Role |
|---|---|---|---|
| Zxo.sol | 1067 | 8c8c2947cfe6 | Farm, staking, emissions, core LP |
| libraries/ZxoLib.sol | 502 | 2af09f5c47af | Ticks, bonus, packing, validation |
| ZxoV3Deployer.sol | 331 | 3c8fb36af7dd | External DEX seeding and finalization |
| ZxoTreasury.sol | 231 | aa47be5c2210 | Fee and referral receiver |
| ZxoVerify.sol | 211 | 02ff34b11012 | Deployment sanity check |
| tokens/ZxoToken.sol | 103 | 87ba572691dd | Per-pool reward token |
| interfaces/Iv3PosManager.sol | 56 | 0a8356439ff0 | Position manager interface |
| interfaces/IZxo.sol | 22 | 5fd91cc8d935 | poolInfo reader |
| interfaces/IZxoToken.sol | 12 | 856487dac631 | mint, coreLP, coreStake |
| interfaces/IUniswapV3.sol | 7 | e5c8cd93d1d8 | Factory interface |
| interfaces/IZxoV3Deployer.sol | 5 | d332e8f4212b | isFinalized |
| ZxoOracle.sol | 26 | 6ac1f047bc61 | 15-minute TWAP tick reader |
| interfaces/IZxoOracle.sol | 17 | bb73eefaf56f | twapTick, inRange, WINDOW |
| libraries/*Lib.sol (11 chains) | 21–22 each | various | Per-chain addresses and HOLDER threshold (all addresses EIP-55 valid) |