Skip to main content

Custom Instruction

Flare Smart Accounts allow users to execute custom function calls on the Flare chain through instructions on XRPL. The process expands on the workflow for other actions by including an additional step at the beginning.

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. The custom instruction is stored in a mapper, with its 30-byte hash as a key. 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. The walletId is a Flare-designated value, used by the operator for wallet identification.

Breakdown of bytes in payment reference for the custom actionBreakdown of bytes in payment reference for the custom action

The expanded workflow

We expand the workflow described in the Flare Smart Accounts overview with an additional step before the first.

  1. A custom instruction is registered with the MasterAccountController contract.
  2. 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.
  3. The operator interacts with the Flare Data Connector to procure a Payment proof. It then calls the executeTransaction function on the MasterAccountController contract, with the Payment proof and the XRPL address that made the transaction.
  4. The XRPL user's smart account performs the actions given in the instructions.

Custom Instructions

Custom instructions are an array of the CustomInstructions.CustomCall Solidity struct. The struct contains three fields:

  • targetContract: the address of the smart contract that will execute the custom function
  • value: the amount of FLR paid to the contract
  • data: transaction calldata, which includes a function selector and values of the function's arguments

Each of the custom instructions in the array will be performed in order. A call to the targetContract address is made, with the specified value and the calldata data.

In Solidity, we can obtain the calldata by doing the following:

abi.encodeWithSignature("<functionName>(<type1>,<type2>,...,<typeN>)", [<value1>, <value2>, ..., <valueN>]);

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.

Only function calls with specific parameter values included can be registered. That means that a new custom instruction needs to be registered for each unique action (though this can be done just seconds in advance). 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.

warning

Encoding calldata by hand is error prone. It is recommended to use established libraries, or an online tool (if you want to quickly check something).

Call hash

To produce the custom instructions calldata, we first ABI encode the array of the CustomInstructions.CustomCall struct. 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 0s that follow it with 1; essentially, we create a mask of length 30*8 of only 1s). 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.

return bytes32(uint256(keccak256(abi.encode(_customInstruction))) & ((1 << 240) - 1));

The call hash can also be obtained through the encodeCustomInstruction helper function of the MasterAccountController contract.

function encodeCustomInstruction(
CustomInstructions.CustomCall[] calldata _customInstruction
) public pure returns (bytes32) {
return CustomInstructions.encodeCustomInstruction(_customInstruction);
}

Behind the scenes, the MasterAccountController contract calls the encodeCustomInstruction function of the CustomInstructions library.

function encodeCustomInstruction(
CustomCall[] calldata _customInstruction
)
internal pure
returns (bytes32)
{
return bytes32(uint256(keccak256(abi.encode(_customInstruction))) & ((1 << 240) - 1));
}

0. Register custom instructions

We register a custom instruction by calling the registerCustomInstruction function on the MasterAccountController contract. The CustomInstructions.CustomCall array is provided as an argument. It is encoded as described above and stored in a CustomInstructions mapping.

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. First, we remove the initial two bytes from this hash. Next, we prepend the hexadecimal value ff followed by the walletId. This is the encoded custom instruction.