Trust lines are a mechanism that enables the XRP Ledger to support user-issued IOUs. They represent bilateral relationships between accounts that establish trust limits and govern the flow of value for specific IOUs.
Think of a trust line as a credit agreement: it defines how much of a particular IOU one account holds from an issuer, along with the terms of that relationship.
For example, suppose Alice is an issuer and Bob wants to hold USD issued by her. Bob would create a trust line, specifying USD as the currency code and Alice's address as the issuer.
Alice can now send USD to Bob.
The IOU is identified by currency code USD and Alice's address. The balance between Alice and Bob is stored on the trust line.
If Alice sent 10 USD to Bob, her balance would be -10, while Bob's balance would be 10. Please note that rippled implementation may not store the balance as such, but the user will see it that way.
Alice can keep issuing the same IOU to other parties, and her total balance would be the sum of her balance on each of the trust lines.
If Alice's account contains RequireAuth flag, then her trust lines have to be authorized. This means that, after Bob creates a trust line, Alice has to authorize it before Bob can receive any IOUs on that trust line. Alice authorizes the trust line by sending a TrustSet transaction with the tfSetfAuth flag, specifying Bob's address in the LimitAmount.issuer field. Alice may choose to freeze or deep freeze the trust line. She can also clawback IOUs from a trust line.
Trust lines have a concept of QualityIn and QualityOut. This is covered in Cross Currency Payments section and trust line quality in DirectStepI. For the sake of manipulating RippleState ledger entry, it is important to know that a quality of 1,000,000,000 is the default quality (QUALITY_ONE). This represents a 1:1 transfer rate, meaning the full amount is transferred without adjustment during cross-currency payments through this trust line.
Trust line limits define the maximum amount of an IOU an account is willing to hold. The LimitAmount field in the TrustSet transaction specifies this maximum. A limit of 0 means the account will not accept any incoming IOUs on that trust line. Trust line limits are soft limits - they can be exceeded during offer crossing, as creating an offer is considered explicit consent to receive IOUs.
See DirectIOfferCrossingStep for implementation details.
Payments on the XRP Ledger often need to flow through intermediate accounts to reach the destination. For example, if Alice wants to pay Bob in USD and both hold trust lines to the same issuer, the payment flows through the issuer: Alice's balance on her trust line with Issuer decreases, and Bob's balance on his trust line with Issuer increases. When an account other than the issuer sits between two trust lines for the same currency, the payment can also flow through that account, entering on one trust line and exiting on another. This is called rippling.
The NoRipple flag (lsfLowNoRipple / lsfHighNoRipple) is a per-account, per-trust-line flag that controls whether a trust line can be used for rippling. A payment is blocked from rippling through an account only when that account has NoRipple set on both the trust line the payment enters on and the trust line it exits on. If the account has NoRipple cleared on at least one of the two trust lines, the payment can flow through.
An issuer who sets DefaultRipple (lsfDefaultRipple) on their account will have NoRipple cleared on their side of every new trust line. Since the issuer's side is always clear, the both-sides condition can never be met, and payments can always ripple through the issuer. A regular holder who does not set DefaultRipple will have NoRipple set on their side of every new trust line. If the holder has two trust lines for the same currency (e.g., USD.IssuerA and USD.IssuerB), both will have NoRipple set on the holder's side, so the both-sides condition is met and payments cannot ripple through the holder's account. This protects the holder from having their balance used as a pass-through without their consent.
NoRipple is checked during both path finding and payment execution. Path finding uses NoRipple as a heuristic filter to avoid exploring paths that would be rejected. The flow engine enforces it as a hard constraint, failing the strand with terNO_RIPPLE when violated. See trust line creation for how NoRipple flags are initialized.
A trust line is in default state when both accounts have all their parameters set to default values. The default state is important because:
- Trust lines in default state are automatically deleted to reduce ledger bloat
- Default state determines whether an account must pay a reserve for its side of the trust line
- Attempting to create a trust line in default state fails with
tecNO_LINE_REDUNDANT
Default values for an account's side of a trust line:
- QualityIn: 0 or absent (equivalent to QUALITY_ONE = 1,000,000,000)
- QualityOut: 0 or absent (equivalent to QUALITY_ONE = 1,000,000,000)
- NoRipple flag: set if the account does not have
lsfDefaultRipple; cleared if it does - Freeze flag: not set
- Limit: 0
- Balance: 0 or negative from the account's perspective (meaning the account owes IOUs rather than holds them)
Both the low and high accounts must have their parameters in the default state for the trust line to be considered in the default state and eligible for deletion.
Issuer vs Holder perspective:
- Issuer (negative balance): Has limit = 0, balance <= 0. The issuer's side is typically in default state when no IOUs have been redeemed yet.
- Holder (positive balance): Has limit > 0, balance >= 0. The holder's side requires a non-zero limit to receive IOUs, so it's not in default state while the trust line is usable.
When a TrustSet transaction is processed and the trust line would transition to default state, the RippleState object is deleted, and both accounts' owner directories are updated to remove the trust line entries.
graph LR
A[Account A<br/>Low Account]
B[Account B<br/>High Account]
R[RippleState<br/>Currency: USD<br/>Balance: -10]
A -->|lowLimit<br/>lowQualityIn/Out<br/>LowNode| R
B -->|highLimit<br/>highQualityIn/Out<br/>HighNode| R
R -.->|Balance: -10<br/>A owes B<br/>A is issuer| A
R -.->|Balance: +10<br/>B holds IOUs<br/>B is holder| B
A single RippleState ledger entry represents the trust line relationship between two accounts for a specific currency code. RippleState stores two account IDs in canonical order: the first is always the account ID sorted lower (the low account), and the second is the account ID sorted higher (the high account). Please see xrpl.org.
Storage vs User Perspective:
- What's stored: One
RippleStateentry with a singlesfBalancefield from the low account's perspective - What users see: Each account sees their own balance on the trust line, which is the inverse of their peer's balance
- Example: If
sfBalance = -10(stored), low account sees-10and high account sees+10
Issuer is not defined by RippleState. The issuer is conceptually the entity that created (issued) the IOU, but nothing in RippleState stores that information explicitly. The issuer is determined by the balance direction: the account with a negative balance (owing IOUs) is issuing, while the account with a positive balance is holding (has
redeemed IOUs). When the balance is 0, either party can potentially issue IOUs, limited by the other party's trust line limit. The flow engine determines the issuer contextually based on the debt direction during payment execution. See DirectStepI debt direction for details.
The sfBalance field in RippleState is stored from the low account's perspective:
- Positive balance = Low account holds currency (owes nothing, has credit)
- Negative balance = High account holds currency (low account owes money)
The key of the RippleState object is the result
of SHA512-Half of the following values
concatenated in order:
- The
RippleStatespace key0x0072(lowercaser) - The
AccountIDof the low account. - The
AccountIDof the high account. - The 160-bit
Currencycode.
Low account is the account with a lower ID than the high account, ensuring that a trust line between any two
accounts is always represented by the same RippleState.
Please see RippleState Fields
Please see RippleState Flags
Trust lines can be created or modified with certain pseudo-accounts as the destination:
- AMM accounts (has
sfAMMID): Can create new trust lines for the AMM's LP token, or modify existing trust lines - Vault accounts (has
sfVaultID): Can only modify existing trust lines, cannot create new ones - Other pseudo-accounts: Cannot create or modify trust lines (fails with
tecPSEUDO_ACCOUNT)
The TrustSet transaction never creates, deletes, or modifies the pseudo-account itself - it only creates or modifies
the RippleState entry that represents the trust line relationship.
A RippleState ledger entry is jointly owned by both participating accounts. When a trust line is created, it is added to the owner directory of both the low account and the high account.
The RootIndex of each account's owner DirectoryNode is the result of SHA512-Half of the following values concatenated in order:
- The
OwnerDirectoryspace key0x004F(uppercaseO) - The
AccountIDof the account
The sfLowNode and sfHighNode fields in the RippleState entry store the directory page indices where the trust line appears in each account's owner directory. These serve as deletion hints for efficient removal when the trust line is deleted.
An account only pays a reserve for a trust line when the account owns more than two items total.
An account's side of a trust line requires a reserve when any of the following are in a non-default state:
- QualityIn is set (non-zero)
- QualityOut is set (non-zero)
- NoRipple flag differs from the account's default ripple setting
- Freeze flag is set
- Limit is non-zero
- Balance is positive (account holds IOUs)
When a trust line side transitions from default to non-default state, the lsfLowReserve or lsfHighReserve flag is
set and the account's OwnerCount is incremented. When all parameters return to default state, the reserve flag is
cleared and OwnerCount is decremented.
The TrustSet transaction creates, modifies or deletes a RippleState ledger entry and manages the corresponding DirectoryNode entries.
When a trust line is created or deleted, it affects the owner directories of both participating accounts. Each account maintains an owner directory that tracks all ledger objects it owns, including its trust lines. The sfLowNode and sfHighNode fields in the RippleState entry store the page numbers where the trust line appears in each account's owner directory
| Field Name | Required? | Modifiable? | JSON Type | Internal Type | Default Value | Description |
|---|---|---|---|---|---|---|
TransactionType |
:heavy_checkmark: | No |
String |
UINT16 |
The transaction type. Must be TrustSet |
|
LimitAmount |
:heavy_checkmark: | Yes |
Object |
Amount |
Object defining the currency and the peer account when manipulating a trust line | |
QualityIn |
Yes |
Number |
UINT32 |
None |
Quality ratio applied when receiving IOUs on this trust line. When absent or 0, defaults to QUALITY_ONE (1,000,000,000 = 1:1 ratio) | |
QualityOut |
Yes |
Number |
UINT32 |
None |
Quality ratio applied when sending IOUs on this trust line. When absent or 0, defaults to QUALITY_ONE (1,000,000,000 = 1:1 ratio) | |
Flags |
Yes |
String |
UINT32 |
0 |
The LimitAmount field specifies the maximum amount of the IOU the account is willing to hold. It is a composite field containing the currency code, the peer account, and the limit value. Setting the value to 0 means the account will not accept any incoming IOUs on that trust line.
Counterintuitively, LimitAmount.issuer is not always the issuer account, even in the business logic sense. For
example, if the issuer wants to authorize a trust line, they will send their account id in account and holder's
account id as the issuer.
For flags, please refer to TrustSet Flags.
Terminology in this document
- Source account is the account specified as
accountin the transaction. - Destination account is the account specified as
issuerfield inLimitAmount.
Static validation:
temINVALID_FLAG: one of the specified flags is not one of flags.temINVALID_FLAG: flags containtfSetDeepFreezeortfClearDeepFreezeand DeepFreeze amendment is not enabled.temBAD_AMOUNT:LimitAmountis XRP and mantissa is bigger than100000000000000000ull.temBAD_LIMIT:LimitAmountis XRP.temBAD_CURRENCY:currencyfield inLimitAmountisXRP.temBAD_LIMIT:valuefield inLimitAmountis less than0.temDST_NEEDED:issuerfield inLimitAmountis not specified or it represents a noAccount.
Validation against the ledger view:
terNO_ACCOUNT: source account does not exist.tefNO_AUTH_REQUIRED: source account does not have alsfRequireAuthflag set, but the transaction containstfSetfAuthflag.temDST_IS_SRC: fixTrustLinesToSelf amendment is enabled, or if the trust line does not already exist and the source account and destination account are the same.tecNO_DST: one of DisallowIncoming, AMM or SingleAssetVault amendments are enabled and the issuer account does not exist.tecNO_PERMISSION: DisallowIncoming is enabled and the destination account haslsfDisallowIncomingTrustlineflag:- If the trust line was already created for a destination with
lsfDisallowIncomingTrustlineand amendment fixDisallowIncomingV1 was enabled, do not fail.
- If the trust line was already created for a destination with
- If the destination account is a pseudo-account:
sfAMMID: destination is an AMM account (hassfAMMIDfield), but the trust line does not already exist between source and AMM. -tecAMM_EMPTY: AMM has zero LP IOUs - cannot create trust lines to empty AMMs. -tecNO_PERMISSION: currency in the trust line request does not match the AMM's LP token currency. -tecINTERNAL: AMM ledger entry cannot be found.tecNO_PERMISSION: destination is a Vault account (hassfVaultIDfield) and the trust line does not already exist.tecPSEUDO_ACCOUNT: destination is any other type of pseudo-account.
- If DeepFreeze amendment is enabled, validate freeze flag combinations:
tecNO_PERMISSION: source account haslsfNoFreezeflag set and the transaction containstfSetFreezeortfSetDeepFreezeflags.tecNO_PERMISSION: transaction contains both freeze flags (tfSetFreezeortfSetDeepFreeze) and unfreeze flags (tfClearFreezeortfClearDeepFreeze) - user should not be able to send conflictingsetandclearflags in the same instructions.tecNO_PERMISSION: transaction results in a trust line having a deep freeze set but a normal freeze cleared - user should not be able to deep freeze a trust line that is not in frozen state.
Validation during doApply:
tefINTERNAL: source account does not exist.tecNO_DST: destination account does not exist.tecNO_PERMISSION: the user is trying to settfSetNoRippleand the source account's balance on the trust line is negative.tecINSUF_RESERVE_LINE: user does not have enough balance to cover the reserve and wants to modify an existing trust line, regardless of whether they or the counterparty have created the original trust line.tecNO_LINE_INSUF_RESERVE: user does not have enough balance to cover the reserve and wants to create a new trust line.tecNO_LINE_REDUNDANT: trust line does not already exist, amount is0, andQualityInandQualityOutare either not set, or set to their default value (1,000,000,000), and iftfSetfAuthflag is not set.
-
RippleStateobject is deleted if an existing trust line exists when sendingTrustSettransaction and:- If fixTrustLinesToSelf is not enabled and source and destination accounts are the same (legacy cleanup for two historical self-referential trust lines).
- If the trust line is in its default state after updating it.
- If the currency code of the IOU is
XRP. - When deleted, the trust line is removed from both accounts' owner directories:
- Removed from low account's owner directory via
dirRemoveusing thesfLowNodepage number. - Removed from high account's owner directory via
dirRemoveusing thesfHighNodepage number. - If removing the trust line empties a non-root directory page, that page is deleted and the directory chain is repaired.
- If either removal fails, the transaction returns
tefBAD_LEDGER.
- Removed from low account's owner directory via
-
RippleStateobject is modified:- If
QualityInfield was specified in the transaction:- If
QualityIn!=1,000,000,000, set the appropriate quality field (sfLowQualityInfor low source account,sfHighQualityInfor high source account) to theQualityInvalue - If
QualityIn=1,000,000,000, clear the appropriate quality field (sfLowQualityInfor low source account,sfHighQualityInfor high source account)
- If
- If
QualityOutfield was specified in the transaction:- If
QualityOut!=1,000,000,000, set the appropriate quality field (sfLowQualityOutfor low source account,sfHighQualityOutfor high source account) to theQualityOutvalue - If
QualityOut=1,000,000,000, clear the appropriate quality field (sfLowQualityOutfor low source account,sfHighQualityOutfor high source account)
- If
- If the transaction contains
tfSetNoRippleflag and nottfClearNoRippleflag:- If the source account's balance on the trust line is non-negative, set the appropriate NoRipple flag (
lsfLowNoRipplefor low account,lsfHighNoRipplefor high account) - If the transaction contains
tfClearNoRippleflag and nottfSetNoRippleflag, clear the appropriate NoRipple flag (lsfLowNoRipplefor low account,lsfHighNoRipplefor high account)
- If the source account's balance on the trust line is non-negative, set the appropriate NoRipple flag (
- If the transaction contains
tfSetfAuthflag setlsfLowAuthfor low source account andlsfHighAuthfor high source account. - Note: if, after applying the above modifications, the trust line is in its default state, it is deleted rather than kept as modified (see deletion conditions above)1.
- If account's parameters in a trust line change to non-default values such that it requires reserve but did not
before:
- Set appropriate
lsfLowReserveorlsfHighReserveflag
- Set appropriate
- If account no longer requires reserve because its values in a trust line are now default values:
- Clear appropriate
lsfLowReserveorlsfHighReserveflag
- Clear appropriate
- If flags are provided, they will modify the currently stored flags value.
- If
-
RippleStateobject is created:- If an existing trust line for the same IOU does not exist between the two accounts.
- When created, the trust line is added to both accounts' owner directories:
- Added to low account's owner directory via
dirInsert. The page number is stored insfLowNode. - Added to high account's owner directory via
dirInsert. The page number is stored insfHighNode. - If either directory is full, the transaction fails with
tecDIR_FULL.
- Added to low account's owner directory via
- NoRipple flags are initialized on both sides of the trust line2:
- The source account's NoRipple flag (
lsfLowNoRippleorlsfHighNoRipple) is set if the TrustSet transaction containstfSetNoRippleand nottfClearNoRipple3. - The destination account's NoRipple flag is set if the destination account does not have
lsfDefaultRippleon their account4.lsfDefaultRippleis an account-level flag set via AccountSet (asfDefaultRipple). When an issuer setslsfDefaultRipple, new trust lines are created without NoRipple on the issuer's side, allowing rippling by default.
- The source account's NoRipple flag (
-
DirectoryNodeobject is created or modified:- When a trust line is created, it is added to both participating accounts' owner directories.
- If an owner directory page is full (32 entries), a new page is created and linked to the directory chain.
- If creating a new page would exceed 262,144 pages, the transaction fails with
tecDIR_FULL.
-
AccountRootobject is modified:- If account now requires reserve but did not before:
- Increment
sfOwnerCountby 1, without overflowingstd::uint32_t.
- Increment
- If account no longer requires reserve:
- Decrement
sfOwnerCountby 1, without going below0.
- Decrement
- If account now requires reserve but did not before:
Design note - in rippled, the same transactor implementation of Clawback is used to clawback
both IOUs and MPTs. rippled's Clawback implementation of
Transactor uses an adaptation of a visitor pattern. Any transaction containing an MPT in Amount field will visit
MPTIssue implementation, while transactions referring to IOUs in Amount field will visit Issue implementation.
This allows for the code to be sufficiently separated that even when describing rippled implementation of Clawback we
can discuss two implementations separately.
Transaction fields are described in Clawback Fields.
Terminology in this document
- Issuer is the account specified as
accountin the transaction. - Holder is the account specified, counterintuitively, as
issuerfield inAmount. This is a commonly used pattern to useissuerto denote a peer's account.
Static validation
temINVALID_FLAG: any flags, other than universal transaction flags, are specified.temMALFORMED:- For IOU clawback: user provided
Holderfield in the transaction (IOUs useAmount.issuerto specify the holder). - For MPT clawback: user did NOT provide
Holderfield in the transaction (MPTs require theHolderfield).
- For IOU clawback: user provided
temBAD_AMOUNT:- issuer and holder are the same account.
Amountprovided is XRP.Amountis not bigger than0.
Validation against the ledger view
terNO_ACCOUNT: issuer's or holder's account does not exist.tecAMM_ACCOUNT: If holder's account is an AMM account, such as a SingleAssetVault or AMM account and if amendment SingleAssetVault is not enabled.tecPSEUDO_ACCOUNT: IfSingleAssetVaultis enabled and holder's account is any pseudo-account.tecNO_PERMISSION: issuer does not havelsfAllowTrustLineClawbackor it does havelsfNoFreeze. Only trust lines of issuers that can be frozen and that allow trust line clawback can be clawed back.tecNO_LINE: there is no existing trust line between issuer and holder for the specified currency.tecNO_PERMISSION: the account specified asaccount(issuer) is not actually the issuer based on the trust line balance. The true issuer is the account with the negative balance (owing IOUs), not the positive balance (holding IOUs). This error occurs when the transaction sender has the accounts reversed.tecINSUFFICIENT_FUNDS: holder's balance is0.
-
RippleStateobject is modified:- Amount specified in
Amountfield is transferred from holder to issuer on the trust line for specified currency code. This is done by subtracting the amount from holder's account and adding it to issuer's account. No limits or fees are enforced.- If the
amountis bigger than the available balance, only the available balance is moved.
- If the
- If account no longer requires reserve because its values in a trust line are now default values after the
transfer:
- Clear appropriate
lsfLowReserveorlsfHighReserveflag - Adjust owner count for the account
- Clear appropriate
- Amount specified in
-
RippleStateobject is deleted:- If, after clawing back, the trust line is in its default state,
RippleStateis deleted. - When deleted, the trust line is removed from both accounts' owner directories:
- Removed from low account's owner directory via
dirRemoveusing thesfLowNodepage number. - Removed from high account's owner directory via
dirRemoveusing thesfHighNodepage number. - If removing the trust line empties a non-root directory page, that page is deleted and the directory chain is repaired.
- Removed from low account's owner directory via
- If, after clawing back, the trust line is in its default state,
-
AccountRootobject is modified (whenRippleStateis deleted):- If
lsfLowReserveflag was set on the deleted trust line: decrement low account'ssfOwnerCountby 1 - If
lsfHighReserveflag was set on the deleted trust line: decrement high account'ssfOwnerCountby 1 - Note: Due to the "first two items are free" policy (see Reserves), an account may not have a reserve flag set even though the trust line exists, so OwnerCount may not be decremented for both accounts.
- If