Skip to content
SECTION 11 / 15

Verify on chain

How to check every major VIRUS number yourself on Blockscout or with cast, from wiring and host eligibility to fees, burns and developer balances.

§Principle

Nothing VIRUS displays needs to be taken on trust. Every figure comes from a contract read or an event, and this page shows how to reproduce it. The indexer, when used, only adds speed and history; its numbers can be recomputed from the same reads and logs.

§Setup

The examples use Foundry's cast. Replace each placeholder with the real value.

export ETH_RPC_URL=<RPC_URL>      # a Robinhood Chain RPC endpoint; cast reads this variable
# VIRUS
#   <REGISTRY> <COORDINATOR> <FEEDER_FACTORY> <TREASURY>
#   <VIRUS>    a virus token            <FEEDER>   its feeder
#   <HOST>     its host token           <CURVE>    the virus's Pons curve
# Pons
#   <FACTORY>  the Pons V2 launch factory
#   <HOOK>     factory.memeHook()       <ESCROW>   factory.feeEscrow()

With ETH_RPC_URL set, no command below needs --rpc-url. Struct returns are written as tuples, in field order. Enums decode as uint8.

On Blockscout, the same checks are available through each contract's Read contract tab when its source is verified there, its Logs tab for events, and each transaction's Token transfers and Logs. The default explorer is https://robinhoodchain.blockscout.com; that URL is UNVERIFIED in the VIRUS reference audit.

§1. The VIRUS contracts are wired to each other and to Pons

cast call <REGISTRY> "coordinator()(address)"
cast call <REGISTRY> "treasury()(address)"
cast call <COORDINATOR> "registry()(address)"
cast call <COORDINATOR> "feederFactory()(address)"
cast call <COORDINATOR> "ponsFactory()(address)"
cast call <FEEDER_FACTORY> "registry()(address)"
cast call <FEEDER_FACTORY> "implementation()(address)"

Expect: the registry's coordinator is <COORDINATOR>; the coordinator and the feeder factory both point to <REGISTRY>; the coordinator points to <FEEDER_FACTORY> and to the Pons factory.

Admin state:

cast call <REGISTRY> "owner()(address)"
cast call <REGISTRY> "pendingOwner()(address)"
cast call <REGISTRY> "launchesPaused()(bool)"
cast call <REGISTRY> "pendingTreasury()(address)"
cast call <REGISTRY> "pendingTreasuryAt()(uint256)"
cast call <REGISTRY> "pendingCoordinator()(address)"
cast call <REGISTRY> "pendingCoordinatorAt()(uint256)"
cast call <TREASURY> "owner()(address)"

A non-zero pendingTreasury or pendingCoordinator is a change in progress; the ...At value is the earliest time it can execute.

§2. Pons wiring and live terms

cast call <FACTORY> "memeHook()(address)"
cast call <FACTORY> "feeEscrow()(address)"
cast call <FACTORY> "poolManager()(address)"
cast call <FACTORY> "positionManager()(address)"
cast call <FACTORY> "locker()(address)"

cast call <FACTORY> "launchEnabled()(bool)"
cast call <FACTORY> "canLaunch(address)(bool)" <COORDINATOR>
cast call <FACTORY> "canLaunch(address)(bool)" <YOUR_ADDRESS>
cast call <FACTORY> "launchFee()(uint256)"
cast call <FACTORY> "maxCreatorTaxBps()(uint256)"
cast call <FACTORY> "snipeTaxStartBps()(uint256)"
cast call <FACTORY> "snipeTaxSeconds()(uint256)"

cast call <FACTORY> "launchConfigCount()(uint256)"
# LaunchConfig: supply, curveFeeBps, phantomQuote, graduationThreshold, poolFee, tickSpacing, enabled
cast call <FACTORY> "getLaunchConfig(uint256)((uint256,uint256,uint256,uint256,uint24,int24,bool))" 0

# FeePolicy: protocolFeeRecipient, protocolFeeShareBps, buybackBurnBps, hookFeeBps, maxInternalPriceImpactBps
cast call <HOOK> "currentFeePolicy()((address,uint16,uint16,uint16,uint16))"

Compare poolManager() and positionManager() with the Uniswap addresses in Parameters. Compare feeEscrow(), memeHook() and locker() with the secondary-source addresses there, which VIRUS never uses directly.

§3. A host is eligible

# LaunchedToken: token, curve, deployer, creatorFeeRecipient, pairToken, graduationThreshold,
#   poolFee, tickSpacing, creatorTaxBps, buybackEnabled, phase, sweptQuote, sweptTokens, sweptAt, exists
cast call <FACTORY> \
  "getLaunchedToken(address)((address,address,address,address,address,uint256,uint24,int24,uint16,bool,uint8,uint256,uint256,uint256,bool))" \
  <HOST>

cast call <HOST> "launchFactory()(address)"     # must equal <FACTORY>
cast call <HOST> "curve()(address)"             # must equal the curve in the launch record
cast call <HOST> "decimals()(uint8)"            # must be 18
cast call <FACTORY> "approvedPairTokens(address)(bool)" <HOST>
cast call <FACTORY> "pairTokenEconomics(address)(uint256,uint256,uint8)" <HOST>
cast call <REGISTRY> "hostDisabled(address)(bool)" <HOST>

Or run all of it at once through the coordinator:

# HostCheck: isContract, ponsLaunchToken, provenance, pairApproved, economicsUsable, notDisabled,
#   depthOk, viable, hostDepth, childDepth, decimals, ponsCurve, ponsPairToken, phantomQuote, graduationThreshold
cast call <COORDINATOR> \
  "hostCheck(address)((bool,bool,bool,bool,bool,bool,bool,bool,uint8,uint8,uint8,address,address,uint256,uint256))" \
  <HOST>

Depth by hand: read getLaunchedToken(<HOST>), take its pairToken, and repeat while the pair is itself a Pons launch (exists == true). The number of Pons-to-Pons steps is the host's depth; a virus of it has depth one more, at most 3.

§4. The economics a launch would get

# EconomicsPreview: launchConfigId, configEnabled, supply, curveFeeBps, hookFeeBps, protocolShareBps,
#   creatorTaxBps, maxCreatorTaxBps, taxWithinCap, curvePhase(pons,feeder,total), poolPhase(pons,feeder,total),
#   parityExact, launchFeeWei
cast call <COORDINATOR> \
  "previewEconomics(uint256)((uint256,bool,uint256,uint256,uint256,uint256,uint256,uint256,bool,(uint256,uint256,uint256),(uint256,uint256,uint256),bool,uint256))" \
  0

cast call <FACTORY> "previewLaunchEconomics(uint256,address)(bytes32)" 0 <HOST>

Recompute the tax yourself: ceil((800,000 − curveFeeBps × (10,000 − protocolShareBps)) / 10,000), or 0 if negative. Phase figures are scaled (bps × 10,000); divide by 1,000,000 for percent.

§5. A virus is what VIRUS says it is

# Virus: token, curve, host, feeder, developer, treasury, depth, active, creatorTaxBps, strain, launchBlock,
#   launchConfigId, graduationThreshold, firstBuy, inoculationFee, totalFed, totalBurned, devBooked, devClaimed, treasuryPaid
cast call <REGISTRY> \
  "getVirus(address)((address,address,address,address,address,address,uint8,bool,uint16,uint32,uint64,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256))" \
  <VIRUS>

cast call <FACTORY> \
  "getLaunchedToken(address)((address,address,address,address,address,uint256,uint24,int24,uint16,bool,uint8,uint256,uint256,uint256,bool))" \
  <VIRUS>

In the Pons record, check:

Field Must equal
creatorFeeRecipient (4th) <FEEDER>, the registry's feeder
pairToken (5th) <HOST>, the registry's host
creatorTaxBps (9th) The registry's creatorTaxBps
buybackEnabled (10th) false
deployer (3rd) The coordinator. This is Pons metadata; the real developer is the registry's developer.

The feeder side:

cast call <FEEDER_FACTORY> "feederOf(address)(address)" <VIRUS>        # <FEEDER>
cast call <FEEDER_FACTORY> "isFeeder(address)(bool)" <FEEDER>           # true
cast call <FEEDER> "virus()(address)"                                   # <VIRUS>
cast call <FEEDER> "curve()(address)"                                   # <CURVE>
# Config: host, developer, treasury, registry, ponsFactory, feeEscrow, memeHook, feederFactory
cast call <FEEDER> "config()((address,address,address,address,address,address,address,address))"
cast code <FEEDER>

cast code <FEEDER> shows the runtime code of the clone: the 45-byte EIP-1167 minimal proxy, which embeds the address from implementation(), followed by the ABI-encoded Config. That is why none of the config can change.

§6. The creator tax and fee policy of a virus

cast call <CURVE> "feeBps()(uint256)"
cast call <CURVE> "creatorTaxBps()(uint256)"
cast call <FACTORY> "getLaunchFeePolicy(address)((address,uint16,uint16,uint16,uint16))" <VIRUS>

getLaunchFeePolicy is the policy Pons froze at the virus's launch. Recomputing requiredCreatorTax(curve.feeBps(), protocolFeeShareBps) from these values should give curve.creatorTaxBps(). The pool phase uses the frozen hookFeeBps.

§7. The feeder's balances

# State: virus, host, developer, treasury, phase, isPonsRecipient, escrowed, undistributed,
#   devOwed, devClaimed, totalFed, totalBurned, totalTreasury
cast call <FEEDER> \
  "state()((address,address,address,address,uint8,bool,uint256,uint256,uint256,uint256,uint256,uint256,uint256))"

cast call <ESCROW> "balanceOfToken(address,address)(uint256)" <FEEDER> <HOST>   # = state().escrowed
cast call <HOST> "balanceOf(address)(uint256)" <FEEDER>                          # = undistributed + devOwed
cast call <FEEDER> "devOwed()(uint256)"
cast call <FEEDER> "severed()(bool)"
cast call <FEEDER> "lastSweep()(uint8)"      # 0 None, 1 Swept, 2 CurveSweepFailed, 3 AwaitingPonsSweep, 4 NotRecipient

Invariants you can check at any time:

  • host.balanceOf(feeder) ≥ devOwed. The developer's balance is always backed.
  • totalFed == totalBurned + totalDevBooked + totalTreasury.
  • The registry's getVirus(<VIRUS>).totalBurned equals the feeder's totalBurned.

§8. Fees waiting on Pons

Before graduation (phase 0):

cast call <CURVE> "quoteFeeBalance()(uint256)"
cast call <CURVE> "creatorTaxBalance()(uint256)"

After graduation (phase 2):

cast call <FEEDER> "poolId()(bytes32)"
cast call <HOOK> "pendingFees(bytes32,address)(uint256)" <POOL_ID> <VIRUS>        # virus-denominated: needs the Pons operator
cast call <HOOK> "pendingCreatorTax(bytes32,address)(uint256)" <POOL_ID> <VIRUS>
cast call <HOOK> "pendingFees(bytes32,address)(uint256)" <POOL_ID> <HOST>
cast call <HOOK> "pendingCreatorTax(bytes32,address)(uint256)" <POOL_ID> <HOST>
cast call <HOOK> "feeSweepOperator()(address)"

If either virus-denominated figure is non-zero, the creator sweep is refused and the feeder reports AwaitingPonsSweep (3).

Recompute the pool id independently, with currency0 < currency1 sorted by address and poolFee and tickSpacing from the launch record:

cast keccak $(cast abi-encode "f(address,address,uint24,int24,address)" <CURRENCY0> <CURRENCY1> <POOL_FEE> <TICK_SPACING> <HOOK>)

§9. A feed burned what it says it burned

Take a transaction that emitted VirusFed.

cast receipt <FEED_TX>

In its logs you should find, from the host token, a Transfer from <FEEDER> to the zero address for the burned amount and a Transfer from <FEEDER> to the feeder's treasury; from the registry, VirusFed, HostBurned, DevBooked and TreasuryPaid; and from the feeder, its own VirusFed with the sweep outcome.

Check the split from the VirusFed fields:

burned         == floor(grossReceived × 5,000 / 10,000)
devBooked      == floor(grossReceived × 3,000 / 10,000)
treasuryAmount == grossReceived − burned − devBooked

Check the supply delta across the transaction's block:

BLOCK=$(cast receipt <FEED_TX> blockNumber)
cast call <HOST> "totalSupply()(uint256)" --block $((BLOCK - 1))
cast call <HOST> "totalSupply()(uint256)" --block $BLOCK

The difference equals burned when no other burn of that host happened in the same block; hostTotalSupplyAfter in the event is the supply right after this burn. Within the transaction itself, the feeder already asserted the exact delta: a mismatch would have reverted with BurnNotReflected.

Topic hashes, for filtering logs by hand:

cast sig-event "HostBurned(address,address,uint256,uint256)"
cast sig-event "VirusFed(address,address,uint256,uint256,uint256,uint256,uint256)"          # registry
cast sig-event "VirusFed(address,address,uint256,uint256,uint256,uint256,uint256,uint8)"    # feeder

§10. A host's total burn

Sum every HostBurned for a host and compare with the registry's running total:

cast logs --address <REGISTRY> --from-block <VIRUS_DEPLOY_BLOCK> --to-block latest \
  "HostBurned(address indexed host, address indexed virus, uint256 amount, uint256 hostTotalSupplyAfter)" \
  <HOST>

# Host: registered, disabled, depth, registeredAt, children, ponsCurve, ponsPairToken,
#   totalControlledFees, totalBurned, totalDevBooked, totalTreasury, totalInoculationFees
cast call <REGISTRY> \
  "getHost(address)((bool,bool,uint8,uint64,uint32,address,address,uint256,uint256,uint256,uint256,uint256))" \
  <HOST>

totalBurned (9th field) should equal the sum of amount over those logs. The host register's ranking uses this figure.

§11. The first buy and its fee

From the launch transaction's FirstInfection(virus, developer, firstBuy, inoculationFee, virusOut, hostRefund):

inoculationFee == ceil(firstBuy × 250 / 10,000)

firstBuy in the event is the host actually spent. The same values are in getVirus(<VIRUS>) (firstBuy, inoculationFee) and in the registry's InoculationRecorded event. The fee should appear as a host Transfer from the coordinator to the treasury recorded for this virus.

§12. The developer's claims

cast call <FEEDER> "developer()(address)"
cast call <FEEDER> "devClaimed()(uint256)"
cast logs --address <REGISTRY> --from-block <VIRUS_DEPLOY_BLOCK> --to-block latest \
  "DevClaimed(address indexed virus, address indexed developer, uint256 amount)" <VIRUS>

Every DevClaimed pays the address in the feeder's config; there is no other recipient.

§13. Graduation state

cast call <CURVE> "readyToGraduate()(bool)"
cast call <CURVE> "realQuoteReserve()(uint256)"
cast call <CURVE> "graduationThreshold()(uint256)"
cast call <CURVE> "reservedTokens()(uint256)"
cast call <CURVE> "sellableTokens()(uint256)"
cast call <CURVE> "getReserves()(uint256,uint256)"

Progress is realQuoteReserve / graduationThreshold, in host units. The phase is the 11th field of getLaunchedToken(<VIRUS>): 0 NotGraduated, 1 Swept, 2 PoolCreated, 3 Rescued.

§14. Whether Pons is redirecting a virus's fees

cast call <FACTORY> "pendingCreatorFeeRecipient(address)(address,uint256,uint256)" <VIRUS>

A non-zero address is a Pons owner override in progress, with the times it becomes executable and expires. Once executed, getLaunchedToken(<VIRUS>).creatorFeeRecipient no longer equals <FEEDER>, and the next feed() marks the feeder severed.

§15. The treasury's holdings

cast call <HOST> "balanceOf(address)(uint256)" <TREASURY>
cast logs --address <TREASURY> --from-block <VIRUS_DEPLOY_BLOCK> --to-block latest \
  "Withdrawn(address indexed token, address indexed to, uint256 amount)"

Treasury inflows are the TreasuryPaid events and inoculation-fee transfers; outflows are Withdrawn and EthWithdrawn.