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.solscripts/e2e-anvil.shscripts/showcase.sh
- The deployer signs with a Foundry keystore or Ledger. No deployment private key is read from a repository config.
CORNER_STORE_TESTNET_ISSUER_KEYsigns 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.
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.
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.shAfter 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.
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-v1Element 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.
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 --interactiveParticipant wallets can use their existing wallet software. They do not need to expose keys to the deployment script.
From the repository root:
cp .env.testnet.example .env.testnetFill the blank values locally, then load them without committing the file:
set -a
source .env.testnet
set +aGenerate 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.
Run the wrapper without --broadcast first:
scripts/deploy-testnet-rfq.sh \
--rpc-url "$RPC_URL" \
--chain-id "$CHAIN_ID" \
--account corner-store-testnetThis executes a Foundry simulation. Review the contract list, sender, gas estimate and target chain before continuing.
scripts/deploy-testnet-rfq.sh \
--rpc-url "$RPC_URL" \
--chain-id "$CHAIN_ID" \
--account corner-store-testnet \
--broadcastFor 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 --jsonThe 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> \
--broadcastThe script grants the exact deployed RFQ adapter allowance for both the RWA and quote token. It does not transfer inventory or change compliance claims.
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.
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.
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 \
--verifyBlockscout-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.
Publish:
- network name and chain ID;
- deployment transaction hashes;
- verified contract links;
rwaToken,router,engine,rfqAdapterandmakerAuthorizeraddresses;- 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.
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 onlyThen run:
scripts/run-testnet-rfq-demo.shOpen 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/healthTerminate 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.jsonBoth 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.shremains the deterministic presenter/security demo;scripts/run-testnet-rfq-demo.shproves 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.