Skip to content

Latest commit

 

History

History
513 lines (410 loc) · 20.5 KB

testnet-deployment.md

File metadata and controls

513 lines (410 loc) · 20.5 KB

Public Testnet RFQ Deployment

Purpose

This runbook deploys a working, RFQ-only Corner Store reference stack to a public EVM testnet for hackathon review. It creates:

  • a real T-REX ERC-3643 token and ONCHAINID registry fixture;
  • mock Trusted Issuer KYC claims for externally supplied test wallets;
  • the Corner Store Element, Recipe, Manifest, Engine and Router contracts;
  • the RFQ adapter, maker authorizer and RFQ venue;
  • an unregulated test quote token and initial two-way inventory;
  • an active BUIDL-like reference Manifest with one approved maker.

The deployment proves the protocol on a public chain. It is not a production issuer onboarding, legal approval, real BUIDL deployment or Securitize integration.

The existing local Anvil scripts remain unchanged:

  • script/DeployStack.s.sol
  • scripts/e2e-anvil.sh
  • scripts/showcase.sh

Security Boundaries

  • The deployer signs with a Foundry keystore or Ledger. No deployment private key is read from a repository config.
  • CORNER_STORE_TESTNET_ISSUER_KEY signs mock ONCHAINID claim payloads during deployment. It is a disposable test fixture secret, not a production TA key.
  • Maker and investor addresses are external wallets. The deployment script mints test inventory to them but cannot approve tokens on their behalf.
  • Every participant must separately approve the deployed RFQ adapter.
  • Browser-triggered public-network deployment remains disabled.
  • Mainnet and legally approved ERC-3643 onboarding continue to use deployment-production.md.

1. Choose the Testnet

Obtain the official values from the hackathon or network operator:

  • RPC URL;
  • chain ID;
  • explorer and source-verification method;
  • faucet;
  • per-transaction and block gas limits.

Do not infer these values from the network name. The deployment wrapper checks that the RPC-reported chain ID exactly matches --chain-id.

Monad Onchain Finance hackathon profile

The current official Monad developer resources list the following public testnet values. Re-check them before broadcast because public endpoints can change:

Input Value
network Monad Testnet
chain ID 10143
public RPC https://testnet-rpc.monad.xyz
explorer https://testnet.monadexplorer.com
faucet https://faucet.monad.xyz

The verified monad-developers organization links the canonical docs, testnet hub, faucet and explorer. Before loading any signer, run the read-only preflight:

scripts/preflight-monad-hackathon.sh

After loading the local deployment environment, add --deployment-ready to validate required inputs, distinct actor addresses and a non-zero deployer gas balance without printing any secret value.

Use a unique append-only deployment id for the hackathon record:

export CORNER_STORE_DEPLOYMENT_ID=monad-onchain-finance-rfq-v1
export CORNER_STORE_TESTNET_INVESTOR_B_HOLDING_PERIOD_PENDING=true
export CORNER_STORE_TESTNET_TOKEN_NAME="Demo Restricted Security"
export CORNER_STORE_TESTNET_TOKEN_SYMBOL=DRS
# Optional fourth scene:
export CORNER_STORE_TESTNET_EXPIRED_INVESTOR=0x...

Do not put a faucet wallet key, Maker key or credential-bearing fallback RPC in the deployment artifact or repository config.

The optional investor-B flag changes only that wallet's acquisition clock. It creates a deterministic Sell rejection with an onchain-derived availability time while leaving the default public-testnet fixture behavior unchanged.

Judge-facing terminology contract

The public demo uses the following terms consistently. These names preserve the product's technical vocabulary while keeping the reference and legal boundary explicit:

  • Demo Restricted Security (DRS) is the fictional public asset name. Do not use a real product name or imply issuer affiliation.
  • compliance pre-check names the Corner Store technical evaluation step. A passing result means the configured policy passed; it is not a legal or regulatory determination and does not guarantee settlement.
  • Qualified Purchaser (QP) is retained when the A-13-v1 Element is the actual failed rule. Call the fixture a demo QP claim; do not replace the rule name with a generic qualification that hides what was evaluated.
  • venue is the canonical execution-layer term. In explanatory prose use reference RFQ venue so it is not presented as a licensed exchange or ATS.
  • test participant and test operator distinguish judge-controlled wallet actions from fixture maintenance. Do not call either party a customer, investor client, broker or custodian.
  • technical evidence, execution trace and settlement evidence describe what the demo exposes. Do not call these outputs a compliance certificate, legal record or audit approval.

When outside copy advice conflicts with this contract, this repository contract and the accepted policy terminology take precedence.

When CORNER_STORE_TESTNET_EXPIRED_INVESTOR is present, the fixture registers that separate wallet with valid mock identity wiring and a QP claim whose verifiedAt is 366 days before deployment. The default one-year freshness cap therefore produces FAIL_QP_CLAIM_EXPIRED without changing the cap for other wallets. The wallet must be unique; omitting it preserves older deployment inputs and disables only the fourth judge scene.

2. Prepare Wallets

Prepare five required externally controlled wallets and one optional scenario wallet:

Wallet Purpose Needs native test gas
deployer deploys and initializes the reference stack yes, substantial
maker signs quotes, supplies RWA/quote inventory yes
eligible investor A normal success scenario yes
eligible investor B holding-period scenario when the optional flag is enabled yes
ineligible investor rejection scenario yes
expired-claim investor optional claim-expiry pre-check no transaction required

Governance and operator may equal the deployer for a short-lived hackathon fixture. They should be separate controlled addresses for longer-lived environments. Control-plane addresses must not be reused as maker or investor addresses.

Import only the deployer into a Foundry keystore:

cast wallet import corner-store-testnet --interactive

Participant wallets can use their existing wallet software. They do not need to expose keys to the deployment script.

3. Configure Runtime Inputs

From the repository root:

cp .env.testnet.example .env.testnet

Fill the blank values locally, then load them without committing the file:

set -a
source .env.testnet
set +a

Generate a disposable mock issuer key with an approved secret-generation tool and place it only in CORNER_STORE_TESTNET_ISSUER_KEY. The issuer wallet does not need native gas because it signs claim payloads off-chain during the deployment script.

4. Build and Simulate

Run the wrapper without --broadcast first:

scripts/deploy-testnet-rfq.sh \
  --rpc-url "$RPC_URL" \
  --chain-id "$CHAIN_ID" \
  --account corner-store-testnet

This executes a Foundry simulation. Review the contract list, sender, gas estimate and target chain before continuing.

5. Broadcast

scripts/deploy-testnet-rfq.sh \
  --rpc-url "$RPC_URL" \
  --chain-id "$CHAIN_ID" \
  --account corner-store-testnet \
  --broadcast

For a Ledger, replace --account corner-store-testnet with --ledger. CI or an isolated operator machine may instead use --keystore /secure/path/key.json --password-file /secure/path/password; keep both files outside the repository.

The script writes an append-only, commit-safe public deployment artifact:

deployments/public/<deployment-id>-<chain-id>.json

The artifact records chain ID, actors, every named protocol/identity address, the exact checked-out source commit, the full Foundry deployment transaction list, and a deployedContracts index. It never contains private keys or RPC credentials. Broadcast refuses a dirty worktree or a source-commit override that differs from HEAD. The wrapper enriches a private candidate, verifies that complete candidate against the chain, and then publishes it atomically to deployments/public/; failed or partial broadcasts cannot become published deployment records.

The wrapper uses Foundry's sequential broadcast mode because this fixture emits many dependent initialization and governance-handoff transactions. This is slower than batch submission but avoids public-RPC nonce propagation races.

Do not reuse a deployment ID on the same chain. The wrapper refuses to overwrite an existing public record. List every recorded deployment and its main addresses at any time:

scripts/list-testnet-deployments.sh
scripts/list-testnet-deployments.sh --json

6. Approve Participant Inventory

The maker and every investor that will trade must run the approval script with their own signer. Example for a Foundry keystore:

CORNER_STORE_ARTIFACT=deployments/public/<deployment-id>-<chain-id>.json \
forge script script/ApproveTestnetRFQ.s.sol:ApproveTestnetRFQ \
  --rpc-url "$RPC_URL" \
  --chain-id "$CHAIN_ID" \
  --account <participant-keystore> \
  --broadcast

The script grants the exact deployed RFQ adapter allowance for both the RWA and quote token. It does not transfer inventory or change compliance claims.

7. Verify On-chain Readiness

Before approvals, verify deployment and activation:

CORNER_STORE_ARTIFACT=deployments/public/<deployment-id>-<chain-id>.json \
forge script script/VerifyTestnetRFQ.s.sol:VerifyTestnetRFQ \
  --rpc-url "$RPC_URL" \
  --chain-id "$CHAIN_ID"

After the maker and primary investor approve:

CORNER_STORE_ARTIFACT=deployments/public/<deployment-id>-<chain-id>.json \
CORNER_STORE_REQUIRE_APPROVALS=true \
forge script script/VerifyTestnetRFQ.s.sol:VerifyTestnetRFQ \
  --rpc-url "$RPC_URL" \
  --chain-id "$CHAIN_ID"

The verifier fails closed on:

  • chain/artifact mismatch;
  • missing runtime code;
  • governance ownership mismatch;
  • missing operator authorization;
  • Engine, policy registry, Router, selector, venue registry or RFQ adapter wiring that differs from the artifact;
  • token/Identity Registry/storage/trusted-issuer/topic wiring, wallet identity binding or ERC-3643 verification that differs from the artifact;
  • inactive Manifest or RFQ venue;
  • RFQ venue type or adapter mismatch;
  • unapproved maker;
  • missing maker/investor inventory;
  • an optional expired-claim wallet whose identity, inventory or exact QP expiry result does not match the published fixture;
  • missing allowances when requested.

8. Refresh Existing Fixture Evidence

An operator may refresh only QP and acquisition evidence for a wallet already named in the deployment artifact. The command is a dry-run by default:

scripts/refresh-testnet-evidence.sh \
  --rpc-url "$RPC_URL" \
  --artifact deployments/public/<deployment-id>-<chain-id>.json \
  --subject <existing-fixture-wallet> \
  --qp qualified \
  --acquisition-status valid \
  --clock-start <unix-seconds> \
  --expires-at <future-unix-seconds> \
  --source-ref 0x<64-hex-characters>

Use --qp clear to remove the existing QP claim. Acquisition status is either valid, with a nonzero clock start that is not in the future, or lineage-broken, with a zero clock start. Every acquisition refresh requires an explicit future expiry and nonzero PII-free bytes32 source reference.

After reviewing the simulation, add --broadcast and exactly one operator signer option: --account, --keystore with an optional --password-file, or --ledger. The wrapper verifies that signer address against the artifact operator before mutation. It then broadcasts sequentially and runs a separate read-only pass that verifies the exact selected QP claim shape and acquisition snapshot fields. The deployment artifact is never modified.

This tool does not register identities or wallets, mint or transfer inventory, grant approvals, change policy, or infer qualification from legal or personal data. It does not accept raw private keys.

9. Explorer Source Verification

If the selected explorer is supported by Foundry, set its API key in the environment and add --verify to the broadcast command:

export ETHERSCAN_API_KEY=...

scripts/deploy-testnet-rfq.sh \
  --rpc-url "$RPC_URL" \
  --chain-id "$CHAIN_ID" \
  --account corner-store-testnet \
  --broadcast \
  --verify

Blockscout-based explorers require the network's official verifier endpoint:

export BLOCKSCOUT_API_URL=<blockscout-api-url>

scripts/deploy-testnet-rfq.sh \
  --rpc-url "$RPC_URL" \
  --chain-id "$CHAIN_ID" \
  --account corner-store-testnet \
  --broadcast \
  --verify \
  --verifier blockscout \
  --verifier-url "$BLOCKSCOUT_API_URL"

Other custom explorers may require a different verifier URL or Sourcify. Use the network's official instructions; never commit explorer credentials.

Deployment Outputs for a Hackathon Submission

Publish:

  • network name and chain ID;
  • deployment transaction hashes;
  • verified contract links;
  • rwaToken, router, engine, rfqAdapter and makerAuthorizer addresses;
  • the non-secret deployment artifact;
  • one successful Router RFQ transaction;
  • expected rejection transactions or simulation evidence;
  • an explicit statement that the TA, claims, quote asset and inventory are public-testnet fixtures.

The public-testnet demo runtime that consumes this artifact is maintained as a separate integration surface. The local Anvil demo does not silently switch to public networks.

10. Run the Artifact-bound Browser Demo

After the public artifact is committed and participant allowances are ready, load the same .env.testnet and set:

export CORNER_STORE_TESTNET_ARTIFACT=deployments/public/<deployment-id>-<chain-id>.json
export CORNER_STORE_TESTNET_RPC_URL=https://...
export CORNER_STORE_TESTNET_EXPLORER_URL=https://...
export CORNER_STORE_TESTNET_MAKER_KEY=... # disposable testnet Maker only

Then run:

scripts/run-testnet-rfq-demo.sh

Open the printed URL and connect an artifact-listed investor wallet. Before it listens, the runtime checks the artifact schema and chain plus runtime code, governance owners, operator roles, core contract wiring, RFQ venue activation, maker approval and fixture inventory. The backend signs a Maker quote using the existing RFQ SDK; it does not sign for the investor. The browser wallet submits its own token approval and final Router transaction. The page exposes the exact artifact addresses, current Manifest/Maker readiness, balances, QP pre-check, signed quote, transaction hash and block. It also renders the non-secret judge scenario addresses and measures wallet transaction confirmation time from submission to receipt. Test-wallet secrets must be delivered out of band and are never rendered by the service. For chain ID 10143 the page shows the official Monad testnet wallet settings; the backend's configured RPC URL remains private and is never returned. The quick-start action can add or switch the browser wallet using only those public network values. Changing the selected account or chain clears the previous pre-check, signed quote and signer state before another scenario can run. For another public testnet, wallet setup remains disabled unless the operator sets all four public values below. They are validated independently from the private runtime RPC and are bound to the artifact chain ID:

export CORNER_STORE_TESTNET_WALLET_NETWORK_NAME="<public network name>"
export CORNER_STORE_TESTNET_WALLET_RPC_URL="https://<credential-free-public-rpc>"
export CORNER_STORE_TESTNET_WALLET_NATIVE_CURRENCY="<symbol>"
export CORNER_STORE_TESTNET_WALLET_EXPLORER_URL="https://<public-explorer>"

Partial metadata, credential-bearing URLs and unsupported URL schemes fail closed. The runtime never derives public wallet settings from CORNER_STORE_TESTNET_RPC_URL, which may contain operator credentials. After a successful Router settlement, download the generated JSON evidence. It binds the deployment/source commit, chain, public wallet and contract addresses, amounts, transaction hash, block and measured submission-to-confirmation time. Review the file before attaching it to the hackathon submission; it contains no private key, RPC URL or quote signature. When a pre-check is rejected, the page also presents a plain-language reason, the required user/operator action and an unlock or evidence-expiry time only when those values can be derived from current chain evidence. Holding-period countdowns advance from the latest observed chain timestamp, not the judge browser's local clock.

For an external judge URL, keep this Node service on a private application port behind an HTTPS reverse proxy. The public edge must enforce a bounded request rate and body size; the disposable Maker key remains available only to the server process. Do not expose the Maker environment, artifact filesystem or RPC credentials as static assets.

GET /health verifies that the process can still read the configured chain and returns the latest observed block number; it does not expose local paths or credentials.

The repository includes a portable non-root/read-only OCI target and Compose contract for this boundary. The container consumes the verified artifact and a Docker secret at runtime; neither is copied into the image:

export CORNER_STORE_TESTNET_ARTIFACT="$PWD/deployments/public/<deployment-id>-10143.json"
export CORNER_STORE_TESTNET_RPC_URL=https://testnet-rpc.monad.xyz
export CORNER_STORE_TESTNET_EXPLORER_URL=https://testnet.monadexplorer.com
export CORNER_STORE_TESTNET_MAKER_KEY_FILE=/secure/path/disposable-maker-key

docker compose -f deploy/hackathon-monad/compose.example.yaml up --build -d
curl --fail http://127.0.0.1:8791/health

Terminate HTTPS at the selected hosting platform or reverse proxy and forward only to port 8791. The application enforces a 16 KiB request limit, bounded request/header/keep-alive timeouts, a global budget for every chain-reading /health or /api/* request, a stricter global signed-quote budget, browser security headers and an explicit hackathon-testnet-only acknowledgement for non-loopback binding. Because the application deliberately does not trust forwarded client-IP headers, the HTTPS edge must add its own client-aware rate limit. Outbound JSON-RPC calls are paced by CORNER_STORE_TESTNET_RPC_REQUESTS_PER_SECOND (default 10) to stay under the public Monad RPC limit of 15 requests per second; a request that would wait more than 10 seconds for that budget returns 503 service_unavailable. Allowlisted input-validation failures return 400 invalid_request; chain/runtime dependency failures return a sanitized 503 service_unavailable, and unclassified internal failures return 500 internal_error. Server failures emit a secret-free operator event with only the HTTP method, normalized API surface, status and observation time. These are demo safeguards, not a replacement for the production RFQ host.

Before sharing or recording the judge URL, run the repository rehearsal gate:

node scripts/rehearse-monad-judge-url.mjs --url https://<judge-host>

The Monad command is a strict compatibility wrapper fixed to chain 10143 and the exact Monad Testnet wallet metadata. For another hosted public-testnet surface, provide the expected artifact chain and network name explicitly:

node scripts/rehearse-public-testnet-url.mjs \
  --url https://<hosted-url> \
  --chain-id <chain-id> \
  --network-name "<public network name>" \
  --evidence /path/to/downloaded-evidence.json

Both commands fail closed when the public endpoint is not HTTPS, redirects off-origin, does not serve the expected security headers, does not match the expected chain and network metadata, lacks active Maker/Manifest/inventory readiness, or omits one of the required scenes. The Monad wrapper additionally requires chain 10143 and the exact published Monad Testnet wallet settings. The claim-expiry scene is reported separately because it is the first scenario to cut if the submission schedule slips. After one successful settlement, rerun with --evidence <downloaded-json> to bind the measured transaction record to the same deployment, contracts, fixture participant, token pair and current chain height. The verifier reads public responses only and never accepts or prints wallet credentials.

This is separate from the feature-rich local Anvil demo:

  • scripts/showcase.sh remains the deterministic presenter/security demo;
  • scripts/run-testnet-rfq-demo.sh proves that a committed public deployment can be consumed without deploying a second stack;
  • no local account derivation, operator mutation or private investor key is available to the public-testnet service.