Actions & Signing
An action is how a ChainRender UI lets a user sign a transaction. Actions are declared, not arbitrary — which is what makes it impossible for an app to hide a malicious transaction behind an innocent button.
Declared actions
A button carries an action declaration: a target address, a function signature, and an argument shape. It does not carry raw calldata. At sign time, the runtime constructs the calldata from the declaration:
// the button declares:
{ to: "0x…", sig: "claim(uint256)", args: "$amount" }
// the runtime constructs exactly:
claim(<the user's amount>) // nothing else is expressible
The constructed transaction can only be the declared selector with the declared argument shape. There is no path to smuggle a different call, a different target, or a hidden value transfer. This is called construct-for-sign.
Why not just carry calldata?
Because raw calldata is opaque. A button labeled "Claim" could carry calldata for any call — drain a wallet, call a different contract, move 100× the displayed amount. By declaring the action structurally and constructing the calldata from the declaration, the transaction is provably what the button says it is. The enforcer's rails (R3–R5) guarantee the declaration is honest and in-envelope.
Batches, signing, and value
- Batches — a button can carry an ordered list of actions submitted atomically under one signature; each member is individually gated. One signature, N verified actions, no authority beyond their union.
- Typed signing — EIP-712 typed-data signing is a declared capability (
SIGN_MESSAGE), gated like any other. - Value — moving ETH requires the
VALUEcapability to be openly declared (rail R4). An app can't move value it never said it could.
Related
- The Capability Enforcer — the rails that keep actions honest.
- Guides → Declare a Safe Action — build one.
Human-readable action decoding ("you see what you sign")
Before a user signs an action, ChainRender renders a plain-English decode of exactly what the transaction will
do -- derived from the declared action {to, sig, value, args}, not guessed from calldata. For example, a mint
button decodes to:
This button will: Mint 3 -> 0x36a4...c3c9 - sends 0.03 ETH function
mint(uint256)on0x36a4...C3c9- sends 0.03 ETH
Because the runtime can only execute exactly the declared action, the decode is a guarantee, not an inference -- the button cannot call anything else or send undeclared value. The panel states this explicitly.
Swaps and multi-arg financial actions carry an honesty flag: because the raw parameters don't label which value is the slippage minimum or which addresses are the token path, the decode flags "verify which value is your minimum-received and which token addresses are in the path." This is deliberate -- a decode that looked complete while hiding the important half would be worse than none. ChainRender decodes and admits the limits of the decode.
Payments build on actions
A crypto payment is just a declared action (pay(...) / transfer(...)), decoded before signing like any other.
To also serve users without crypto, an app can add an optional off-chain card on-ramp — honestly graded off-chain so
the on-chain guarantee is never overstated. See Payments for the full pattern (crypto floor + card
enhancement + honest grading).