Skip to content

[Pre-Mainnet] Contract integration audit checklist #320

@realproject7

Description

@realproject7

Context

PlotLink is preparing to deploy on Base mainnet. Before switching from Base Sepolia, we need a thorough review of all contract integration code — ABIs, constants, indexers, trading widgets, SDK, and CLI.

There are no Solidity source files in this repo. The three contracts are already deployed externally:

Contract Role Sepolia Address Mainnet Address
StoryFactory Storyline creation, plot chaining, donations 0xfa5489b6710Ba2f8406b37fA8f8c3018e51FA229 TBD (0x000...000)
MCV2_Bond (Mint Club V2) Bonding curve trading, token creation, royalties 0x5dfA75b0185efBaEF286E80B847ce84ff8a62C2d 0xc5a076cad94176c2996B32d8466Be1cE757FAa27
ERC-8004 Registry AI agent identity NFTs 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432 Same address (?)

The integration layer is spread across:

  • Web app ABIs: lib/contracts/abi.ts, lib/contracts/erc8004.ts, lib/price.ts (extended MCV2 ABI)
  • SDK ABIs (separate copy): packages/sdk/src/abi.ts
  • Constants: lib/contracts/constants.ts (web), packages/sdk/src/constants.ts (SDK)
  • Indexer routes: src/app/api/index/{storyline,plot,donation}/route.ts
  • Backfill cron: src/app/api/cron/backfill/route.ts
  • Frontend write flows: src/hooks/usePublish.ts, src/hooks/useChainPlot.ts
  • Trading/Donate widgets: src/components/TradingWidget.tsx, src/components/DonateWidget.tsx
  • SDK client: packages/sdk/src/client.ts
  • CLI commands: packages/cli/src/commands/{create,chain,claim,status,agent-register}.ts
  • IPFS upload: packages/sdk/src/ipfs.ts
  • Price/TVL reads: lib/price.ts
  • Content hashing: lib/content.ts
  • Reconciliation: lib/reconcile.ts
  • RPC client: lib/rpc.ts

Audit Checklist

Review each item. Post your findings as comments with the checklist item number and PASS/FAIL/CONCERN.


A. ABI Consistency and Correctness

  • A1. ABI duplication drift risk — ABIs are maintained in TWO separate copies: lib/contracts/abi.ts (web app) and packages/sdk/src/abi.ts (SDK). Additionally, lib/price.ts defines its own extended mcv2BondAbi with extra functions (mint, burn, getReserveForToken, etc.). Verify all three are consistent where they overlap. Flag any divergence.

  • A2. getRoyaltyInfo ABI mismatch — The web app (lib/price.ts:62-72) defines getRoyaltyInfo with inputs: [wallet, reserveToken] and outputs: [balance, claimed] (two outputs). The SDK (packages/sdk/src/abi.ts:131-142) defines it with inputs: [token, beneficiary] and outputs: [unclaimed] (one output). These are structurally different. Verify which matches the actual MCV2_Bond contract and fix the wrong one. The SDK client (packages/sdk/src/client.ts:503-510) casts the result as a single bigint — is this correct?

  • A3. claimRoyalties ABI mismatch — Web app (lib/price.ts:76-79) has inputs: [{ name: "reserveToken" }]. SDK (packages/sdk/src/abi.ts:143-148) has inputs: [{ name: "token" }]. The parameter name differs — more importantly, the SDK client calls claimRoyalties(tokenAddress) passing the storyline token address, but the web ABI suggests it takes the reserve token. Verify against the actual Mint Club V2 contract which address it expects.

  • A4. Event signatures match deployed contract — Verify that all event ABIs (StorylineCreated, PlotChained, Donation, Registered) match the actual deployed contract events exactly (parameter names, types, indexed flags). A mismatch would cause event decoding to silently fail.

  • A5. Function signatures match deployed contract — Verify createStoryline, chainPlot, donate, priceForNextMint, tokenBond, register, setAgentWallet, mint, burn, getReserveForToken, getRefundForTokens all match the deployed contracts.


B. Hardcoded Values and Magic Numbers

  • B1. IPFS gateway URL duplicated"https://ipfs.filebase.io/ipfs/" is hardcoded in 3 files: src/app/api/index/storyline/route.ts:15, src/app/api/index/plot/route.ts:14, src/app/api/cron/backfill/route.ts:13. Should be a shared constant or env var.

  • B2. IPFS timeout duplicatedIPFS_TIMEOUT_MS = 10_000 is independently defined in 3 files (same files as B1). Should be shared.

  • B3. Gas limits hardcoded in multiple placesgas: BigInt(500_000) in useChainPlot.ts:37, gas: BigInt(2_000_000) in TradingWidget.tsx:111-112,146-147, gas: BigInt(150_000) in DonateWidget.tsx:87. Are these appropriate for mainnet? Gas costs differ between testnet and mainnet. Consider whether these need tuning or should be configurable.

  • B4. Slippage tolerance hardcodedSLIPPAGE_BPS = 300 (3%) in TradingWidget.tsx:14. Should this be user-configurable or at least a shared constant? 3% may be too tight for low-liquidity early tokens or too loose for established ones.

  • B5. Block time assumptionBLOCKS_PER_24H = BigInt(43200) in lib/price.ts:168 assumes exactly 2-second block times on Base. Base block times can vary. If used for price change calculations, slight inaccuracy is acceptable — but document the assumption.

  • B6. Backfill scan rangeSCAN_BLOCKS = BigInt(200) in backfill cron. At 2s/block this covers ~6.6 minutes per 5-minute cron run. Verify this keeps up with block production and doesn't fall behind.

  • B7. DEPLOYMENT_BLOCK is testnet-specificpackages/sdk/src/constants.ts:23: DEPLOYMENT_BLOCK = BigInt(20_000_000) is a Base Sepolia block number. This is used as fromBlock for event log queries. On mainnet, this would need to be the actual deployment block. Currently the SDK has no mainnet deployment block.

  • B8. Indexer sleepawait new Promise((r) => setTimeout(r, 5000)) in usePublish.ts:115 and DonateWidget.tsx:95. A fixed 5-second delay before calling the indexer. Is this sufficient on mainnet? Too long? Should it be adaptive or use polling instead?

  • B9. EIP-712 signature deadlinepackages/sdk/src/client.ts:445: deadline is Date.now()/1000 + 3600 (1 hour). Is this appropriate? Too short for slow signers, too long for security?

  • B10. Token decimals hardcoded to 18parseUnits(amount, 18) and formatUnits(*, 18) used throughout TradingWidget.tsx, DonateWidget.tsx, lib/price.ts. This assumes both the reserve token and storyline tokens are 18 decimals. If the mainnet reserve token ($PLOT) has different decimals, everything breaks. getTokenTVL() in lib/price.ts:245-249 correctly reads decimals on-chain — but the trading/donate widgets don't.

  • B11. Filebase endpoint hardcodedpackages/sdk/src/ipfs.ts:21: endpoint: "https://s3.filebase.com" and region: "us-east-1". These are fine if Filebase is the permanent IPFS provider, but should be noted.

  • B12. Retry constants scatteredmaxRetries = 3 in IPFS upload, maxAttempts = 5 in receipt retry, attempt * 2000 backoff in receipt retry, Math.pow(2, attempt-1) * 1000 in IPFS retry. Different retry strategies in different places.


C. Mainnet Readiness and Zero Addresses

  • C1. StoryFactory mainnet address is 0x000...000lib/contracts/constants.ts:31: mainnet fallback is the zero address. This MUST be updated before mainnet deployment. The backfill cron correctly skips when the address is zero (line 64), but the trading widgets and indexers would call the zero address.

  • C2. PLOT_TOKEN mainnet address is 0x000...000lib/contracts/constants.ts:41: mainnet $PLOT token is zero address. Every trade and donation on mainnet would try to transfer from/approve the zero address.

  • C3. ZAP_PLOTLINK permanently zerolib/contracts/constants.ts:34: always 0x000...000. If this contract isn't part of the initial mainnet launch, remove references. If it is planned, flag it.

  • C4. ERC-8004 Registry address is chain-agnosticlib/contracts/constants.ts:78 and packages/sdk/src/constants.ts:42: same address 0x8004A1...9A432 for both testnet and mainnet. Verify this is a cross-chain deployment at the same address, or if the mainnet address is different.

  • C5. SDK defaults are testnet-onlypackages/sdk/src/constants.ts only has Sepolia addresses as defaults. SDK users targeting mainnet must override every address via PlotLinkConfig. Is this documented?

  • C6. Chain ID env var defaultlib/contracts/constants.ts:14 and lib/rpc.ts:4 both default to 84532 (Sepolia). On mainnet deploy, NEXT_PUBLIC_CHAIN_ID=8453 must be set. Verify there's no path where the default leaks through.


D. Security and Access Control

  • D1. Indexer routes have no authenticationPOST /api/index/storyline, /api/index/plot, /api/index/donation are open to anyone. An attacker could submit arbitrary txHash values. The routes do verify on-chain receipt + content hash, so they can't inject fake data — but they can trigger RPC calls, IPFS fetches, and DB writes. Consider rate limiting or requiring an API key.

  • D2. Backfill cron auth is optionalsrc/app/api/cron/backfill/route.ts:24: if (!secret) return true — when CRON_SECRET is not set, anyone can trigger the backfill. Fine for dev, dangerous for production.

  • D3. IPFS content used directly — Indexers fetch arbitrary CIDs from IPFS and store content in Supabase. If the IPFS content is very large (the gateway fetch has a 10s timeout but no size limit), it could cause memory issues or DB bloat. Consider adding a content size limit.

  • D4. Content hash verification is sound — Verify that hashContent() (keccak256(toHex(content))) matches the Solidity-side hash computation exactly. The comment says it matches keccak256(abi.encodePacked(content)) — confirm this is true for UTF-8 strings. This is the primary integrity check.

  • D5. No double-index protection beyond upsert — Indexer idempotency relies on onConflict: "tx_hash,log_index". Verify this unique constraint exists in the Supabase schema for all three tables (storylines, plots, donations).

  • D6. Reconciliation race conditionreconcileStorylinePlotCount() reads plot count then updates storyline. If two plots for the same storyline are indexed concurrently, the count could be stale. The function uses COUNT(*) which should be consistent, but verify there's no TOCTOU issue with Supabase.


E. Logic and Correctness

  • E1. SDK getRoyaltyInfo may return wrong datapackages/sdk/src/client.ts:503-510 calls getRoyaltyInfo(tokenAddress, beneficiary) but the SDK ABI (packages/sdk/src/abi.ts:134-140) takes (token, beneficiary). The web ABI (lib/price.ts:62-72) takes (wallet, reserveToken). If the actual contract signature is getRoyaltyInfo(address wallet, address reserveToken), then the SDK is passing arguments in the wrong order.

  • E2. SDK claimRoyalties argumentpackages/sdk/src/client.ts:521-533 calls claimRoyalties(tokenAddress) where tokenAddress is the storyline token. But the web ABI names the parameter reserveToken. If the contract expects the reserve token address (not the storyline token), claims will fail or claim the wrong royalties.

  • E3. Backfill skips plot titleprocessPlotChained() in backfill/route.ts:261 destructures title from args but doesn't include it in the PlotInsert row. The direct indexer index/plot/route.ts:118 does include title: title || "". Inconsistency — backfilled plots will have no title.

  • E4. BigInt to Number precision — Multiple places convert bigint to Number(): Number(storylineId), Number(plotIndex), Number(toBlock) for Supabase rows and backfill cursor. JavaScript Number is safe up to 2^53 - 1. Storyline IDs and plot indices should be fine, but block numbers on mature chains could eventually overflow (though Base is far from this).

  • E5. approve() exact amount vs max — Trading and donate widgets approve the exact amount needed per transaction. This is safer but means every trade requires a separate approval tx (2 wallet popups). On mainnet with real gas costs, this UX friction is significant. Consider MAX_UINT256 approval or Permit2 integration (Permit2 address is already in constants).

  • E6. Backfill silently skips failed IPFS content — In backfill/route.ts:265-266, if IPFS content can't be fetched or hash doesn't match, the plot is silently skipped with no retry mechanism. These plots will never be backfilled unless the cron is replayed from an earlier cursor.

  • E7. Backfill advances cursor even on partial failurebackfill/route.ts:165: cursor advances to toBlock even if some events within the range failed to process. Those failed events are permanently skipped. Consider only advancing to the last successfully processed block.

  • E8. Genesis plot count raceindex/storyline/route.ts:139: storyline is created with plot_count: 1 (counting genesis). But if the genesis plot insert (step 9) fails while the storyline insert (step 8) succeeds, plot_count would be 1 but no genesis plot exists in the plots table. The reconciler would later correct it to 0. Minor data inconsistency window.


F. Over-Engineering and Duplication

  • F1. lib/viem.ts is a useless re-exportlib/viem.ts contains only export { publicClient } from "./rpc" with a backward compat comment. The backfill route imports from lib/viem, everything else from lib/rpc. Pick one and delete the other.

  • F2. ABI definitions in three places — As noted in A1, ABIs exist in lib/contracts/abi.ts, packages/sdk/src/abi.ts, and lib/price.ts. The web app ones could import from the SDK package instead of maintaining a separate copy. Evaluate if the SDK ABI can be the single source of truth.

  • F3. Duplicate error helper — The error() helper function is independently defined in all three indexer routes with identical code. Should be a shared utility.

  • F4. Duplicate IPFS_GATEWAY + IPFS_TIMEOUT_MS — Same as B1/B2. Three identical definitions across indexer routes and backfill.

  • F5. Inconsistent publicClient import paths — Backfill imports publicClient from lib/viem, indexer routes import from lib/rpc. Same object, different paths. Confusing.

  • F6. Content hashing duplicatedlib/content.ts defines hashContent(). packages/sdk/src/client.ts:598-599 defines its own identical hashContent(). If the hashing algorithm ever changes, both must be updated.

  • F7. Unused constantsZAP_PLOTLINK, UNISWAP_V4_ROUTER, UNISWAP_V4_QUOTER, PERMIT2 are defined in lib/contracts/constants.ts but never imported or used anywhere. They're reserved for future features. If not launching with mainnet, remove to reduce confusion.


G. Mainnet Deployment Switchover

  • G1. Create a mainnet deployment checklist covering at minimum: (1) Deploy StoryFactory on Base mainnet, (2) Deploy or verify $PLOT reserve token, (3) Update STORY_FACTORY mainnet address in constants, (4) Update PLOT_TOKEN mainnet address, (5) Set NEXT_PUBLIC_CHAIN_ID=8453 in production env, (6) Set NEXT_PUBLIC_CONTRACT_ADDRESS in production env, (7) Update DEPLOYMENT_BLOCK for mainnet in SDK, (8) Set CRON_SECRET in production, (9) Verify ERC8004_REGISTRY address on mainnet, (10) Initialize backfill_cursor table with mainnet deployment block.

  • G2. No mainnet integration test path — There's no way to test against mainnet contracts without real funds. Consider a staging environment pointing to mainnet RPCs with a separate Supabase instance.

  • G3. Reserve token label accuracyRESERVE_LABEL switches between "WETH" and "$PLOT" based on IS_TESTNET. Verify $PLOT is the correct mainnet reserve token symbol and it displays correctly everywhere.


How to Review

  1. Read each checklist item and the referenced file(s)
  2. For ABI items (A1-A5), compare against the actual deployed contract on BaseScan
  3. Post findings as a comment with format: **A1: PASS/FAIL/CONCERN** — explanation
  4. Flag any additional issues you discover

Acceptance Criteria

  • All checklist items reviewed and commented on by T2a and T2b
  • All FAIL items have follow-up tickets created
  • All CONCERN items are triaged (accept risk or create ticket)
  • Mainnet deployment blockers clearly identified

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent/T2aAssigned to T2a reviewer agentagent/T2bAssigned to T2b reviewer agentphase/8-launchPhase 8: Launch Prep

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions