# Guide: Writing an App Contract (the Backend in Solidity)

**Goal:** put an app's logic and data in your own contract, so the app runs on-chain end to end. The ChainRender
component then reads the contract with on-chain slots, and its buttons call it through declared actions.

Most apps need no contract of their own. Composing reads of existing contracts is enough: balances, NFTs, prices,
ENS (see **[Resolver Kinds](../reference/resolver-kinds.md)**). Write one when the app has its own **rules or
state**: a fundraiser, a vote, a registry, a game, an escrow.

The worked example is `contracts/examples/GoalEscrow.sol`, an all-or-nothing fundraiser, with its tests in
`test/GoalEscrow.ts`. Every claim below about it is tested there.

## Before you write: does it already exist?

These are deployed or built ChainRender contracts. Check **[Deployed Addresses](../reference/addresses.md)** for
where each one is live.

| You need | Use | Guide |
|---|---|---|
| Two-player games, a waiting room, stakes, on-chain results | `GameLobbyV2` + `GameChannelV2` + your `IGameRulesV2` | **[Multiplayer & Lobbies](./multiplayer-and-lobbies.md)** |
| Show any view function of any contract | `CallResolver` (`staticread`), `MultiCallResolver` (`multiread`), `ComputeResolver` (`compute`) | **[Resolver Kinds](../reference/resolver-kinds.md)** |
| A custom on-chain value or image renderer | your own resolver | **[Writing a Resolver](./writing-a-resolver.md)** |

## The rules

| Rule | Why | In GoalEscrow |
|---|---|---|
| **Ownerless and immutable.** No admin, no `onlyOwner`, no proxy, no pause. A new version is a new deployment, and the old one stays callable. | An app whose backend can be changed or frozen by a key is not trustless, whatever the frontend says. | No owner state at all; one contract serves every campaign. |
| **Parameters per item, not constants.** Goals, deadlines, fees and windows are arguments stored per item. Never bake in chain limits (code size, gas caps, block times). | The same bytecode works on every chain and for every use. | `create(beneficiary, goal, deadline, metaHash)`. |
| **Pull payments.** Each party withdraws what they are owed with their own call, and state changes before the transfer. Never loop over recipients to push funds. | One reverting recipient cannot block everyone else, and reentrancy finds nothing left to take. | `refund` zeroes the caller's balance, then sends it; `payOut` sets `paidOut` first. |
| **Few slot writes.** Pack related fields into one 32-byte slot. Put descriptions in events or `SourceStore`, not storage. | After Glamsterdam (EIP-8038), slot writes are the expensive part of a transaction. Reads are cheap. | Two slots per campaign; the description is a hash emitted in `CampaignCreated`. |
| **One single-value view per field.** For example `raisedOf(id)`, not only `getCampaign(id)` returning a struct. | A `staticread` slot decodes **one** return value. With single-value views, the floor can show any field directly. | `goalOf`, `raisedOf`, `deadlineOf`, `statusOf`, `progressBps`, `beneficiaryOf`, `contributionOf`. |
| **Display-ready numbers.** Return status as a small integer and progress in basis points. Document the encoding in NatSpec. | The resolver formats numbers (`FMT.FIXED` with decimals), but it cannot run your logic. | `statusOf` returns 0 to 3; `progressBps` returns 10000 for 100%. |
| **An event for every state change**, with indexed ids. | Live views and indexers follow events (see **[Live Data & Events](./live-data-and-events.md)**). | `CampaignCreated`, `Contributed`, `PaidOut`, `Refunded`. |
| **Custom errors**, not revert strings. | They are cheaper, and clients and tests can match them by name. | `Closed()`, `GoalNotMet()`, … |
| **NatSpec `@notice` on the contract and on every external function.** | The generated **[Deployed Addresses](../reference/addresses.md)** page uses the contract's `@notice` as "what it is". AIs and reviewers read it too. | — |
| **Say what the code does, nothing more.** | Your page and the docs must not promise what the contract does not enforce. | The NatSpec states the payout rule exactly. |

## Toolchain

Use the settings ChainRender itself is built with, which **[Local Build Environment](./local-build-environment.md)**
lists:

- Hardhat 3, with solc 0.8.36 pinned from npm;
- `evmVersion: "cancun"`;
- the optimizer on at 200 runs with `details: { yul: false }`;
- **never viaIR**;
- `pragma solidity ^0.8.28;`.

Tests use `node:test` with viem through Hardhat (`npx hardhat test nodejs test/<file>.ts`).

## Test it like the example

`test/GoalEscrow.ts` covers four things, and an app contract should have the same four kinds of test:

1. **The happy path, end to end.** Create, contribute, reach the deadline, pay out, and check the balances moved.
2. **Every failure path the contract promises.** Late contributions, an early payout, a double payout, a refund when
   the goal was met, a refund for someone who never contributed, and a double refund.
3. **Bad input.** A zero beneficiary, a zero goal, a deadline in the past, an unknown id.
4. **The reads your app will display.** Deploy `CallResolver` in the test and read each field through it, exactly
   as the floor will. Then `checkNode` the component tree you plan to ship.

To move time in a test, use `const t = await viem.getTestClient(); await t.increaseTime({ seconds }); await
t.mine({ blocks: 1 })`.

## Deploy it

Keys and RPC URLs go through Hardhat configuration variables, which keeps them out of the shell history.

```bash
npx hardhat keystore set SEPOLIA_RPC_URL
npx hardhat keystore set SEPOLIA_PRIVATE_KEY      # prompts; never paste a key on the command line
npx hardhat compile
npx hardhat test nodejs test/GoalEscrow.ts
```

A minimal deploy script (`scripts/deploy-goal-escrow.ts`):

```ts
import { network } from "hardhat";
const { viem } = await network.create();
const c = await viem.deployContract("GoalEscrow", []);
console.log("GoalEscrow", c.address);
```

```bash
npx hardhat run scripts/deploy-goal-escrow.ts --network sepolia
npx hardhat verify --network sepolia <address>   # source verification on the explorer (verify)
```

Every chain in this repository's `hardhat.config.ts` has its own variables, for example
`ROBINHOOD_TESTNET_PRIVATE_KEY` and `ARC_TESTNET_PRIVATE_KEY`. Arc's gas token is USDC, so the deployer needs
testnet USDC there. **On a mainnet, deploy from a hardware wallet or an encrypted keystore**, never from a raw key.

To deploy the same address on several chains, deploy from the same fresh account with the same nonce on each chain,
or use a deterministic deployer (CreateX, as the cross-chain contracts do; see
**[Proven Balances](../concepts/cross-chain-balances.md)**).

## Bind it into the app

```ts
import { compose, reads, checkNode, grade, type Node } from "@chainrender/client";

const ESCROW = "0x…" as `0x${string}`;          // your deployed GoalEscrow
const CALL_RESOLVER = "0x…" as `0x${string}`;  // CallResolver on the same chain (Deployed Addresses)
const read = (signature: string, args: unknown[] = [], fmt = {}) =>
  reads.staticReadSlot(CALL_RESOLVER, { target: ESCROW, callData: reads.callData(signature, args), ...fmt }) as Node;
const eth = { format: reads.FMT.FIXED, decimals: 18, fracDigits: 3 };

const campaign = compose.box([
  compose.heading("Campaign #0"),
  compose.box([compose.text("Raised"), read("raisedOf(uint256)", [0n], eth),
               compose.text("of"), read("goalOf(uint256)", [0n], eth)], { direction: "row" }),
  compose.text("Progress (basis points):"), read("progressBps(uint256)", [0n]),
  compose.button("Contribute 0.01", { variant: "primary",
    action: { to: ESCROW, sig: "contribute(uint256)", value: "10000000000000000", args: "0" } }),
  compose.button("Refund me", { action: { to: ESCROW, sig: "refund(uint256)", args: "0" } }),
], { direction: "column" });
console.log(checkNode(campaign).ok, grade(campaign).letter);
```

Things to know when binding:

- **Slots and contracts must be on the same chain.** `CallResolver` STATICCALLs your contract, so it can only read a
  contract on its own chain.
- **A declared action's `value` is fixed at compile time.** The wallet decode can therefore state the exact amount
  sent, but a floor button sends a fixed amount. Offer fixed amounts ("Contribute 0.01", "0.1"), or let a client
  page build the transaction for an arbitrary amount.
- **User input fills `$name` arguments.** An `input` with `name: "id"` fills `args: "$id"`. The runtime coerces the
  text to the parameter type in `sig`.
- **The grade reflects only what is on-chain.** A tree of `staticread` slots and declared actions against an
  ownerless contract grades A. Anything the client computes or fetches is graded as such.

## Related

- **[Multiplayer & Lobbies](./multiplayer-and-lobbies.md)**: a complete multi-contract backend (lobby, channel,
  rules)
- **[Declare a Safe Action](./declare-action.md)** · **[Bind Data](./bind-data.md)** ·
  **[Writing a Resolver](./writing-a-resolver.md)**
- **[Local Build Environment](./local-build-environment.md)** · **[Deployed Addresses](../reference/addresses.md)**
