You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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"
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:
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.
81
24
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.
85
27
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.
89
31
:::
90
32
91
-
## Call hash
33
+
## User operation payload
92
34
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.
-`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`:
110
69
111
-
Behind the scenes, the `MasterAccountController` contract calls the `encodeCustomInstruction` function of the `CustomInstructions` library.
function executeUserOp(Call[] calldata _calls) external payable;
78
+
```
122
79
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
+
});
123
108
```
124
109
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`.
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