Publish a tokenized fund’s Net Asset Value to multiple chains on a schedule, with a Chainlink-compatible on-chain interface and an operator kill-switch
This guide walks you through building a Proof-of-Reserves / NAV oracle for a tokenized RWA fund. A single Compose task runs every 5 minutes, fetches a structured NAV bundle from a custodian endpoint, and publishes it to ReserveAggregator contracts on Base Sepolia and Arbitrum Sepolia in one run. The on-chain contract implements AggregatorV3Interface, so any existing Chainlink consumer can read it unchanged.
Fetch the NAV bundle from a custodian JSON endpoint. If the response carries ripcord: true, the task logs a skip message and returns without publishing.
Scale human USD numbers to 18-decimal fixed point, matching Chainlink’s convention for USD-denominated feeds.
Publish to both chains independently via Promise.allSettled. A failure on one chain does not block the other. The next cron cycle reconciles.
Multi-chain writes from a single task: one wallet.writeContract call per chain, issued in parallel via Promise.allSettled.
sponsorGas: true: the publisher wallet is a Compose-managed Privy wallet whose transactions are gas-sponsored by Goldsky. No ETH needed on the publisher ever.
context.fetch: retries the custodian call with exponential backoff before surfacing a failure.
Ripcord pattern: the data source itself carries a boolean kill-switch the operator can flip out-of-band. Useful when the upstream has a known issue and you want the oracle to pause without a redeploy.
ReserveAggregator.sol is a single-operator, AggregatorV3Interface-compatible publisher. The full bundle is stored in one struct; the scalar totalNav is exposed via the standard Chainlink reader so existing consumers work unchanged:
Compose’s wallet-create command provisions the named wallet in the cloud and prints its address, so you can pass that address as the publisher constructor argument before you deploy the contracts:
forge create contracts/ReserveAggregator.sol:ReserveAggregator \ --rpc-url https://sepolia.base.org \ --private-key $PRIVATE_KEY \ --broadcast --root . \ --constructor-args 0xYOUR_PUBLISHER_ADDRESS "Example RWA Fund I NAV / USD"
And Arbitrum Sepolia:
forge create contracts/ReserveAggregator.sol:ReserveAggregator \ --rpc-url https://sepolia-rollup.arbitrum.io/rpc \ --private-key $PRIVATE_KEY \ --broadcast --root . \ --constructor-args 0xYOUR_PUBLISHER_ADDRESS "Example RWA Fund I NAV / USD"
Record both deployed addresses.
--broadcast must come before--constructor-args: forge treats --constructor-args as variadic, so any flag that follows it gets consumed as another positional argument and the transaction is never sent.
Any host can flip the kill-switch by returning "ripcord": true from the custodian endpoint. The next task run logs Ripcord engaged … and skips the publish without erroring, so Compose’s retry logic is not triggered. Flip it back to false and publishing resumes on the following cycle.
If you need to re-create the Compose wallet or move to a different key, call setPublisher(newAddress) from the current publisher. The old wallet loses updateNav permissions; only the new one can publish.