stellar_fee_abstraction/lib.rs
1//! # Fee Abstraction Module
2//!
3//! This module provides utilities for implementing fee abstraction in Stellar
4//! contracts, allowing users to pay transaction fees in tokens instead of
5//! native XLM.
6//!
7//! # Core Features
8//!
9//! - **Target invocation and fee collection** helper
10//! - **Fee Token Allowlist**: Optional allowlist for accepted fee tokens
11//! - **Token Sweeping**: Optional functions to collect accumulated fees
12//! - **Fee Validation**: Utilities for validating fee amounts
13//! - **Approval strategies**: utilities for collecting fee from users support
14//! two approval semantics:
15//! - [`FeeAbstractionApproval::Eager`]: always approve `max_fee_amount`
16//! (overwriting any existing allowance)
17//! - [`FeeAbstractionApproval::Lazy`]: only approve if the current allowance
18//! is less than `max_fee_amount`
19//!
20//! # Usage
21//!
22//! This module provides storage functions and event helpers that can be
23//! integrated into a fee forwarding contract. The implementing contract is
24//! responsible for the authorization checks and who can manage fee tokens or
25//! sweep collected fees.
26#![no_std]
27
28mod storage;
29
30#[cfg(test)]
31mod test;
32
33use soroban_sdk::{contracterror, contractevent, Address, Env, Symbol, Val, Vec};
34
35// ################## CONSTANTS ##################
36
37const DAY_IN_LEDGERS: u32 = 17280;
38
39/// TTL threshold for extending storage entries (in ledgers)
40pub const FEE_ABSTRACTION_EXTEND_AMOUNT: u32 = 30 * DAY_IN_LEDGERS;
41
42/// TTL extension amount for storage entries (in ledgers)
43pub const FEE_ABSTRACTION_TTL_THRESHOLD: u32 = FEE_ABSTRACTION_EXTEND_AMOUNT - DAY_IN_LEDGERS;
44
45pub use crate::storage::{
46 collect_fee, collect_fee_and_invoke, is_allowed_fee_token, is_fee_token_allowlist_enabled,
47 set_allowed_fee_token, sweep_token, validate_expiration_ledger, validate_fee_bounds,
48 FeeAbstractionApproval, FeeAbstractionStorageKey,
49};
50
51// ################## ERRORS ##################
52
53/// Errors that can occur in fee abstraction operations.
54#[contracterror]
55#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
56#[repr(u32)]
57pub enum FeeAbstractionError {
58 /// The fee token is not allowed
59 FeeTokenNotAllowed = 5000,
60 /// The fee token has been already allowed
61 FeeTokenAlreadyAllowed = 5001,
62 /// The amount of allowed tokens reached `u32::MAX`
63 TokenCountOverflow = 5002,
64 /// The fee amount exceeds the maximum allowed
65 InvalidFeeBounds = 5003,
66 /// No tokens available to sweep
67 NoTokensToSweep = 5004,
68 /// User address is current contract
69 InvalidUser = 5005,
70 /// Expiration ledger is passed
71 InvalidExpirationLedger = 5006,
72}
73
74// ################## EVENTS ##################
75
76/// Event emitted when a fee token is added or removed from the allowlist.
77#[contractevent]
78#[derive(Clone, Debug, Eq, PartialEq)]
79pub struct FeeTokenAllowlistUpdated {
80 #[topic]
81 pub token: Address,
82 pub allowed: bool,
83}
84
85/// Emits an event when a fee token is added or removed from the allowlist.
86///
87/// # Arguments
88///
89/// * `e` - Access to Soroban environment.
90/// * `token` - The token contract address.
91/// * `allowed` - Whether the token is now allowed.
92pub fn emit_fee_token_allowlist_updated(e: &Env, token: &Address, allowed: bool) {
93 FeeTokenAllowlistUpdated { token: token.clone(), allowed }.publish(e);
94}
95
96/// Event emitted when a fee is collected from a user.
97#[contractevent]
98#[derive(Clone, Debug, Eq, PartialEq)]
99pub struct FeeCollected {
100 #[topic]
101 pub user: Address,
102 #[topic]
103 pub recipient: Address,
104 pub token: Address,
105 pub amount: i128,
106}
107
108/// Emits an event when a fee is collected from a user.
109///
110/// # Arguments
111///
112/// * `e` - Access to Soroban environment.
113/// * `user` - The address of the user who paid the fee.
114/// * `recipient` - The address that received the fee.
115/// * `token` - The token contract address used for payment.
116/// * `amount` - The amount of tokens collected.
117pub fn emit_fee_collected(
118 e: &Env,
119 user: &Address,
120 recipient: &Address,
121 token: &Address,
122 amount: i128,
123) {
124 FeeCollected { user: user.clone(), recipient: recipient.clone(), token: token.clone(), amount }
125 .publish(e);
126}
127
128/// Event emitted when a call is forwarded to a target contract.
129#[contractevent]
130#[derive(Clone, Debug, Eq, PartialEq)]
131pub struct ForwardExecuted {
132 #[topic]
133 pub user: Address,
134 #[topic]
135 pub target_contract: Address,
136 pub target_fn: Symbol,
137 pub target_args: Vec<Val>,
138}
139
140/// Emits an event when a call is forwarded to a target contract.
141///
142/// # Arguments
143///
144/// * `e` - Access to Soroban environment.
145/// * `user` - The address of the user who initiated the forward.
146/// * `target_contract` - The contract address that was called.
147/// * `target_fn` - The function name that was invoked.
148/// * `target_args` - The arguments passed to the function.
149pub fn emit_forward_executed(
150 e: &Env,
151 user: &Address,
152 target_contract: &Address,
153 target_fn: &Symbol,
154 target_args: &Vec<Val>,
155) {
156 ForwardExecuted {
157 user: user.clone(),
158 target_contract: target_contract.clone(),
159 target_fn: target_fn.clone(),
160 target_args: target_args.clone(),
161 }
162 .publish(e);
163}
164
165/// Event emitted when tokens are swept from the contract.
166#[contractevent]
167#[derive(Clone, Debug, Eq, PartialEq)]
168pub struct TokensSwept {
169 #[topic]
170 pub token: Address,
171 #[topic]
172 pub recipient: Address,
173 pub amount: i128,
174}
175
176/// Emits an event when tokens are swept from the contract.
177///
178/// # Arguments
179///
180/// * `e` - Access to Soroban environment.
181/// * `token` - The token contract address that was swept.
182/// * `recipient` - The address that received the tokens.
183/// * `amount` - The amount of tokens swept.
184pub fn emit_tokens_swept(e: &Env, token: &Address, recipient: &Address, amount: i128) {
185 TokensSwept { token: token.clone(), recipient: recipient.clone(), amount }.publish(e);
186}