Skip to content

Commit aab74ca

Browse files
refactor(docs): make hash custom instruction the preferred one
1 parent e1405d4 commit aab74ca

14 files changed

Lines changed: 979 additions & 985 deletions

docs/smart-accounts/1-overview.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ The first nibble is the instruction type.
106106
This is either `FXRP`, `Firelight`, or `Upshift` (with corresponding type IDS `0`, `1`, and `2`).
107107
The second nibble is the instruction command; the available commands are different for each instruction type.
108108

109-
For the direct-minting flow, the memo carries a different layout see the [Full Custom Instruction guide](/smart-accounts/full-custom-instruction) for the `PackedUserOperation` format, the [Hash Custom Instruction guide](/smart-accounts/hash-custom-instruction) for the 0xFE variant, or the [comparison](/smart-accounts/custom-instruction-comparison) of `0xFF`/`0xFE` and the executor-management codes `0xE0`/`0xE1`/`0xE2`/`0xD0`/`0xD1`.
109+
For the direct-minting flow, the memo carries a different layout - see the [Custom Instruction guide](/smart-accounts/custom-instruction) for the 0xFE variant, the [Raw Custom Instruction guide](/smart-accounts/raw-custom-instruction) for the `PackedUserOperation` format, or the [comparison](/smart-accounts/custom-instruction-comparison) of `0xFE`/`0xFF` and the executor-management codes `0xE0`/`0xE1`/`0xE2`/`0xD0`/`0xD1`.
110110

111111
<details>
112112
<summary>Table of instruction IDs and corresponding actions.</summary>
@@ -180,8 +180,8 @@ The facet enforces that the caller is the `AssetManager`, resolves (or deploys)
180180

181181
The XRPL user's smart account performs the actions in the instructions.
182182
This can be any of the instructions listed above, reserving collateral for minting FXRP, transferring FXRP to another address, redeeming FXRP, depositing it into a vault ...
183-
Furthermore, custom instructions can be executed arbitrary function calls on Flare, encoded as an EIP-4337 `PackedUserOperation` and replayed on-chain by the personal account.
184-
The user operation can be carried in the XRPL memo in full (opcode `0xFF`, see the [Full Custom Instruction guide](/smart-accounts/full-custom-instruction)), or committed to as a 32-byte hash with the bytes delivered to Flare by an off-chain executor (opcode `0xFE`, see the [Hash Custom Instruction guide](/smart-accounts/hash-custom-instruction)).
183+
Furthermore, custom instructions can be executed - arbitrary function calls on Flare, encoded as an EIP-4337 `PackedUserOperation` and replayed on-chain by the personal account.
184+
The user operation can be committed to as a 32-byte hash with the bytes delivered to Flare by an off-chain executor (opcode `0xFE`, see the [Custom Instruction guide](/smart-accounts/custom-instruction)), or carried in the XRPL memo in full (opcode `0xFF`, see the [Raw Custom Instruction guide](/smart-accounts/raw-custom-instruction)).
185185
Authorization comes from the XRPL `Payment` signature itself; the on-chain check only validates the `sender` and `nonce` fields of the `PackedUserOperation`.
186186
The [Custom Instruction Comparison](/smart-accounts/custom-instruction-comparison) covers when to pick each.
187187

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
---
2+
sidebar_position: 1
3+
slug: custom-instruction
4+
title: Custom Instruction
5+
authors: [nikerzetic]
6+
description: Performing custom function calls in the Flare Smart Accounts by committing to a PackedUserOperation hash on XRPL and delivering the bytes via an executor (0xFE).
7+
tags: [intermediate, ethereum, flare-smart-accounts]
8+
keywords:
9+
[
10+
flare-fdc,
11+
ethereum,
12+
flare-smart-accounts,
13+
evm,
14+
flare-network,
15+
account-abstraction,
16+
]
17+
---
18+
19+
import ThemedImage from "@theme/ThemedImage";
20+
import useBaseUrl from "@docusaurus/useBaseUrl";
21+
22+
Flare Smart Accounts let an XRPL user execute arbitrary contract calls on Flare through an XRPL [`Payment`](https://xrpl.org/docs/references/protocol/transactions/types/payment) transaction.
23+
Each personal account exposes an [EIP-4337](https://eips.ethereum.org/EIPS/eip-4337) style `executeUserOp` entry point that the [`MasterAccountController`](/smart-accounts/reference/IMasterAccountController) invokes when it processes a custom instruction memo.
24+
25+
The **custom instruction** (memo opcode `0xFE`) is the recommended way to drive those calls.
26+
The XRPL memo is a fixed 42 bytes that commits to a `PackedUserOperation` by carrying only its `keccak256` hash, and an off-chain executor delivers the actual user operation bytes to the FAssets `AssetManager` on Flare.
27+
This keeps the XRPL footprint constant regardless of how complex the call batch is.
28+
29+
For the simpler single-actor variant that ships the entire `PackedUserOperation` inline in the XRPL memo, see the [Raw Custom Instruction](/smart-accounts/raw-custom-instruction); the [comparison guide](/smart-accounts/custom-instruction-comparison) covers when to pick which.
30+
31+
:::warning No destination tags
32+
XRPL transactions targeting smart accounts must not use a destination tag.
33+
A destination tag forces [FAssets direct minting](/fassets/direct-minting) to credit the tag-holder, which would let an unrelated party front-run the user operation.
34+
:::
35+
36+
## User operation payload
37+
38+
A custom instruction has two layers: the outer EIP-4337 [`PackedUserOperation`](https://eips.ethereum.org/EIPS/eip-4337#useroperation) that the XRPL memo commits to, and the inner [`executeUserOp(Call[])`](/smart-accounts/reference/IPersonalAccount#executeuserop) that the personal account runs once the controller dispatches it.
39+
40+
Only three fields from the [`PackedUserOperation`](https://eips.ethereum.org/EIPS/eip-4337#useroperation) struct are required for Flare Smart Accounts:
41+
42+
- `sender` **must** equal the address of the personal account derived from the XRPL sender.
43+
Use [`getPersonalAccount`](/smart-accounts/reference/IMasterAccountController#getpersonalaccount) to look it up - the address is deterministic, so you can fetch it before the account is even deployed.
44+
- `nonce` **must** equal the personal account's current nonce returned by [`getNonce`](/smart-accounts/reference/IMasterAccountController#getnonce).
45+
The nonce auto-increments on every successful execution to prevent replay.
46+
- `callData` is the calldata that the controller invokes on the personal account.
47+
In practice, this is `abi.encodeCall(IPersonalAccount.executeUserOp, (calls))` - anything else either reverts or is rejected by the personal account's `onlyController` modifier.
48+
49+
The remaining fields are not validated on-chain and can be left empty.
50+
Authorization comes from the XRPL signature on the `Payment` XRPL payment transaction itself: only the XRPL key for `sender`'s `xrplOwner` can deliver the memo.
51+
If the personal account has pinned an executor via [`getExecutor`](/smart-accounts/reference/IMasterAccountController#getexecutor), only that executor is permitted to relay the mint.
52+
53+
### `executeUserOp` and the `Call` struct
54+
55+
The personal account's [`executeUserOp(Call[])`](/smart-accounts/reference/IPersonalAccount#executeuserop) runs each entry in the `Call[]` in order, forwarding it with its supplied `value` and `data`:
56+
57+
```solidity
58+
struct Call {
59+
address target;
60+
uint256 value;
61+
bytes data;
62+
}
63+
64+
function executeUserOp(Call[] calldata _calls) external payable;
65+
```
66+
67+
Each call is dispatched with the personal account as `msg.sender`.
68+
If any call reverts, the whole user operation reverts with [`CallFailed`](/smart-accounts/reference/IPersonalAccount#callfailed) - partial execution is not possible.
69+
70+
The `executeUserOp` function is `payable`, so the user operation can forward native tokens (e.g. FLR) alongside the calls.
71+
To fund the personal account, send FLR to the address using the [Flare faucet](https://faucet.flare.network/).
72+
73+
### Building `callData` in TypeScript
74+
75+
You can build the `callData` in TypeScript using the [`encodeFunctionData`](https://viem.sh/docs/contract/encodeFunctionData#encodefunctiondata) function from the `viem` library:
76+
77+
```typescript
78+
import { encodeFunctionData } from "viem";
79+
80+
const calls = [
81+
{
82+
target: counterAddress,
83+
value: 0n,
84+
data: encodeFunctionData({
85+
abi: counterAbi,
86+
functionName: "increment",
87+
args: [],
88+
}),
89+
},
90+
];
91+
92+
const callData = encodeFunctionData({
93+
abi: personalAccountAbi,
94+
functionName: "executeUserOp",
95+
args: [calls],
96+
});
97+
```
98+
99+
The encoded `callData` becomes the `callData` field of the `PackedUserOperation` that the XRPL memo commits to.
100+
101+
## Memo layout
102+
103+
The custom instruction memo is a constant 42 bytes:
104+
105+
| Bytes | Field | Meaning |
106+
| ------- | ---------------- | ------------------------------------------------------------------- |
107+
| `0` | `instructionId` | `0xFE` - custom instruction |
108+
| `1` | `walletId` | One-byte wallet identifier assigned by Flare; `0` if not registered |
109+
| `2-9` | `executorFeeUBA` | Executor fee in the FAsset's smallest unit, big-endian `uint64` |
110+
| `10-41` | `userOpHash` | `keccak256(abi.encode(userOp))` - the 32-byte commitment |
111+
112+
The memo length is independent of the call batch: a single small call and a 50-call batch both fit in the same 42 bytes, because the user-operation bytes the executor delivers off-chain never touch the XRPL ledger.
113+
This is the main practical advantage over the [Raw Custom Instruction](/smart-accounts/raw-custom-instruction), whose memo carries the entire ABI-encoded `PackedUserOperation` and is therefore subject to the XRPL's 1024-byte memo cap.
114+
115+
The off-chain delivery also makes the call payload **private on XRPL**.
116+
Only the 32-byte commitment is published; the inner `target`, `value`, and `data` of each call only become visible when the executor submits the user operation to Flare.
117+
118+
## Three-step protocol
119+
120+
The 0xFE flow runs three steps that map onto two independent actors in production.
121+
A demo script can run all three from the same process, but the on-chain checks are designed around the two-actor split.
122+
The executor bridges the XRPL payment to Flare with a proof from the [Flare Data Connector (FDC)](/fdc/overview), the same attestation system used by the proof-based flow:
123+
124+
```mermaid
125+
sequenceDiagram
126+
participant User as User (XRPL)
127+
participant XRPL
128+
participant Executor
129+
participant AM as AssetManagerFXRP
130+
participant MasterAccountController
131+
participant PersonalAccount
132+
133+
User->>User: 1. encode UserOp
134+
User->>User: compute keccak256(userOp)
135+
User->>XRPL: 2. Payment to direct-minting<br/>address with 42-byte memo<br/>[0xFE][walletId][fee][hash]
136+
Note over Executor: receives userOp bytes<br/>out-of-band
137+
138+
Executor->>Executor: 3. fetch FDC XRPPayment proof
139+
Executor->>AM: 4. executeDirectMintingWithData(proof, data)<br/>{value: sum(call.value)}
140+
AM->>MasterAccountController: handleMintedFAssets(..., _memoData, _executor, _data)
141+
MasterAccountController->>MasterAccountController: keccak256(_data) == hash?
142+
MasterAccountController->>PersonalAccount: call{value}(executeUserOp(calls))
143+
MasterAccountController-->>User: UserOperationExecuted event
144+
```
145+
146+
### Step 1: user side
147+
148+
The user constructs the `PackedUserOperation` as described in [User operation payload](#user-operation-payload), computes `keccak256` over the ABI encoding, and packs the 42-byte memo from [Memo layout](#memo-layout).
149+
150+
The user sends an XRPL `Payment` to the FAssets direct-minting address with this memo, and delivers the full `PackedUserOperation` bytes to the executor **out-of-band** (e.g. over an authenticated HTTP API).
151+
The bytes never appear on the XRPL ledger.
152+
153+
### Step 2: executor side
154+
155+
The executor takes the XRPL transaction hash, requests an [`IXRPPayment` attestation](/fdc/attestation-types/payment) from the [Flare Data Connector](/fdc/overview), and calls `executeDirectMintingWithData` on `AssetManagerFXRP` (see the [FAssets direct minting page](/fassets/direct-minting)):
156+
157+
```solidity
158+
function executeDirectMintingWithData(
159+
IXRPPayment.Proof calldata _payment,
160+
bytes calldata _data
161+
) external payable;
162+
```
163+
164+
- `_payment` is the FDC proof of the XRPL `Payment`.
165+
- `_data` is the ABI-encoded `PackedUserOperation` that was delivered out-of-band.
166+
- `msg.value` **must equal the sum of `call.value` across the user operation**.
167+
`AssetManagerFXRP` forwards this value into `MasterAccountController.handleMintedFAssets`, which forwards it again into the personal account's `executeUserOp` so the inner calls can attach native value.
168+
169+
The `executeDirectMintingWithData` function is **only valid for smart-account targets** - calling it for a non-smart-account direct mint reverts.
170+
171+
### Step 3: confirmation
172+
173+
The `MasterAccountController` verifies on-chain that `keccak256(_data) == userOpHash` from the memo.
174+
If it matches, it decodes `_data` as a `PackedUserOperation`, validates `sender` and `nonce`, executes `executeUserOp` on the personal account, and emits [`UserOperationExecuted`](/smart-accounts/reference/IMasterAccountController#useroperationexecuted) - **all inside the executor's transaction**.
175+
This is the key difference from the proof-based dispatch: there is no separate cross-chain wait, because the executor's call already executed the user operation by the time it returns.
176+
177+
## Hash mismatch
178+
179+
If the bytes the executor submits do not hash to the commitment in the memo, `handleMintedFAssets` reverts with `CustomInstructionHashMismatch(expected, actual)`.
180+
The FAsset transfer is performed before the memo is decoded, however, so even on a mismatch the FXRP credited by direct minting remains in the personal account, and the user can recover by issuing a fresh user operation (see [Failure Handling](#failure-handling)).
181+
182+
## Call value accounting
183+
184+
Whatever native value the executor attaches to `executeDirectMintingWithData` is forwarded all the way to `executeUserOp` (`AssetManagerFXRP -> MasterAccountController.handleMintedFAssets -> PersonalAccount.executeUserOp`).
185+
The executor must therefore compute the total native value to attach as the sum of `call.value` across the user operation it received out-of-band.
186+
The user-side helper in the [TypeScript guide](/smart-accounts/guides/typescript-viem/custom-instruction-ts) returns this value alongside the XRPL transaction hash, so the executor does not have to recompute it from scratch.
187+
188+
## Replay protection
189+
190+
Two replay-protection layers gate every custom instruction:
191+
192+
- The user operation's `nonce` must equal the personal account's current memo-instruction nonce; the nonce auto-increments on every successful execution.
193+
- The XRPL transaction ID is recorded in the controller and cannot be reused for a second mint.
194+
195+
The on-chain hash check additionally pins the executor's `_data` to the exact bytes the user signed via XRPL, so the executor cannot substitute a different payload after the fact.
196+
197+
## Failure Handling
198+
199+
The whole pipeline is atomic with respect to the user operation:
200+
201+
- If `sender` does not match the personal account, the call reverts with [`InvalidSender`](/smart-accounts/reference/IMasterAccountController#invalidsender).
202+
- If `nonce` is not the expected value, it reverts with [`InvalidNonce`](/smart-accounts/reference/IMasterAccountController#invalidnonce).
203+
- If the memo length is not exactly `42` bytes, `handleMintedFAssets` reverts with [`InvalidMemoData`](/smart-accounts/reference/IMasterAccountController#invalidmemodata); an unrecognized instruction byte reverts with [`InvalidInstructionId`](/smart-accounts/reference/IMasterAccountController#invalidinstructionid).
204+
- If `keccak256(_data)` does not match the hash in the memo, the call reverts with `CustomInstructionHashMismatch(expected, actual)`.
205+
- If the executor's `msg.value` is less than the sum of `call.value` across the inner calls, the inner call reverts with [`CallFailed`](/smart-accounts/reference/IPersonalAccount#callfailed) and the whole user operation reverts.
206+
- If the personal account has pinned an executor via [`getExecutor`](/smart-accounts/reference/IMasterAccountController#getexecutor) and the caller of `executeDirectMintingWithData` is not that executor, the call reverts with [`WrongExecutor`](/smart-accounts/reference/IMasterAccountController#wrongexecutor).
207+
- If any inner call reverts, the personal account surfaces it as [`CallFailed`](/smart-accounts/reference/IPersonalAccount#callfailed) and the entire user operation reverts.
208+
209+
Because the FAsset transfer happens before the memo is decoded, **the FXRP mint succeeds even if the user operation reverts** - see [`DirectMintingExecuted`](/smart-accounts/reference/IMasterAccountController#directmintingexecuted).
210+
The minted FXRP remains in the personal account and the user can recover by either re-submitting a fixed user operation (with the next nonce) or moving the FXRP through standard [FAssets instructions](/smart-accounts/fasset-instructions).
211+
212+
## Next steps
213+
214+
- Walk through a Viem implementation in the [Custom Instruction TypeScript guide](/smart-accounts/guides/typescript-viem/custom-instruction-ts).
215+
- See the simpler single-actor variant in the [Raw Custom Instruction](/smart-accounts/raw-custom-instruction).
216+
- Compare the two flows in the [Custom Instruction Comparison](/smart-accounts/custom-instruction-comparison).
217+
- Dig into `IMasterAccountController` in the [reference](/smart-accounts/reference/IMasterAccountController).

0 commit comments

Comments
 (0)