XRP Ledger Standards

XLS-0101
Proposal
  title: XRPL Smart Contracts
  description: An L1 native implementation of Smart Contracts on the XRP Ledger
  created: 2025-07-28
  Author: Mayukha Vadari (@mvadari), Denis Angell (@dangell7)
  status: Proposal
  category: Amendment

XRPL Smart Contracts

Abstract

This document is a formal design of a smart contract system for the XRPL, which takes inspiration from several existing smart contract systems (including Xahau’s Hooks and the EVM).

Some sample use cases (a far-from-exhaustive list):

  • Integration with a new bridging protocol
  • A new DeFi protocol (e.g. derivatives or perpetuals)
  • Issued token staking rewards

Note: This document is still a fairly early draft, and therefore there are a few TODOs and open questions sprinkled through it on some of the more minor points. Any input on those questions would be especially appreciated.

1. Design Objectives

The main requirements we aimed to satisfy in our design:

  • Permissionless (i.e. no need for UNL approval to deploy a smart contract)
  • Easy access to native features/primitives (to build on the XRPL’s powerful building blocks)
  • Easy learning for new developers (i.e. familiar design paradigms)
  • Minimal impact to existing users/use-cases of the XRPL (especially with regard to payments and performance)
  • Minimal impact to node/validator costs
  • Able to auto-generate some sort of ABI from smart contract source code (to make tooling easier)

One of the great advantages of the XRPL is its human-readable transaction structure - unlike e.g. EVM transactions. In this design, we tried to keep that ethos of things being human-readable - such as keeping the ABI on-chain (even though that would increase storage).

1.1. General XRPL Programmability Vision

We envision programmability on the XRPL as the glue that seamlessly connects its powerful, native building blocks with the flexibility of custom on-chain business logic. This vision focuses on preserving what makes the XRPL special—its efficiency, reliability, and simplicity—while empowering builders to unlock new possibilities.

See the blog post here for more details.

2. Overview

This design for Smart Contracts combines the easy-to-learn overall design of EVM smart contracts (addresses with functions) with the familiarity of XRPL transactions. A Smart Contract lives on a pseudo-account and is triggered via a new ContractCall transaction, which calls a specific function on the smart contract, with provided parameters. The Smart Contract can modify its own state data, or interact with other XRPL building blocks (including other smart contracts) via submitting its own XRPL transactions via its code.

The details of the WASM engine and the API will be in separate XLSes published later.

This proposal involves:

  • Three new ledger entry types: ContractSource, Contract, and ContractData
  • Six new transaction types: ContractCreate, ContractCall, ContractModify, ContractDelete, ContractUserDelete, and ContractClawback
  • Two new RPC methods: contract_info and event_history
  • One new RPC subscription: eventEmitted
  • Modifications to the UNL-votable fee parameters (and the FeeSettings object that keeps track of that info)
  • Three new serialized types: STParameters, STParameterValues, and STData

2.1. Background: Pseudo-Accounts

A pseudo-account (XLS-64d) is an XRPL account that is impossible for any person to have the keys for (it is cryptographically impossible to have those keys). It may be associated with other ledger entries.

Since it is not governed by any set of keys, it cannot be controlled by any user. Therefore, it may host smart contracts.

2.2. Background: Serialized Types

The XRPL encodes data into a set of serialized types (all of whose names begin with the letters ST, standing for “Serialized Type”).

For example:

  • The “Account” field is of type STAccount (which represents XRPL account IDs)
  • The “Sequence” field is of type STUInt32 (which represents an unsigned 32-bit integer)
  • The “Amount” field is of type STAmount (which represents all amount types - XRP, IOUs, and MPTs)

2.3. Design Overview

  • Smart contracts (henceforth referred to as just “contracts”) are stored in pseudo-accounts
  • Contract stores the contract info
  • ContractSource is an implementation detail to make code storage more efficient
  • ContractData stores any contract-specific data
  • ContractCreate creates a new contract + pseudo-account, and allows the contract to do some setup work
  • ContractCall is used to trigger the transaction and call one of its functions
  • ContractModify allows the contract owner (or the contract itself) to modify a contract
  • ContractDelete allows the contract owner (or the contract itself) to delete a contract
  • ContractUserDelete allows a user of a smart contract to delete their data associated with a contract (and allows the contract to do any cleanup for that)
  • ContractClawback allows a token issuer to claw back from a contract (and allows the contract to do any cleanup for that)
  • There are some modifications to transaction common fields to support contract-submitted transactions
  • The contract_info RPC fetches the ABI of a contract and any other relevant information.
  • The event_history RPC fetches the event emission history for a contract.
  • The eventEmitted subscription allows you to subscribe to “events” emitted from a contract.
  • All computation limitations and fees will be configurable by UNL vote (just like transaction fees and reserves are currently).

2.4. Overview of Smart Contract Capabilities

  • Contract data storage
  • Per-user contract data storage
  • On-chain verified ABI
  • Read-only access to ledger state (any ledger entries)
  • Other changes to the ledger state are done via transactions that the pseudo-account “submits” from within contract code
  • Contract-level “environment/class variable” parameters that don’t require the source code to be changed
  • May have an init function that runs on ContractCreate for any account setup

3. Ledger Entry: ContractSource

The objective of this object is to save space on-chain when deploying the exact same contract (i.e. if the same code is used by multiple contracts, the ledger only needs to store it once). This feature was heavily inspired by the existing Hooks HookDefinition object (see this page for its documentation).

This object is essentially just an implementation detail to decrease storage costs, so that duplicate contracts don't need to have their source code copied on-chain. End-users won't have to worry about it. The core object in this design is the Contract object.

3.1. Fields

Field Name Required? JSON Type Internal Type Description
LedgerEntryType ✔️ string UInt16 The ledger entry's type (ContractSource).
ContractHash ✔️ string Hash256 The hash of the contract's code.
ContractCode ✔️ string blob The WebAssembly (WASM) bytecode for the contract.
InstanceParameters array STParameters The parameters that are provided by a deployment of this contract.
Functions ✔️ array STArray The functions that are included in this contract.
ReferenceCount ✔️ number UInt32 The number of Contract objects that are based on this ContractSource. This object is deleted when that value goes to 0.

3.1.1. Object ID

hash of prefix + ContractHash

3.1.2. InstanceParameters and Functions

  • Instance parameters are analogous to environment variables
  • TODO: should there be default values for these parameters?

3.1.3 Functions

  • All parameter types must be valid XRPL STypes (maybe ban STObjects and STArrays and other complex STypes like Transaction)
  • Advantage of storing the parameter types separately: you don’t have to open the VM to check if the parameters provided are valid (more efficient). You also get the ABI on-chain.
  • TODO: Maybe all variables in the smart contract must be STypes?
  • No more than 4 parameters allowed (for now - we can relax that restriction later)
  • All parameters are required, no overloading allowed
  • This could potentially change in a future version
  • Possible flags:
  • tfSendAmount - if the type is an STAmount, then that amount will be sent to the contract from your funds
  • tfSendNFToken - if the type is a Hash256, then the NFToken with that ID will be sent to the contract from your holdings
  • tfAuthorizeTokenHolding - if the type is an STIssue or STAmount, then you can automatically create a trustline/MPToken for that token (assuming it’s not XRP).
  • Something authorizing some amount of reserve to be claimed?
  • Others? We have space for 32 possible flags

3.2. Object Deletion

The ContractSource object does not have an owner.

  • The object is deleted if the ReferenceCount ever goes to 0
  • This could be an invariant check - no ContractSource object should exist with a ReferenceCount value of 0

3.3. Example Object

{
  LedgerEntryType:  "ContractSource",
  ContractHash: "610F33B8EBF7EC795F822A454FB852156AEFE50BE0CB8326338A81CD74801864",
  Code: "B80BE0CB156AEC7B852156AEFEC79FE50BE0CB83267E3267E5F822A454F1CD528574801864338A8610F33B8EBF95F822A454F1CD74801864338A8610F33B8EBF",
  InstanceParameters: [(0, "UInt16"), (0, "AccountID"), (tfSendAmount, "STAmount")],
  Functions: [
    {
      "name": "transfer",
      "parameters": [(0, "UInt32"), (tfSendAmount, "STAmount"), (0, "AccountID")]
    },
    {
      "name": "create",
      "parameters": [(0, "UInt32"), (0, "STAmount")]
    }
  ],
  ReferenceCount: 1
}

4. Ledger Entry: Contract

4.1. Fields

Field Name Required? JSON Type Internal Type Description
LedgerEntryType ✔️ string UInt16 The ledger entry's type (Contract).
ContractAccount ✔️ string AccountID The pseudo-account that hosts this contract.
Owner ✔️ string AccountID The owner of the contract, which defaults to the account that deployed the contract.
Flags ✔️ number UInt32 Flags that may be on this ledger entry.
Sequence ✔️ string UInt16 The ledger entry's type (ContractSource).
ContractHash ✔️ string Hash256 The hash of the contract's code.
InstanceParameterValues array STParameterValues The parameters that are provided by this deployment of the contract.
URI string Blob A URI that points to the source code of this contract, to make it easier for tooling to find the source code for the contract.

TODO: should the URI field live on ContractSource or Contract?

4.1.1. Object ID

hash of prefix + ContractHash + Sequence

4.1.2. Flags

  • lsfImmutable - the code can’t be updated.
  • lsfCodeImmutable - the code can’t be updated, but the instance parameters can.
  • lsfABIImmutable - the code can be updated, but the ABI cannot. This ensures backwards compatibility.
  • lsfUndeletable - the contract can’t be deleted.

A Contract can have at most one of lsfImmutable, lsfCodeImmutable, and lsfABIImmutable enabled.

4.1.3. InstanceParameters

The instance parameter list must match the types of the matching ContractSource object.

4.2. Account Deletion

The Contract object is a deletion blocker.

4.3. Example Object

{
  LedgerEntryType:  "Contract",
  Owner: "rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm",
  ContractHash: "610F33B8EBF7EC795F822A454FB852156AEFE50BE0CB8326338A81CD74801864",
  InstanceParameters: [(0, 1), (0, "rABCD...."), (tfSendAmount, "myToken")]
}

5. Ledger Entry: ContractData

Data is serialized using the STData serialization format.

5.1. Fields

Field Name Required? JSON Type Internal Type Description
LedgerEntryType ✔️ string UInt16 The ledger entry's type (ContractData).
Owner ✔️ string AccountID The account that hosts this data.
ContractAccount string AccountID The contract that controls this data. This field is only needed for user-owned data (where the Owner is different).
Data ✔️ string STData The contract-defined contract data.

5.1.1. Object ID

hash of prefix + Owner [+ ContractAccount]

5.2. Account Deletion

The ContractData object is a deletion blocker.

5.3. Reserves

Probably one reserve per 256 bytes

See Reserves

5.4. Example Object

{
    LedgerEntryType: "ContractData",
    Owner: "rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm",
    Name: "dataForFunction1",
    Data: {
        "count": 3,
        "total": 12,
        "destination": "r3PDXzXky6gboMrwUrmSCiUyhzdrFyAbfu"
    }
}

6. Transaction: ContractCreate

This transaction creates a pseudo-account with the contract inside it.

This transaction will also trigger a special init function in the contract, if it exists - which allows smart contract devs to do their own setup work as needed.

6.1. Fields

Field Name Required? JSON Type Internal Type Description
TransactionType ✔️ string UInt16 The transaction type (ContractCreate).
Account ✔️ string AccountID The account sending the transaction.
ContractOwner string AccountID The account that owns (controls) the contract. If not set, this is the same as the Account.
Flags number UInt32 Accepted bit-flags on this transaction.
ContractCode string Blob The WASM bytecode for the contract.
ContractHash string Hash256 The hash of the WASM bytecode for the contract.
Functions array STArray The functions that are included in this contract.
InstanceParameters array STParameters The parameters that are provided by a deployment of this contract.
InstanceParameterValues array STParameterValues The values of the instance parameters that apply to this instance of the contract.

6.1.1. Flags

  • tfImmutable - the code can’t be changed and the instance parameters can't be updated either.
  • tfCodeImmutable - the code can’t be updated, but the instance parameters can.
  • tfABIImmutable - the code can be updated, but the ABI cannot.
  • tfUndeletable - the contract can’t be deleted.

A contract may have at most one of tfImmutable, tfCodeImmutable, and lsfABIImmutable enabled.

6.1.2. ContractCode and ContractHash

Exactly one of these two fields must be included.

ContractCode should be used if the code has not already been uploaded to the XRPL (i.e. there is already a matching ContractSource object). This transaction will be more expensive.

ContractHash should be used if the code has already been uploaded to the XRPL. This transaction will be cheaper, since the code does not need to be re-uploaded.

If ContractCode is provided even if the code has already been uploaded, it will have the same outcome as if the ContractHash had been provided instead (albeit with a more expensive fee).

If ContractCode is provided, InstanceParameters and Functions must also be provided.

6.2. Fee

Similar to AMMCreate, 1 object reserve fee (+ fees for running the init code, and probably also + fees per byte of code uploaded)

6.3. Failure Conditions

  • ContractHash is provided but there is no existing corresponding ContractSource ledger entry.
  • The ContractCode provided is invalid.
  • The ABI provided in Functions doesn't match the code.
  • InstanceParameters don't match what's in the existing ContractSource ledger entry.

6.4. State Changes

If the transaction is successful:

  • The pseudo-account that hosts the Contract is created.
  • The Contract object is created.
  • If the ContractSource object already exists, the ReferenceCount will be incremented.
  • If the ContractSource object does not already exist, it will be created.

6.5. Example Transaction

{
  TransactionType:  "ContractCreate",
  Flags: tfImmutable,
  ContractCode: "74292CC654D754F217D0934762EA08742924F2762EA083DC8817D09341B1F2CC6C3DCDAE01565DB0994CA3E76E91C881B1F2DAE01565DB0994CA3E76E9154D75"
}

7. Transaction: ContractCall

This transaction triggers a specific function in a given contract, with the provided parameters

  • Must match the parameter types/flags provided in the contract (and in the right order)

7.1. Fields

Field Name Required? JSON Type Internal Type Description
TransactionType ✔️ string UInt16 The transaction type (ContractCall).
Account ✔️ string AccountID The account sending the transaction.
ContractAccount ✔️ string AccountID The contract to call.
FunctionName ✔️ string Blob The function on the contract to call.
Parameters array STParameterValues The parameters to provide to the contract’s function. Must match the order and type of the on-chain ABI.

7.2. Fee

The max number of instructions you’re willing to run (gas-esque behavior)

7.3. Failure Conditions

  • The ContractAccount doesn't exist or isn't a smart contract pseudo-account.
  • The function doesn't exist on the provided contract.
  • The parameters don't match the function's ABI.

7.4. State Changes

If the transaction is successful, the WASM contract will be called. The WASM code will govern the state changes that are made.

7.5. Example Transaction

{
  TransactionType: "ContractCall",
  ContractAccount: "rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm",
  FunctionName: "call_function", // could also be a number to save space
  Parameters: [
    {
      Flags: tfSendAmount,
      Amount: "1000000" // 1 XRP
    },
    {
      Flags: 0,
      Amount: {
        "currency": "USD",
        "issuer": "rJKnVATqzNsWa4jgnK5NyRKmK5s9QQWQYm",
        "value": "10"
      }
    },
    {
      Flags: 0,
      Account: "rMJAmiEQW4XUehMixb9E8sMYqgsjKfB1yC"
    },
    ...
  ]
}

8. Transaction: ContractModify

This transaction modifies a contract's code or instance parameters, if allowed.

8.1. Fields

Field Name Required? JSON Type Internal Type Description
TransactionType ✔️ string UInt16 The transaction type (ContractModify).
Account ✔️ string AccountID The account sending the transaction.
ContractAccount string AccountID The pseudo-account hosting the contract that is to be changed. This field is not needed if the pseudo-account is emitting this transaction itself.
ContractOwner ✔️ string AccountID The new owner of the contract (so that contract ownership is transferrable).
Flags number UInt32 Accepted bit-flags on this transaction.
ContractCode string Blob The new WASM bytecode for the contract.
ContractHash string Hash256 The hash of the new WASM bytecode for the contract.
Functions ✔️ array STArray The functions that are included in this contract.
InstanceParameters array STParameters The parameters that are provided by a deployment of this contract.
InstanceParameterValues array STParameterValues The values of the instance parameters that apply to this instance of the contract.

8.1.1. Flags

  • tfImmutable - the code can’t be changed anymore.
  • tfCodeImmutable - the code can’t be updated, but the instance parameters can.
  • tfABIImmutable - the code can be updated, but the ABI cannot.
  • tfUndeletable - the contract can’t be deleted anymore.

8.2. Fee

Will be equivalent to the per-byte/init fees of the ContractCreate transaction

8.3. Failure Conditions

  • The ContractAccount doesn’t exist or isn’t a contract pseudo-account.
  • The Account isn't the contract owner.
  • If ContractAccount isn’t specified, the Account isn’t a contract pseudo-account.
  • The contract has an lsfImmutable flag.
  • The contract has lsfABIImmutable enabled and isn't backwards-compatible (function names or parameters are changed). (Note: functions may be added)
  • ContractCode or ContractHash are provided but the contract has the tfCodeImmutable flag enabled.

8.4. State Changes

If the transaction is successful:

  • The Contract object is updated accordingly.
  • If the code is changed:
  • If the previous Contract object was the only user of a ContractSource object, the ContractSource object is deleted.
  • If the new Contract object does not have a corresponding existing ContractSource object, it is created.

9. Transaction: ContractDelete

This transaction deletes a contract. Only the pseudo-account itself or the owner of the transaction can do so.

9.1. Fields

Field Name Required? JSON Type Internal Type Description
TransactionType ✔️ string UInt16 The transaction type (ContractDelete).
Account ✔️ string AccountID The account sending the transaction.
ContractAccount string AccountID The pseudo-account hosting the contract that is to be changed. This field is not needed if the pseudo-account is emitting this transaction itself.

9.2. Failure Conditions

  • The ContractAccount doesn't exist or isn't a smart contract pseudo-account.
  • The ContractAccount holds deletion blocker objects (e.g. Escrow or ContractData).
  • The account submitting the transaction is neither the contract account itself nor the owner of the contract.

9.3. State Changes

If the transaction is successful:

  • The contract will be deleted, along with the pseudo-account.
  • All objects that are still owned by the account (which are not deletion blockers) will be deleted.
  • All remaining XRP in the account will be returned to the owner.

10. Transaction: ContractUserDelete

This transaction allows a user to delete their data associated with a contract. Only the user can submit this transaction (if the contract wants to modify user data, it can do that from the WASM code).

This transaction will also trigger a special user_delete function in the contract, if it exists - which allows smart contract devs to do their own cleanup work as needed.

10.1. Fields

Field Name Required? JSON Type Internal Type Description
TransactionType ✔️ string UInt16 The transaction type (ContractUserDelete).
Account ✔️ string AccountID The account sending the transaction (the user).
ContractAccount string AccountID The pseudo-account hosting the contract that is to be changed. This field is not needed if the pseudo-account is emitting this transaction itself.

10.2. Failure Conditions

  • The ContractAccount doesn't exist or isn't a smart contract pseudo-account.
  • The Account does not have any ContractData object for the contract in ContractAccount.

10.3. State Changes

If the transaction is successful:

  • The user's ContractData object associated with the ContractAccount will be deleted.
  • The user_delete function on the contract will be run to perform any cleanup work, if it exists.

11. Transaction: ContractClawback

This transaction allows issuers to claw back tokens from a contract, while also allowing smart contract devs to perform any cleanup they need to based on this clawback result.

This transaction will trigger a special clawback function in the contract, if it exists - which allows smart contract devs to do their own cleanup work as needed.

11.1. Fields

Field Name Required? JSON Type Internal Type Description
TransactionType ✔️ string UInt16 The transaction type (ContractClawback).
Account ✔️ string AccountID The account sending the transaction (the issuer of the token).
ContractAccount string AccountID The pseudo-account hosting the contract that is to be changed.
Amount ✔️ object Amount The amount to claw back from the contract.

11.2. Failure Conditions

  • The ContractAccount doesn't exist or isn't a smart contract pseudo-account.
  • Amount is invalid in some way (e.g. is negative, token doesn't exist, is XRP).
  • The Account isn't the issuer of the token specified in Amount.
  • The ContractAccount doesn't hold the token specified in Amount.
  • The ContractAccount holds the token specified in Amount, but holds less than the amount specified.

11.3. State Changes

If the transaction is successful:

  • The balance of the ContractAccount's token is decreased by Amount.

12. Transaction Common Fields

This standard doesn't add any new field to the transaction common fields, but it does add another global transaction flag and add another metadata field.

12.1. Flags

Flag Name Value
tfContractSubmittedTxn 0x20000000

This flag should only be used if a transaction is submitted from a smart contract. This signifies that the transaction shouldn't be signed. Any transaction that is submitted normally that includes this flag should be rejected.

Contract-submitted transactions will be processed in a method very similar to Batch inner transactions - i.e. executed within the ContractCall processing, rather than as a separate independent transaction. This allows the smart contract code to take actions based on whether the transaction was successful.

12.2. Metadata

Every contract-submitted transaction will contain an extra metadata field, ParentContractCallId, containing the hash of the ContractCall transaction that triggered its submission.

13. RPC: contract_info

This RPC fetches info about a deployed contract.

13.1. Request Fields

Field Name Required? JSON Type Description
contract_account ✔️ string The pseudo-account hosting the contract.
function string The function to specifically get information for.
user_account string An account to specifically get the contract’s data for.

13.2. Response Fields

Field Name Always Present? JSON Type Description
contract_account ✔️ string The pseudo-account hosting the contract.
code ✔️ string The WASM bytecode for the contract.
account_info ✔️ object The account_info output of the contract_account.
functions ✔️ array The functions in the smart contract and their parameters.
source_code_uri string The URI pointing to the source code of the contract (if it exists on-chain).
contract_data object The contract’s stored data.
user_data If user_account is included in the request object The contract’s stored data pertaining to that user.

13.2.1. functions

Each object in the array will contain the following fields:

Field Name Always Present? JSON Type Description
name ✔️ string The name of the function.
parameters ✔️ array A list of the parameters accepted for the function, in the format described above. This will be an empty list if the function takes no parameters.
fees ✔️ string The amount, in XRP, that you would likely have to pay to execute this function. TODO: not sure how doable this is, but it'd be nice if possible

14. RPC Subscription: eventEmitted

Subscribe to events emitted from a contract.

14.1. Request Fields

Field Name Required? JSON Type Description
contract_account ✔️ string The pseudo-account hosting the contract.
events array The event types to subscribe to, as an array of strings. If omitted, all events from the contract will be subscribed to.

TODO: maybe you should also be able to subscribe to all instances of a ContractSource?

14.2. Response Fields

Field Name Always Present? JSON Type Description
contract_account ✔️ string The pseudo-account hosting the contract.
events ✔️ array The events that were emitted from the contract, as explained below.
hash ✔️ string The hash of the transaction that triggered the event.
ledger_index ✔️ number The ledger index in which the event was triggered.

14.2.1. events

Each object in the events array will contain the following fields:

Field Name Always Present? JSON Type Description
name ✔️ string The name of the event.
data ✔️ object The data emitted as a part of the event.

The rest of the fields in this object will be dev-defined fields from the emitted event.

15. RPC Subscription: event_history

Fetch a list of historical events emitted from a given contract account.

15.1. Request Fields

Field Name Required? JSON Type Description
contract_account ✔️ string The pseudo-account hosting the contract.
events array The event types to retrieve, as an array of strings. If omitted, all events from the contract will be retrieved.
ledger_index_min number Use to specify the earliest ledger to include events from. A value of -1 instructs the server to use the earliest validated ledger version available.
ledger_index_max number Use to specify the most recent ledger to include events from. A value of -1 instructs the server to use the most recent validated ledger version available.
ledger_index LedgerIndex Use to look for events from a single ledger only.
ledger_hash string Use to look for events from a single ledger only.
binary boolean Defaults to false. If set to true, returns events as hex strings instead of JSON.
limit number Default varies. Limit the number of events to retrieve. The server is not required to honor this value.
marker any Value from a previous paginated response. Resume retrieving data where that response left off. This value is stable even if there is a change in the server's range of available ledgers. See here for details on how markers work.
transactions boolean Defaults to false. If set to true, returns the whole transaction in addition to the event. If set to falls, returns only the transaction hash.

15.2. Response Fields

Field Name Always Present? JSON Type Description
contract_account ✔️ string The pseudo-account hosting the contract.
events ✔️ array The events that were emitted from the contract.
ledger_index_min number The ledger index of the earliest ledger actually searched for events.
ledger_index_max number The ledger index of the most recent ledger actually searched for events.
limit number The limit value used in the request. (This may differ from the actual limit value enforced by the server.)
marker any Server-defined value indicating the response is paginated. Pass this to the next call to resume where this call left off.

15.2.1. events

Each object in the events array will contain the following fields:

Field Name Always Present? JSON Type Description
name ✔️ string The name of the event.
data object The data emitted as a part of the event, in JSON. Included if binary is set to false.
data_blob string The data emitted as a part of the event, in binary. Included if binary is set to true.
tx_json object The transaction that triggered the event, in JSON. Included if transactions is set to true and binary is set to false.
tx_blob string The transaction that triggered the event, in binary. Included if transactions is set to true and binary is set to true.
tx_hash string The transaction that triggered the event, in binary. Included if transactions is set to false.
validated boolean Whether or not the transaction that contains this event is included in a validated ledger. Any transaction not yet in a validated ledger is subject to change.

15.3. Implementation Details

This RPC will use the account transactions database. It will iterate through all ContractCall transactions sent to the provided contract_account and filter out the events based on the other provided parameters.

16. UNL-Votable Parameters

  • Number of drops per instruction run (in some unit that allows this to be <1)
  • Maximum number of instructions allowed in a single transaction
  • Maybe separate values for contracts vs subroutines (and maybe separate values for each subroutine)
  • Memory limits
  • Memory costs?

Initial values will be high fees/low maxes, but via the standard UNL voting process for fees, this can be adjusted over time as needed.

17. Serialized Type: STParameters

This object is essentially just a list of (Flag, SType value) groupings.

  • Name (maybe just a number to save space?)
  • This might not be needed - maybe just the parameters are encoded this way
  • UInt16 indicating required flags
  • Number of parameters
  • For each parameter:
  • UInt32 indicating the required flags for each param (e.g. “this should send real money”)
  • UInt16 indicating the parameter STypes
  • TODO: we might need an additional SType to describe a parameter
  • UInt32 for flags
  • UInt16 for SType
  • However many bytes for the encoded data

JSON representation is a name/number and the string names of the STypes - for example:

[
  {
    name: "transfer", // may also be a number (for a smaller size)
    parameters: [
      (0, "UInt32"), // could also be a dictionary
      (tfSendAmount, "STAmount"),
      (0, "AccountID"),
    ],
  },
];

18. Serialized Type: STParameterValues

This object is essentially just a list of (Flag, SType value, value that is of type `SType`) groupings.

Each of these groupings looks like this:

  • UInt32 indicating the required flags for each param (e.g. “this should send real money”)
  • UInt16 indicating the parameter SType
  • The byte encoding of the Value

JSON representation is a name/number and the string representation of the values - for example:

[
  (0, "UInt32", 1234), // could also be a dictionary
  (tfSendAmount, "STAmount", "1000000000"),
  (0, "AccountID", "rABCD...")
}

19. Serialized Type: STData

The following, serialized one after another:

  • Key-value pairs - this is repeated as many times as needed to cover all the data
  • A VL-encoded “key” (this could potentially be an SType if needed? Or just a number that the ABI defines?)
  • A VL-encoded “value”
  • TODO: you probably want a way to nest dictionaries
  • TODO: should this be called STJson instead and just handle JSON data?

JSON representation is a dictionary with the key-value pairs - for example:

{
  "count": 3,
  "total": 12,
  "destination": "r3PDXzXky6gboMrwUrmSCiUyhzdrFyAbfu"
}

20. Examples

20.1. Sample Code

Note: this is entirely made up, and is only intended to provide a rough idea of the concepts described in this document. The exact syntax is heavily subject to change (during both design iterations and in the implementation phase, as more details are figured out). For example, the final version will likely be in Rust.

// This sample function pays the destination some funds and transfers some data to the destination
function transfer(UInt16 number, STAmountSend amount, AccountID destination)
{
  const { balance } = getUserData(this, this.caller)
  if (!balance || balance < number)
    reject("Not enough balance") // error message included in metadata
  const { balance: destBalance } = getUserData(this, destination)
  if (!destBalance)
    return "Destination hasn't authorized data" // failure
  setUserData(this, this.caller, { balance: balance - number }) // assuming the "user data and reserves are handled by the user" model of contract data reserves
  setUserData(this, destination, { balance: balance + number })
  submitTransaction({
    TransactionType: "Payment",
    Destination: destination,
    Amount: amount
  })
  console.log("Transfer finished") // This is printed in the rippled debug.log (for testing purposes)
  emitEvent("transfer", {
    account: this.caller,
    destination: destination,
    number: number,
    amount: amount
  }) // this is included in the metadata - there can be limits on how much data may be included
  return 0 // success
}
  • Submitting transactions is like Hooks - the pseudo-account essentially “sends” transactions to do all of its on-chain modifications (other than contract-specific state)
  • There's a special init() function that allows you to do any initial account setup (instead of needing to do it in a separate function call)
  • emitEvent is used to emit an event that is stored in the metadata

21. Invariants

  • No ContractSource object should have a ReferenceCount of 0
  • Every Contract object should have an existing corresponding ContractSource object
  • A Contract cannot have both lsfImmutable and lsfCodeImmutable enabled.

22. Security

22.1. Pseudo-Account Account-Level Permissions

These settings will all be enabled by default on a pseudo-account:

  • Disable master key
  • Enable DepositAuth

These functions will all be disallowed from pseudo-accounts:

  • SignerListSet
  • SetRegularKey
  • Enable master key
  • Disable DepositAuth
  • AccountPermissionsSet

This prevents the contract account from receiving any funds directly (i.e. outside of the ContractCall transaction) and prevents any other account (malicious or otherwise) from submitting transactions directly from the contract account. This ensures that the contract account’s logic is entirely governed by the code, and nothing else.

22.2. Scam Contracts

Any amount you send to a contract can be rug-pulled. This is the same level of risk as any other scam on the XRPL right now - such as buying a rug-pulled token.

22.3. Fee-Scavenging/Dust Attacks

Don’t think this is possible here.

22.4. Re-entrancy Attacks

These will need to be guarded against in this design. One option for this is to disallow any recursion in calls - i.e. disallow ContractCall transactions from a contract account to itself. Other options are being investigated.

Open Questions

  • Pre-load all the contract instance params when opening the VM so it’s not expensive to fetch them?
  • How should a contract handle if someone sends a token they don’t have a trustline for?
  • Borsh instead of JSON for STData?
  • Consider other binary encodings alongside consideration of Borsh (e.g., maybe SSZ or SCALE)
  • Readonly contract functions like EVM? Might be for v2, or might be unnecessary given the human-readability of the STData type
  • Should ContractData objects be stored in a separate directory structure (e.g. a new one, not the standard owner directory)?
  • What should happen if user_delete or clawback fails or crashes in some way?
  • If it crashes due to running out of ComputationAmount the transaction should probably fail, if it crashes/fails for anything else (that is a result of the WASM code) the transaction should probably succeed
  • Consider a separate data spec, possibly including the concept of Rent.
  • What should happen to Gas fees -- burn them? Pay them out? something else?

Reserves

The biggest remaining question (from the core ledger design perspective) is how to handle object reserves for any contract-specific data (reserves for all existing ledger entries can be handled as they are now).

The most naive option is for the contract account to hold all necessary funds for the reserves. However, this is quite the burden on the contract account (and therefore the deployer of the contract). Ideally, there should be some way to put some of the burden on the contract users for the parts of the data that they use.

One example to illustrate the difference: an ERC-20 contract holds all of its data (e.g. holders and the amount they hold) in the contract itself. However, on the XRPL, an MPT issuer does not need to cover reserves for all of its holders and their data - only the reserves for the issuance data. The EVM doesn’t have the concept of reserves (it’s essentially amortized in the transaction fees), this concern doesn’t apply to those chains.

Account Reserve

The account reserve is essentially covered by the non-refundable ContractCreate transaction fee. This also covers the reserve for the Contract/ContractSource ledger entry, if needed.

TODO: perhaps there should also be limits on fields you can set in the AccountRoot.

Object Reserve

How should object reserves be covered? Some options (numbered for ease of discussion):

  1. You get some free objects via the ContractCreate fee and there's a hard cap. Higher fees for more reserves.
  2. There could also be a way to up this number later (some sort of reserve bump transaction, maybe, with additional fees), that anyone can call.
  3. Higher fees for anything that increases reserve on the account (reserves essentially burned)
  4. Downside: it becomes harder to calculate a contract call tx fee because you don't know what will require more reserve
  5. Some way to amortize higher fees for increasing reserve (a la EVM)
  6. Could be on the contract writer to figure out a way to handle this
  7. If not, could be complex to figure out a good system
  8. A Sponsor-esque way of keeping track of who owns the burden for certain objects (perhaps with some API calls - could default to the contract creator if the API isn't used)
  9. Downside: it would likely add an additional dependency to smart contracts (XLS-68d)
  10. Ignore the issue
  11. This isn’t really a viable option, given the ledger load it would result in - it would defeat the purpose of reserves in the first place
  12. User data and reserves are handled by the user
  13. Inspired by Move
  14. A “user data” object for contracts to store data for a particular user
  15. Hash is Contract ID + Account, only one object stores all of a user’s data
  16. N bytes per reserve charged (perhaps N=256), with some sort of limit towards the max amount of reserves that can be charged
  17. Maybe a flag on function call to indicate acceptance of reserve usage
    • Or maybe you include a number of acceptable reserves?
  18. Transaction to delete user data (to recover reserves) - contracts need to support this
    • Is this acceptable?
    • You can’t recover an escrow reserve until it’s cancellable or finishable so maybe this isn’t needed and it’ll be a norm
  19. Object is a deletion blocker (for the contract and the user)

The authors lean towards something akin to Option 6, as it feels the most XRPL-y, but supporting data deletion becomes complicated, because otherwise contract developers can lock up users’ reserves without them being able to free that reserve easily.

Appendix

Appendix A: FAQ

A.1: How does this compare to Hooks?

The main similarities:

  • The smart contract API is mostly the same. The biggest changes are renaming functions (and a couple of extra methods added).
  • Smart contracts interact with XRPL primitives by submitting/emitting transactions.
  • A “contract definition” object is used so that the ledger doesn’t need to store the same data repeatedly.
  • A WASM VM is used to process the smart contract code.

The main differences:

  • Smart contracts are installed on pseudo-accounts instead of user accounts.
  • Instead of being triggered by transactions, there is a special ContractCall transaction to trigger the smart contract.
  • Smart contract data is stored differently.
  • Users handle the reserves for their own data, instead of it all being handled by the smart contract account (TBD).
  • Smart contracts can emit events that developers can subscribe to.
  • Tools can auto-generate ABIs from source code (and ABIs are stored on-chain).

A.2: How does this compare to EVM?

The main similarities:

  • Smart contracts are installed at their own addresses.
  • Smart contracts are organized by functions, and they are called by transactions that encode the function to call and its parameters.
  • Smart contracts can emit events that developers can subscribe to.

The main differences:

  • Since the XRPL has native features/primitives (unlike EVM chains), transaction emission allows smart contracts to interact with those primitives.
  • Additional complexities due to the XRPL’s reserve system.
  • A “contract definition” object is used so that the ledger doesn’t need to store the same data repeatedly (for example, Uniswap and ERC-20 contracts are repeatedly deployed onto EVM chains, adding a lot of data bloat).
  • For example, Uniswap v2 (the exact code) is deployed more than 2,500 times across the 53 EVM chains that Etherscan tracks. This design paradigm would reduce that to 53 (one per chain).
  • The VM used is WASM, not EVM.
  • ABIs are stored on-chain.

A.3: How can I implement account logic (like in Hooks) with this form of smart contracts?

Use something akin to Ethereum’s Account Abstraction design (ERC-4337).

Might involve XLS-75d (Delegating Account Permissions).

We're also investigating whether additional Smart Features can help with this problem.

A.4: Will I be able to transfer (or copy/paste) EVM/SolVM/MoveVM/etc. bytecode to the XRPL?

No (well, not without a special tool that will do the conversion for you).

A.5: Will I be able to write smart contracts in Solidity?

Solidity will not be prioritized by our team right now, but there are Solidity-to-WASM compilers that someone could use to make this possible.

Note that the syntax would likely not be the exact same as in the EVM. In addition, the conceptual meanings may not map either - for example, addresses are different between the EVM and the XRPL.

A.6: Will I be able to implement something akin to ERC-20 with an XRPL smart contract?

Yes, but it will be more expensive and less efficient than the existing token standards (IOUs and MPTs) and won’t be integrated into the DEX or other parts of the XRPL.

An alternative strategy would be to create a smart contract that essentially acts as a wrapper to the XRPL’s native functionalities (e.g. a mint function that just issues a token via a Payment transaction).

A.7. How will fees be handled for contract-submitted transactions?

It’ll be included in the fees paid for the contract call.

A.8. What happens if a smart contract pseudo-account’s funds are clawed back, or are locked/frozen?

That’s for the smart contract author to deal with, just like in the EVM world.

A.9: What languages can/will be supported?

Any language that can compile to WASM can be supported. We will likely start with Rust.

A.10: Can a smart contract execute a multi-account Batch transaction with another account?

Yes, if the smart contract account submits the final transaction. Constructing a multi-account Batch transaction between two smart contracts will not be possible as a part of this spec. Support for that could be added with a separate design.

A.11: Can I use Smart Escrows with Smart Contracts?

Yes, a smart contract can emit an EscrowCreate transaction that has a FinishFunction.

A.12: Will this design support read-only functions like EVM?

Not in the initial version/design, to keep it simple. This could be added in the future, though.

A.13: How do I get the transaction history of a Contract Account?

Use the existing account_tx RPC.

A.14: How does this design prevent abuse/infinite loops from eating up rippled resources, while allowing for sufficient compute for smart contract developers?

The UNL-Votable Parameters section addresses this. The UNL can adjust the parameters based on the needs and limitations of the network, to ensure that developers have enough computing resources for their needs, while ensuring that they cannot overrun the network with their contracts.

A.15: Why store the ABI on-chain instead of in an Etherscan-like system?

Having the data on-chain removes the need for a centralized party to maintain this data - all the data to interact with a contract is available on-chain. This also means that it’s much easier (and therefore faster) for rippled to determine if the data passed into a contract function is valid, instead of needing to open up the WASM engine for that.

The tradeoff is that this means a contract will take up more space on-chain, and we need new STypes to store that information properly.