Skip to content

Commit 38db4f2

Browse files
committed
feat(docs): enhance smart accounts documentation new custom instructions dos
1 parent 90e1a29 commit 38db4f2

2 files changed

Lines changed: 435 additions & 109 deletions

File tree

docs/smart-accounts/3-custom-instruction.mdx

Lines changed: 92 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -19,126 +19,109 @@ keywords:
1919
import ThemedImage from "@theme/ThemedImage";
2020
import useBaseUrl from "@docusaurus/useBaseUrl";
2121

22-
Flare Smart Accounts allow users to execute custom function calls on the Flare chain through instructions on XRPL.
23-
The process expands on the workflow for other actions by including an additional step at the beginning.
24-
25-
In order for the `MasterAccountController` contract to be able to give a custom instruction to a personal account, the custom action must first be registered with the said contract.
26-
The custom instruction is stored in a mapper, with its 30-byte hash as a key.
27-
That hash is then sent as the payment reference, along with the byte representation of the hexadecimal number `ff` (decimal `255`) in the first byte and the `walletId` in the second byte.
28-
The `walletId` is a Flare-designated value, used by the operator for wallet identification.
29-
30-
<div
31-
style={{
32-
width: "90%",
33-
margin: "0 auto",
34-
display: "flex",
35-
justifyContent: "center",
36-
}}
37-
>
38-
<ThemedImage
39-
alt="Breakdown of bytes in payment reference for the custom action"
40-
style={{ width: "100%" }}
41-
sources={{
42-
light: useBaseUrl(
43-
"/img/docs/smart-accounts/bytes-custom-instruction-light.png",
44-
),
45-
dark: useBaseUrl(
46-
"/img/docs/smart-accounts/bytes-custom-instruction-dark.png",
47-
),
48-
}}
49-
/>
50-
</div>
51-
52-
## The expanded workflow
53-
54-
We expand the workflow described in the [Flare Smart Accounts overview](/smart-accounts/overview) with an additional step before the first.
55-
56-
0. A custom instruction is registered with the `MasterAccountController` contract.
57-
1. The XRPL user sends instructions as a `Payment` transaction to a specific XRPL address, with instructions encoded as the payment reference in the memo field.
58-
2. The operator interacts with the [Flare Data Connector](/fdc/overview) to procure a `Payment` proof.
59-
It then calls the `executeTransaction` function on the `MasterAccountController` contract, with the `Payment` proof and the XRPL address that made the transaction.
60-
3. The XRPL user's smart account performs the actions given in the instructions.
61-
62-
## Custom Instructions
63-
64-
Custom instructions are an array of the `CustomInstructions.CustomCall` Solidity struct.
65-
The struct contains three fields:
66-
67-
- `targetContract`: the address of the smart contract that will execute the custom function
68-
- `value`: the amount of FLR paid to the contract
69-
- `data`: transaction calldata, which includes a function selector and values of the function's arguments
70-
71-
Each of the custom instructions in the array will be performed in order.
72-
A call to the `targetContract` address is made, with the specified `value` and the calldata `data`.
73-
74-
In Solidity, we can obtain the calldata by doing the following:
75-
76-
```Solidity
77-
abi.encodeWithSignature("<functionName>(<type1>,<type2>,...,<typeN>)", [<value1>, <value2>, ..., <valueN>]);
78-
```
79-
80-
where `<functionName>` is the name of the function that we want to call, `<type1>`, `<type2>`, . . . , `<typeN>` are its argument types, and `<value1>`, `<value2>`, . . . , `<valueN>` their values.
22+
Flare Smart Accounts let an XRPL user execute arbitrary contract calls on Flare from a [`Payment`](https://xrpl.org/docs/references/protocol/transactions/types/payment) transaction on the XRPL blockchain.
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.
8124

82-
Only function calls with specific parameter values included can be registered.
83-
That means that a new custom instruction needs to be registered for each unique action (though this can be done just seconds in advance).
84-
It is also the reason why special FAsset actions have their own IDs, instead of defaulting to the custom call - it allows us to also specify certain parameters within the instructions on XRPL.
25+
Custom instructions carry the full call payload in the XRPL **memo** field.
26+
The user signs a `PackedUserOperation`, ships it on XRPL, and the smart account replays it on the Flare blockchain.
8527

86-
:::warning
87-
Encoding calldata by hand is error prone.
88-
It is recommended to use established libraries, or an [online tool](https://abi.hashex.org/) (if you want to quickly check something).
28+
:::warning No destination tags
29+
XRPL transactions targeting smart accounts must not use a destination tag.
30+
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.
8931
:::
9032

91-
## Call hash
33+
## User operation payload
9234

93-
To produce the custom instructions calldata, we first ABI encode the array of the `CustomInstructions.CustomCall` struct.
94-
We then take the `keccak256` hash of that value, and drop the first two bytes (`(1 << 240) - 1` shifts the number binary number `1` left `30*8` times, and replaces it with `0` and all the `0`s that follow it with `1`; essentially, we create a mask of length `30*8` of only `1`s).
95-
That is the call hash that is provided as the payment reference for the custom action, and the ID under which the custom instructions are stored in the `MasterAccountController` contract.
35+
The custom instruction payload has two layers: the outer EIP-4337 [`PackedUserOperation`](https://eips.ethereum.org/EIPS/eip-4337#useroperation) carried in the XRPL memo, and the inner [`executeUserOp(Call[])`](/smart-accounts/reference/IPersonalAccount#executeuserop) that the personal account runs once the controller dispatches it.
9636

97-
```Solidity
98-
return bytes32(uint256(keccak256(abi.encode(_customInstruction))) & ((1 << 240) - 1));
37+
### `PackedUserOperation`
38+
39+
```solidity
40+
struct PackedUserOperation {
41+
address sender;
42+
uint256 nonce;
43+
bytes initCode;
44+
bytes callData;
45+
bytes32 accountGasLimits;
46+
uint256 preVerificationGas;
47+
bytes32 gasFees;
48+
bytes paymasterAndData;
49+
bytes signature;
50+
}
9951
```
10052

101-
The call hash can also be obtained through the `encodeCustomInstruction` helper function of the `MasterAccountController` contract.
53+
Only three fields are required for Flare Smart Accounts:
10254

103-
```Solidity
104-
function encodeCustomInstruction(
105-
CustomInstructions.CustomCall[] calldata _customInstruction
106-
) public pure returns (bytes32) {
107-
return CustomInstructions.encodeCustomInstruction(_customInstruction);
108-
}
109-
```
55+
- `sender` **must** equal the address of the personal account derived from the XRPL sender.
56+
Use [`getPersonalAccount(xrplOwner)`](/smart-accounts/reference/IMasterAccountController#getpersonalaccount) to look it up — the address is deterministic, so you can fetch it before the account is even deployed.
57+
- `nonce` **must** equal the personal account's current nonce returned by [`getNonce(personalAccount)`](/smart-accounts/reference/IMasterAccountController#getnonce).
58+
The nonce auto-increments on every successful execution to prevent replay.
59+
- `callData` is the calldata that the controller invokes on the personal account.
60+
In practice, this is `abi.encodeCall(IPersonalAccount.executeUserOp, (calls))` — anything else either reverts or is rejected by the personal account's `onlyController` modifier.
61+
62+
The remaining fields are not validated on-chain and can be left empty.
63+
Authorization comes from the XRPL signature on the `Payment` transaction itself: only the XRPL key for `sender`'s `xrplOwner` can deliver the memo.
64+
If the personal account has pinned an executor via [`getExecutor`](/smart-accounts/reference/IMasterAccountController#getexecutor), only that executor is permitted to relay the mint.
65+
66+
### `executeUserOp` and the `Call` struct
67+
68+
The personal account's [`executeUserOp(Call[])`](/smart-accounts/reference/IPersonalAccount#executeuserop) iterates the provided calls and forwards each one with its supplied `value` and `data`:
11069

111-
Behind the scenes, the `MasterAccountController` contract calls the `encodeCustomInstruction` function of the `CustomInstructions` library.
70+
```solidity
71+
struct Call {
72+
address target;
73+
uint256 value;
74+
bytes data;
75+
}
11276
113-
```Solidity
114-
function encodeCustomInstruction(
115-
CustomCall[] calldata _customInstruction
116-
)
117-
internal pure
118-
returns (bytes32)
119-
{
120-
return bytes32(uint256(keccak256(abi.encode(_customInstruction))) & ((1 << 240) - 1));
121-
}
77+
function executeUserOp(Call[] calldata _calls) external payable;
78+
```
12279

80+
Each call is dispatched in array order with the personal account as `msg.sender`.
81+
If any call reverts, the whole user operation reverts with [`CallFailed(index, returnData)`](/smart-accounts/reference/IPersonalAccount#callfailed) — partial execution is not possible.
82+
83+
`executeUserOp` is `payable`, so the user operation can forward FLR alongside the calls.
84+
Funding works the usual way: send FLR to the personal account address (it is deterministic; see [State Lookup](/smart-accounts/guides/state-lookup-ts#personal-account-of-an-xrpl-address)) and the calls can spend it via `Call.value`.
85+
86+
### Building `callData` in TypeScript
87+
88+
```typescript
89+
import { encodeFunctionData } from "viem";
90+
91+
const calls = [
92+
{
93+
target: counterAddress,
94+
value: 0n,
95+
data: encodeFunctionData({
96+
abi: counterAbi,
97+
functionName: "increment",
98+
args: [],
99+
}),
100+
},
101+
];
102+
103+
const callData = encodeFunctionData({
104+
abi: personalAccountAbi,
105+
functionName: "executeUserOp",
106+
args: [calls],
107+
});
123108
```
124109

125-
## 0. Register custom instructions
126-
127-
We register a custom instruction by calling the `registerCustomInstruction` function on the `MasterAccountController` contract.
128-
The `CustomInstructions.CustomCall` array is provided as an argument.
129-
It is encoded as described above and stored in a `CustomInstructions` mapping.
130-
131-
To obtain the instruction that can be sent as the memo of an XRPL Payment transaction, we take the call hash produced by the `encodeCustomInstruction` function and modify it the following way.
132-
First, we remove the initial two bytes from this hash.
133-
Next, we prepend the hexadecimal value `ff` followed by the `walletId`.
134-
This is the encoded custom instruction.
135-
136-
<ThemedImage
137-
alt="Flare smart accounts custom instruction workflow"
138-
sources={{
139-
light: useBaseUrl(
140-
"img/docs/smart-accounts/fsa-developer-workflow-light.png",
141-
),
142-
dark: useBaseUrl("img/docs/smart-accounts/fsa-developer-workflow-dark.png"),
143-
}}
144-
/>
110+
The encoded `callData` becomes the `callData` field of the `PackedUserOperation` placed in the XRPL memo.
111+
112+
## Replay protection
113+
114+
Each personal account maintains a monotonically increasing nonce, accessible via [`getNonce`](/smart-accounts/reference/IMasterAccountController#getnonce).
115+
A successful `executeUserOp` increments the nonce and emits [`UserOperationExecuted`](/smart-accounts/reference/IMasterAccountController#useroperationexecuted), so the same `PackedUserOperation` cannot be re-executed.
116+
117+
## Failure Handling
118+
119+
The whole pipeline is atomic with respect to the user operation:
120+
121+
- If `sender` does not match the personal account, the call reverts with [`InvalidSender`](/smart-accounts/reference/IMasterAccountController#invalidsender).
122+
- If `nonce` is not the expected value, it reverts with [`InvalidNonce`](/smart-accounts/reference/IMasterAccountController#invalidnonce).
123+
- If the memo body has the wrong length for its instruction ID, it reverts with [`InvalidMemoData`](/smart-accounts/reference/IMasterAccountController#invalidmemodata); an unrecognized instruction byte reverts with [`InvalidInstructionId`](/smart-accounts/reference/IMasterAccountController#invalidinstructionid).
124+
- If any inner call reverts, the personal account surfaces it as [`CallFailed(index, returnData)`](/smart-accounts/reference/IPersonalAccount#callfailed) and the entire user operation reverts.
125+
126+
Because the FXRP transfer is performed before the memo is decoded, **the mint succeeds even if the user operation reverts** — see [`DirectMintingExecuted`](/smart-accounts/reference/IMasterAccountController#directmintingexecuted).
127+
The freshly minted FXRP lands in the personal account, and the user can recover by either re-submitting a fixed user operation (after advancing the nonce with the `0xE1` memo, which emits [`NonceIncreased`](/smart-accounts/reference/IMasterAccountController#nonceincreased)) or sweeping the FXRP via standard FAsset instructions.

0 commit comments

Comments
 (0)