0101 XLS-101d: XRPL Smart Contracts #271
mvadari
started this conversation in
Standard Proposals
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
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):
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:
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:
ContractSource
,Contract
, andContractData
ContractCreate
,ContractCall
, andContractModify
contract_info
andevent_history
eventEmitted
FeeSettings
object that keeps track of that info)STParameters
,STParameterValues
, andSTData
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 objects.
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:
“Account”
field is of typeSTAccount
(which represents XRPL account IDs)“Sequence”
field is of typeSTUInt32
(which represents an unsigned 32-bit integer)“Amount”
field is of typeSTAmount
(which represents all amount types - XRP, IOUs, and MPTs)2.3. Design Overview
Contract
stores the contract infoContractSource
is an implementation detail to make code storage more efficientContractData
stores any contract-specific dataContractCreate
creates a new contract + pseudo-accountContractCall
is used to trigger the transaction and call one of its functionsContractModify
allows you to modify a contractcontract_info
RPC fetches the ABI of a contract and any other relevant information.event_history
RPC fetches the event emission history for a contract.eventEmitted
subscription allows you to subscribe to “events” emitted from a contract.2.4. Overview of Smart Contract Capabilities
init
function that runs onContractCreate
for any account setup3. Ledger Object:
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
LedgerEntryType
string
UInt16
ContractSource
).ContractHash
string
Hash256
ContractCode
string
blob
InstanceParameters
array
STParameters
Functions
array
STArray
ReferenceCount
number
UInt32
Contract
objects that are based on thisContractSource
. This object is deleted when that value goes to 0.3.1.1. Object ID
hash of prefix +
ContractHash
3.1.2.
InstanceParameters
andFunctions
3.1.3
Functions
STypes
(maybe banSTObjects
andSTArrays
and other complex STypes likeTransaction
)tfSendAmount
- if the type is anSTAmount
, then that amount will be sent to the contract from your fundstfSendNFToken
- if the type is aHash256
, then theNFToken
with that ID will be sent to the contract from your holdingstfAuthorizeTokenHolding
- if the type is anSTIssue
orSTAmount
, then you can automatically create a trustline/MPToken for that token (assuming it’s not XRP).3.2. Object Deletion
The
ContractSource
object does not have an owner.ReferenceCount
ever goes to 0ContractSource
object should exist with aReferenceCount
value of 03.3. Example Object
4. Ledger Object:
Contract
4.1. Fields
LedgerEntryType
string
UInt16
Contract
).Owner
string
AccountID
Deployer
string
AccountID
Flags
number
UInt32
ContractHash
string
Hash256
InstanceParameterValues
array
STParameterValues
URI
string
Blob
TODO: should the
URI
field live onContractSource
orContract
?4.1.1. Object ID
hash of prefix +
Owner
+ContractHash
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 oflsfImmutable
,lsfCodeImmutable
, andlsfABIImmutable
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
5. Ledger Object:
ContractData
Data is serialized using the
STData
serialization format.5.1. Fields
LedgerEntryType
string
UInt16
ContractData
).Owner
string
AccountID
ContractAccount
string
AccountID
Owner
is different).Data
string
STData
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
6. Transaction:
ContractCreate
6.1. Fields
TransactionType
string
UInt16
ContractCreate
).Account
string
AccountID
Flags
number
UInt32
ContractCode
string
Blob
ContractHash
string
Hash256
Functions
array
STArray
InstanceParameters
array
STParameters
InstanceParameterValues
array
STParameterValues
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
, andlsfABIImmutable
enabled.6.1.2.
ContractCode
andContractHash
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 matchingContractSource
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 theContractHash
had been provided instead (albeit with a more expensive fee).If
ContractCode
is provided,InstanceParameters
andFunctions
must also be provided.6.2. Fee
Similar to
AMMCreate
, 1 object reserve fee (+ fees for running theinit
code, and probably also + fees per byte of code uploaded)6.3. Failure Conditions
ContractHash
is provided but there is no existing correspondingContractSource
ledger objectContractCode
provided is invalid.Functions
doesn't match the code.InstanceParameters
don't match what's in the existingContractSource
ledger object.6.4. State Changes
If the transaction is successful:
Contract
is created.Contract
object is created.ContractSource
object already exists, theReferenceCount
will be incremented.ContractSource
object does not already exist, it will be created.6.5. Example Transaction
7. Transaction:
ContractCall
This transaction triggers a specific function in a given contract, with the provided parameters
7.1. Fields
TransactionType
string
UInt16
ContractCall
).Account
string
AccountID
ContractAccount
string
AccountID
FunctionName
string
Blob
Parameters
array
STParameterValues
7.2. Fee
The max number of instructions you’re willing to run (gas-esque behavior)
7.3. Failure Conditions
ContractAccount
doesn't exist or isn't a smart contract pseudo-account.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
8. Transaction:
ContractModify
This transaction modifies a contract's code or instance parameters, if allowed.
8.1. Fields
TransactionType
string
UInt16
ContractModify
).Account
string
AccountID
ContractAccount
string
AccountID
Flags
number
UInt32
ContractCode
string
Blob
ContractHash
string
Hash256
Functions
array
STArray
InstanceParameters
array
STParameters
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 theContractCreate
transaction8.3. Failure Conditions
ContractAccount
doesn’t exist or isn’t a contract pseudo-account.Account
isn't the contract owner.ContractAccount
isn’t specified, theAccount
isn’t a contract pseudo-account.lsfImmutable
flag.lsfABIImmutable
enabled and isn't backwards-compatible (function names or parameters are changed). (Note: functions may be added)ContractCode
orContractHash
are provided but the contract has thetfCodeImmutable
flag enabled.8.4. State Changes
If the transaction is successful:
Contract
object is updated accordingly.Contract
object was the only user of aContractSource
object, theContractSource
object is deleted.Contract
object does not have a corresponding existingContractSource
object, it is created.9. 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.
9.1.
Flags
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.9.2. Metadata
Every contract-submitted transaction will contain an extra metadata field,
ParentContractCallId
, containing the hash of theContractCall
transaction that triggered its submission.10. RPC:
contract_info
This RPC fetches info about a deployed contract.
10.1. Request Fields
contract_account
string
function
string
user_account
string
10.2. Response Fields
contract_account
string
code
string
account_info
object
account_info
output of thecontract_account
.functions
array
source_code_uri
string
contract_data
object
user_data
user_account
is included in the requestobject
10.2.1.
functions
Each object in the array will contain the following fields:
name
string
parameters
array
fees
string
11. RPC Subscription:
eventEmitted
Subscribe to events emitted from a contract.
11.1. Request Fields
contract_account
string
events
array
array
ofstring
s. 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
?11.2. Response Fields
contract_account
string
events
array
hash
string
ledger_index
number
11.2.1.
events
Each object in the
events
array will contain the following fields:name
string
data
object
The rest of the fields in this object will be dev-defined fields from the emitted event.
12. RPC Subscription:
event_history
Fetch a list of historical events emitted from a given contract account.
12.1. Request Fields
contract_account
string
events
array
array
ofstring
s. If omitted, all events from the contract will be retrieved.ledger_index_min
number
-1
instructs the server to use the earliest validated ledger version available.ledger_index_max
number
-1
instructs the server to use the most recent validated ledger version available.ledger_index
LedgerIndex
ledger_hash
string
binary
boolean
false
. If set totrue
, returns events as hex strings instead of JSON.limit
number
marker
any
transactions
boolean
false
. If set totrue
, returns the whole transaction in addition to the event. If set to falls, returns only the transaction hash.12.2. Response Fields
contract_account
string
events
array
ledger_index_min
number
ledger_index_max
number
limit
number
marker
any
12.2.1.
events
Each object in the
events
array will contain the following fields:name
string
data
object
binary
is set tofalse
.data_blob
string
binary
is set totrue
.tx_json
object
transactions
is set totrue
andbinary
is set tofalse
.tx_blob
string
transactions
is set totrue
andbinary
is set totrue
.tx_hash
string
transactions
is set tofalse
.validated
boolean
12.3. Implementation Details
This RPC will use the account transactions database. It will iterate through all
ContractCall
transactions sent to the providedcontract_account
and filter out the events based on the other provided parameters.13. UNL-Votable Parameters
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.
14. Serialized Type:
STParameters
This object is essentially just a list of
(Flag, SType value)
groupings.SType
to describe a parameterUInt32
for flagsUInt16
for STypeJSON representation is a name/number and the string names of the STypes - for example:
15. 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:
JSON representation is a name/number and the string representation of the values - for example:
16. Serialized Type:
STData
The following, serialized one after another:
SType
if needed? Or just a number that the ABI defines?)STJson
instead and just handle JSON data?JSON representation is a dictionary with the key-value pairs - for example:
17. Examples
17.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.
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 metadata18. Invariants
ContractSource
object should have aReferenceCount
of 0Contract
object should have an existing correspondingContractSource
objectContract
cannot have bothlsfImmutable
andlsfCodeImmutable
enabled.19. Security
19.1. Pseudo-Account Account-Level Permissions
These settings will all be enabled by default on a pseudo-account:
DepositAuth
These functions will all be disallowed from pseudo-accounts:
SignerListSet
SetRegularKey
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.19.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.
19.3. Fee-Scavenging/Dust Attacks
Don’t think this is possible here.
19.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
ContractDelete
transaction? Maybe it can be done withContractModify
?ContractDelete
is cleaner/easier to useSTData
?STData
typeContractData
objects be stored in a separate directory structure (e.g. a new one, not the standard owner directory)?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 objects 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 theContract
/ContractSource
ledger object, 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):
ContractCreate
fee and there's a hard cap. Higher fees for more reserves.Contract ID
+Account
, only one object stores all of a user’s dataThe 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 main differences:
ContractCall
transaction to trigger the smart contract.A.2: How does this compare to EVM?
The main similarities:
The main differences:
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 aPayment
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 aFinishFunction
.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.
Beta Was this translation helpful? Give feedback.
All reactions