Smart contract security review · Revision 3.1

Zxo Protocol Security Audit

Deployed on Arbitrum and PulseChain · source verified
Date
5 October 2026
Compiler
solc 0.8.36
Scope
24 Solidity files · 2,831 lines
DEX model
Uniswap V3 and forks (PancakeSwap-style, LibertySwap)
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.8/10
6.6 → 8.5 after remediation → 8.8 deployed

Unweighted mean of ten categories. Raised in Revisions 3.0 and 3.1 on verified evidence from the Arbitrum and PulseChain deployments; 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 Revisions 3.0 and 3.1 checked the Sourcify-verified source of the Arbitrum and PulseChain deployments against the reviewed files and read on-chain events and transactions through block explorer APIs. 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.1 · 5 October 2026 · PulseChain deployment review The six contracts deployed on PulseChain mainnet (including both ZxoVerify runs) are exact-match verified on Sourcify and are the same audited code: ten files match by keccak256 hash and the rest line by line. PulseChain uses LibertySwap, a PancakeSwap-style V3 DEX, so this is the first live confirmation of the H-02 fix on that pool type. Nine pools launched in the enforced order across 100 transactions; a reward claim was reproduced to the last wei, the HOLDER bonus applied at exactly 1.25 times, and core LP fees were collected to the treasury. Score raised from 8.7 to 8.8. No new findings. I-14 (single-wallet owner) now also applies to PulseChain. Details are in the separate PulseChain Deployment Report. Correction, same day. The first issue of this revision described the PulseChain core stakes as large and said stakers earn about half of the target APR while a core stake dominates. In value the core stakes are small, about $330 to $1,030 per pool, and the half-of-target case applies only to pools holding test-size stakes. M-03, the pool notes and the score rationale now say so. Scores are unchanged.
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.1) 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

PulseChain mainnet deployment

The contracts below were deployed on 3 October 2026 and verified on Sourcify with an exact match of creation and runtime bytecode. The main DEX is LibertySwap, a PancakeSwap-style V3 DEX. The full evidence is in the Zxo PulseChain Deployment Report.

ContractAddressBlockSource against the audited code
ZxoTreasury0x923cbe1C05e9c0014D6514a506A8E74B9254FF8627704288Identical (hash match)
ZxoOracle0xC8CaA5F5961d4679C79A18731C53b6023Ac8862B27704319Identical (hash match)
ZxoVerify (first run)0xa8D7A6A9291E60400F0CA64c5dfed64997Cd381727704333Identical apart from the PulseChainLib import (hash match)
ZxoV3Deployer0x3D62C13A12F246A2CAC2d7fCe1A43D63ED253FBE27704349Identical apart from the PulseChainLib import (hash match)
ZxoVerify (second run)0x923028273B38b24fb98F846f0e4c809fAB58523f27704375Identical apart from the PulseChainLib import (hash match)
Zxo0x813984963d4e0CC7fF5d04E830767fe63D815B0A27704394No difference in Zxo.sol, ZxoLib.sol or ZxoToken.sol (line match); ABI matches

Confirmed on-chain

  • Deploy order: Treasury, Oracle, ZxoVerify, V3 Deployer, ZxoVerify, Zxo, then setZxoContract once
  • Nine pools launched; every reward token finalized before its core stake
  • Price reads and the 15-minute average work on LibertySwap pools (H-02 fix confirmed live)
  • 16 stakes, 6 harvests, 2 unstakes, 17 config changes and 1 core LP fee collection
  • A reward claim reproduced to the last wei; HOLDER bonus exactly 1.25 times
  • Four addresses other than the deployer staked or harvested

Read this with care

  • 94 of 100 transactions came from the deployer wallet
  • The owner is a single wallet until the multisig (I-14)
  • Target APRs range from 10% to 510%; referral rates from 3% to 30%
  • Core stakes are small, about $330 to $1,030 per pool. Stakers earn about 90% of target once user stakes are ten times the core stake, and about half only while a pool holds test-size stakes (M-03)
  • Two early initializePool calls reverted on a gas limit that was too low; nothing changed on-chain
  • No contract state was read directly; owner and settings are inferred from events up to 5 October 2026
12

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 and PulseChain deployments import their own chain library with all three addresses set (hash match on PulseChain; hash and line match on Arbitrum).
No "stack too deep" in createPool and initializePoolVerifiedZxo compiled and deployed on Arbitrum and PulseChain mainnet with via-IR off.
Zxo runtime bytecode under 24,576 bytesVerifiedThe Arbitrum and PulseChain deployments 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 eleven contracts on Arbitrum and PulseChain are exact-match verified on Sourcify from that input.
EVM version ShanghaiVerifiedSourcify reports evmVersion shanghai, optimizer 200 runs, via-IR off for every deployed contract.
Set HOLDER_TOKEN and HOLDER_THRESHOLD in each chain libraryOpenSet for Arbitrum (token 0x6F29…E66A) and PulseChain (token 0xDb3f…40d6), each with a 3.69 billion threshold; the HOLDER bonus applied correctly on PulseChain. Still zero on the other 7 mainnets; the team will set each before deployment and the constructor rejects zero values.
Run ZxoVerify before and after ZxoV3DeployerVerifiedOn PulseChain both ZxoVerify deployments are on-chain, exact-match verified, and succeeded (ZxoVerify reverts unless its checks pass). On Arbitrum the team reports the same steps; no ZxoVerify address was provided there, and the Zxo constructor repeats the checks.
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 and on PulseChain is a single wallet.

Tests

ItemStatusEvidence
Read slot0 on every target DEXVerifiedWorks live on Uniswap V3 (Arbitrum) and on LibertySwap, a PancakeSwap-style V3 DEX (PulseChain, 16 stakes and 6 harvests). PancakeSwap on BNB Chain uses the same pool design and has not been run yet.
Stake a USDT pair on EthereumOpenNot evidenced. Fork-test before the Ethereum deployment.
Full launch order; core stake reverts until finalizeTokenVerifiedSix pools on Arbitrum and nine on PulseChain 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, claims on both chains 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-upVerifiedNine mainnet unstakes on two chains succeeded after 24.4 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.
13

Scope and file fingerprints

Revision 3.0 adds the Arbitrum One deployment review of 2 October 2026 and Revision 3.1 the PulseChain mainnet review of 5 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 and the PulseChain Deployment Report. 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)