|
| 1 | +--- |
| 2 | +title: Fee Abstraction |
| 3 | +--- |
| 4 | + |
| 5 | +[Source Code](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/fee-abstraction) |
| 6 | + |
| 7 | +Fee abstraction enables users to pay for Stellar transactions with tokens (e.g., USDC) instead of native XLM. A relayer covers the XLM network fees and is compensated in the user's chosen token through an intermediary contract. |
| 8 | + |
| 9 | +## Overview |
| 10 | + |
| 11 | +The [fee-abstraction](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/fee-abstraction) package provides utilities for implementing fee forwarding contracts on Stellar. The system works by having a relayer submit transactions on behalf of users, then atomically collecting token payment from the user as compensation. |
| 12 | + |
| 13 | +The flow involves an off-chain negotiation between the user and a relayer (quote request, fee agreement), but the actual execution happens through an intermediary contract called a **FeeForwarder**. This contract enforces that the user is charged at most `max_fee_amount`, the cap they signed. The relayer determines the actual `fee_amount` at submission time based on network conditions, but can never exceed the user's authorized maximum. |
| 14 | + |
| 15 | +### Benefits |
| 16 | + |
| 17 | +Fee abstraction provides different advantages depending on the account type. For Stellar classic accounts, users don't need to acquire XLM beyond the [minimum required account balance](https://developers.stellar.org/docs/learn/fundamentals/stellar-data-structures/accounts#base-reserves-and-subentries) just to transact. It’s a great UX improvement by allowing users to use dapps with stablecoins or other tokens they already hold. |
| 18 | + |
| 19 | +Unlike classic accounts, smart accounts (contract-based accounts) don't need to maintain a minimum XLM balance, but they need a mechanism to compensate relayers who submit transactions on their behalf. The proposed fee abstraction model significantly improves smart accounts usability. |
| 20 | + |
| 21 | +### How It Works |
| 22 | + |
| 23 | +```mermaid |
| 24 | +sequenceDiagram |
| 25 | + actor User |
| 26 | + actor Relayer |
| 27 | + participant FeeForwarder |
| 28 | + participant Token |
| 29 | + participant Target Contract |
| 30 | +
|
| 31 | + User->>User: 1. Prepare call to Target.target_fn() |
| 32 | + User->>Relayer: 2. Request quote (fee token, expiration, target) |
| 33 | + Relayer-->>User: Quote: max_fee_amount |
| 34 | + |
| 35 | + User->>User: 3. Sign authorization for FeeForwarder.forward()<br/>including subinvocations for:<br/> Token.approve() and Target.target_fn() |
| 36 | + User->>Relayer: 4. Hand over signed authorization entry |
| 37 | + |
| 38 | + Relayer->>Relayer: 5. Verify params satisfy requirements<br/>(fee amount, token, fee recipient, ...) |
| 39 | + Relayer->>Relayer: 6. Sign as source_account |
| 40 | + Relayer->>FeeForwarder: 7. Execute forward():<br/>submit a tx and pay XLM fees |
| 41 | + |
| 42 | + FeeForwarder->>FeeForwarder: 8. Validate authorizations |
| 43 | + FeeForwarder->>Token: 9. Approve max fee amount (optional) |
| 44 | + FeeForwarder->>Token: 10. Transfer fee amount to fee recipient |
| 45 | + FeeForwarder->>Target Contract: 11. Invoke target_fn(target_args) |
| 46 | + Target Contract-->>User: Result |
| 47 | +``` |
| 48 | + |
| 49 | +## Features |
| 50 | + |
| 51 | +### Target Invocation (Forwarding) and Fee Collection |
| 52 | + |
| 53 | +First and foremost, the user intention is to invoke a function on a target contract which might require an authorization from the user. In that case, the minimal authorization tree the user needs to sign is: |
| 54 | + |
| 55 | +``` |
| 56 | +FeeForwarder.forward() |
| 57 | + └─ Target.target_fn() ← nested sub-invocation (if needed) |
| 58 | +``` |
| 59 | + |
| 60 | +Equally important for the fee abstraction model is enabling the relayer to collect the compensation fee. This is done through the common 2-step fungible token flow of `approve` + `transfer_from`. Soroban authorization framework allows embedding the approval step in the same transaction, so the user might also need to sign `Token.approve()`. |
| 61 | + |
| 62 | +To guarantee the atomicity of both, the target invocation and the fee collection, the [fee-abstraction](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/fee-abstraction) package exposes the function `collect_fee_and_invoke()`, which, in most of the cases, will require signing an authorization tree that contains one entry with two sub-invocations: |
| 63 | + |
| 64 | +``` |
| 65 | +FeeForwarder.forward() |
| 66 | + └─ Token.approve() |
| 67 | + └─ Target.target_fn() |
| 68 | +``` |
| 69 | + |
| 70 | +The approval sub-invocation might be optional depending on the chosen approval strategy. The package supports two approval strategies: |
| 71 | + |
| 72 | +1. **Lazy** |
| 73 | + |
| 74 | + The user pre-approves a lump sum to the FeeForwarder contract in a separate transaction. Each forwarded call then draws from this existing allowance. This strategy is more resource efficient per call, but requires an initial approval transaction and trust in the FeeForwarder contract. |
| 75 | + |
| 76 | +2. **Eager** |
| 77 | + |
| 78 | + The approval is embedded directly in the authorization entry as a sub-invocation for every call, no preliminary transactions needed. This strategy is self-contained with no trust assumptions, but more expensive per call (~25%) due to the nested approval. |
| 79 | + |
| 80 | +## Fee Token Allowlist |
| 81 | + |
| 82 | +The package includes optional allowlist functionality to restrict which tokens can be used for fee payment: |
| 83 | + |
| 84 | +```rust |
| 85 | +// Enable a token for fee payments |
| 86 | +set_allowed_fee_token(e, &usdc_token, true); |
| 87 | + |
| 88 | +// Disable a token |
| 89 | +set_allowed_fee_token(e, &deprecated_token, false); |
| 90 | + |
| 91 | +// Check if a token is allowed |
| 92 | +let is_allowed = is_allowed_fee_token(e, &token); |
| 93 | +``` |
| 94 | + |
| 95 | +The allowlist uses a swap-and-pop algorithm for efficient O(1) removal and automatically extends TTL for frequently-used tokens. |
| 96 | + |
| 97 | +## Token Sweeping |
| 98 | + |
| 99 | +For permissioned implementations where fees accumulate in the contract, the package provides a sweep function: |
| 100 | + |
| 101 | +```rust |
| 102 | +// Transfer all accumulated tokens to a recipient |
| 103 | +let amount = sweep_token(e, &fee_token, &treasury); |
| 104 | +``` |
| 105 | + |
| 106 | +This is typically restricted to manager roles. |
| 107 | + |
| 108 | +## Security Considerations |
| 109 | + |
| 110 | +### Relayer Safety |
| 111 | + |
| 112 | +Relayers **must** ensure the call to the target contract is safe before submitting a transaction. A malicious user could craft a call that harms the relayer or the FeeForwarder contract in a permissioned setup. In most cases, relayers should **simulate the transaction off-chain** before submission to verify: |
| 113 | +- The target contract call will succeed |
| 114 | +- The fee collection will complete |
| 115 | +- The overall transaction outcome is acceptable |
| 116 | + |
| 117 | +This off-chain task is critical for relayer protection and should be part of any production relayer implementation. |
| 118 | + |
| 119 | +## Implementation Examples |
| 120 | + |
| 121 | +### Permissioned FeeForwarder |
| 122 | + |
| 123 | +A role-based implementation where only authorized executors can relay transactions. Fees accumulate in the contract for later distribution. |
| 124 | + |
| 125 | +**Characteristics:** |
| 126 | +- Uses `#[only_role]` macro for executor authorization |
| 127 | +- Collects fees to the contract (not directly to relayer) |
| 128 | +- Managers can sweep accumulated fees to designated recipients |
| 129 | +- Uses lazy approval strategy (requires pre-approval transaction) |
| 130 | +- Suitable for trusted relayer networks |
| 131 | + |
| 132 | +```rust |
| 133 | +#[only_role(relayer, "executor")] |
| 134 | +pub fn forward( |
| 135 | + e: &Env, |
| 136 | + fee_token: Address, |
| 137 | + fee_amount: i128, |
| 138 | + max_fee_amount: i128, |
| 139 | + expiration_ledger: u32, |
| 140 | + target_contract: Address, |
| 141 | + target_fn: Symbol, |
| 142 | + target_args: Vec<Val>, |
| 143 | + user: Address, |
| 144 | + relayer: Address, |
| 145 | +) -> Val { |
| 146 | + collect_fee_then_invoke( |
| 147 | + e, |
| 148 | + &fee_token, |
| 149 | + fee_amount, |
| 150 | + max_fee_amount, |
| 151 | + expiration_ledger, |
| 152 | + &target_contract, |
| 153 | + &target_fn, |
| 154 | + &target_args, |
| 155 | + &user, |
| 156 | + &e.current_contract_address(), // fees go to contract |
| 157 | + FeeAbstractionApproval::Lazy, |
| 158 | + ) |
| 159 | +} |
| 160 | +``` |
| 161 | + |
| 162 | +### Permissionless FeeForwarder |
| 163 | + |
| 164 | +An open implementation where anyone can act as a relayer. Fees go directly to the relayer, creating a competitive market. |
| 165 | + |
| 166 | +**Characteristics:** |
| 167 | +- No role restrictions |
| 168 | +- Fees transfer directly to the relayer |
| 169 | +- Uses eager approval strategy (approval embedded in each call) |
| 170 | +- Suitable for open relayer markets |
| 171 | + |
| 172 | +```rust |
| 173 | +pub fn forward( |
| 174 | + e: &Env, |
| 175 | + fee_token: Address, |
| 176 | + fee_amount: i128, |
| 177 | + max_fee_amount: i128, |
| 178 | + expiration_ledger: u32, |
| 179 | + target_contract: Address, |
| 180 | + target_fn: Symbol, |
| 181 | + target_args: Vec<Val>, |
| 182 | + user: Address, |
| 183 | + relayer: Address, |
| 184 | +) -> Val { |
| 185 | + relayer.require_auth(); |
| 186 | + |
| 187 | + collect_fee_and_invoke( |
| 188 | + e, |
| 189 | + &fee_token, |
| 190 | + fee_amount, |
| 191 | + max_fee_amount, |
| 192 | + expiration_ledger, |
| 193 | + &target_contract, |
| 194 | + &target_fn, |
| 195 | + &target_args, |
| 196 | + &user, |
| 197 | + &relayer, // fees go directly to relayer |
| 198 | + FeeAbstractionApproval::Eager, |
| 199 | + ) |
| 200 | +} |
| 201 | +``` |
| 202 | + |
| 203 | +## Integration with OpenZeppelin Relayer |
| 204 | + |
| 205 | +The fee abstraction package is designed to work seamlessly with [OpenZeppelin Relayer](/relayer/1.3.x). The Relayer can: |
| 206 | + |
| 207 | +1. **Submit sponsored transactions**: Pay XLM fees on behalf of users |
| 208 | +2. **Calculate optimal fees**: Determine the actual fee based on network conditions |
| 209 | +3. **Handle fee collection**: Coordinate with fee forwarder contracts to collect token payment |
| 210 | + |
| 211 | +### Sponsored Transaction Flow |
| 212 | + |
| 213 | +```mermaid |
| 214 | +sequenceDiagram |
| 215 | + participant User |
| 216 | + participant OZ Relayer |
| 217 | + participant Forwarder as FeeForwarder |
| 218 | + participant Target as Target Contract |
| 219 | +
|
| 220 | + User->>OZ Relayer: Request transaction sponsorship |
| 221 | + OZ Relayer->>OZ Relayer: Calculate fee quote |
| 222 | + OZ Relayer-->>User: Return fee estimate |
| 223 | + User->>User: Sign authorization |
| 224 | + User->>OZ Relayer: Submit signed authorization |
| 225 | + OZ Relayer->>Forwarder: Execute forward (pays XLM) |
| 226 | + Forwarder->>Target: Invoke user's action |
| 227 | + Forwarder->>OZ Relayer: Transfer token fee |
| 228 | + OZ Relayer-->>User: Transaction complete |
| 229 | +``` |
| 230 | + |
| 231 | +For detailed integration instructions, see the [Stellar Sponsored Transactions Guide](/relayer/1.3.x/guides/stellar-sponsored-transactions-guide). |
| 232 | + |
| 233 | +## See Also |
| 234 | + |
| 235 | +- [Smart Accounts](/stellar-contracts/accounts/smart-account) |
| 236 | +- [Access Control](/stellar-contracts/access/access-control) |
| 237 | +- [OpenZeppelin Relayer](/relayer/1.3.x) |
| 238 | +- [Stellar Sponsored Transactions Guide](/relayer/1.3.x/guides/stellar-sponsored-transactions-guide) |
0 commit comments