feat: Stellar soroban gas abstraction#642
Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughThis PR introduces comprehensive Soroban gas abstraction support to the Stellar relayer, enabling sponsored transactions with authorization signing, FeeForwarder contract integration for fee management, Soroswap DEX routing for token swaps, and simulation-based contract token balance retrieval. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant StellarRelayer as StellarRelayer<br/>(quote_soroban_from_xdr)
participant Provider as StellarProvider
participant FeeForwarder as FeeForwarderService
Client->>StellarRelayer: quote_sponsored_transaction(xdr, config)
StellarRelayer->>StellarRelayer: detect_soroban_invoke_from_xdr()
alt Soroban Detected
StellarRelayer->>StellarRelayer: extract SorobanInvokeInfo
StellarRelayer->>StellarRelayer: validate token & fee-forwarder
StellarRelayer->>FeeForwarder: build_user_auth_entry_standalone()
FeeForwarder-->>StellarRelayer: user_auth_entry
StellarRelayer->>StellarRelayer: build_forward_args, create invoke
StellarRelayer->>Provider: simulate_transaction_envelope()
Provider-->>StellarRelayer: simulation result + fees
StellarRelayer->>StellarRelayer: apply_max_fee_slippage()
StellarRelayer->>StellarRelayer: compute token conversion
StellarRelayer-->>Client: SponsoredTransactionQuoteResponse
end
sequenceDiagram
participant Client
participant StellarRelayer as StellarRelayer<br/>(build_soroban_sponsored)
participant FeeForwarder as FeeForwarderService
participant Provider as StellarProvider
participant Signer as StellarSigner
Client->>StellarRelayer: build_sponsored_transaction(xdr, config)
StellarRelayer->>StellarRelayer: detect_soroban_invoke_from_xdr()
alt Soroban Detected
StellarRelayer->>FeeForwarder: build_user_auth_entry_standalone()
FeeForwarder-->>StellarRelayer: user_auth_entry
StellarRelayer->>FeeForwarder: build_relayer_auth_entry_standalone()
FeeForwarder-->>StellarRelayer: relayer_auth_entry
StellarRelayer->>StellarRelayer: build_invoke_operation(auth_entries)
StellarRelayer->>StellarRelayer: build_soroban_transaction_envelope()
StellarRelayer->>Provider: simulate_transaction_envelope()
Provider-->>StellarRelayer: sim_response (footprint, fees)
StellarRelayer->>StellarRelayer: apply_simulation_to_soroban_envelope()
StellarRelayer->>Signer: sign_xdr_transaction()
Signer-->>StellarRelayer: signed envelope
StellarRelayer-->>Client: SponsoredTransactionBuildResponse
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
Comment |
Codecov Report❌ Patch coverage is Additional details and impacted files
Flags with carried forward coverage won't be shown. Click here to find out more. @@ Coverage Diff @@
## main #642 +/- ##
==========================================
+ Coverage 91.60% 91.74% +0.14%
==========================================
Files 274 278 +4
Lines 107708 114163 +6455
==========================================
+ Hits 98668 104743 +6075
- Misses 9040 9420 +380
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/domain/transaction/stellar/token.rs (1)
249-314:⚠️ Potential issue | 🔴 CriticalFix Int128 two's complement sign/range handling for simulated balances.
The current code misclassifies negative values and incorrectly rejects balances in the 2^63 to 2^64-1 range. The implementation checks
hi != 0which indiscriminately rejects both negative i128 values (hi = -1) and valid overflow cases, then castslo as i64to detect negatives—which fails for values ≥ 2^63 whenhi == 0.Correctly determine sign from
hiusing two's complement: reject whenhi < 0(negative), reject whenhi > 0(overflow), and returnparts.lodirectly whenhi == 0to cover the full u64 range.🛠️ Proposed fix
- ScVal::I128(parts) => { - // Check for overflow (hi should be 0 for values that fit in u64) - if parts.hi != 0 { - return Err(StellarTransactionUtilsError::BalanceTooLarge( - parts.hi, parts.lo, - )); - } - // Check for negative balance - let lo_as_i64 = parts.lo as i64; - if lo_as_i64 < 0 { - return Err(StellarTransactionUtilsError::NegativeBalanceI128(parts.lo)); - } - debug!( - "Balance for account {} on contract {}: {}", - account_id, contract_address, lo_as_i64 - ); - Ok(lo_as_i64 as u64) - } + ScVal::I128(parts) => { + if parts.hi < 0 { + return Err(StellarTransactionUtilsError::NegativeBalanceI128(parts.lo)); + } + if parts.hi > 0 { + return Err(StellarTransactionUtilsError::BalanceTooLarge( + parts.hi, parts.lo, + )); + } + debug!( + "Balance for account {} on contract {}: {}", + account_id, contract_address, parts.lo + ); + Ok(parts.lo) + }src/models/transaction/response.rs (1)
184-198:⚠️ Potential issue | 🟡 MinorDoc comment mentions
expiration_ledger, butStellarPrepareTransactionResultdoesn't expose it.The struct only includes
user_auth_entryfor Soroban;expiration_ledgeris used internally to computevalid_untilbut is not returned. Remove the inaccurate reference from the doc comment.Doc fix
- /// For Soroban: includes optional user_auth_entry, expiration_ledger + /// For Soroban: includes optional user_auth_entry
🤖 Fix all issues with AI agents
In `@helpers/sign_soroban_auth_entry.rs`:
- Around line 7-9: Update the doc example name in the file
helpers/sign_soroban_auth_entry.rs: replace the incorrect example invocation
string "sign_auth_entry" inside the code block with the actual example name
"sign_soroban_auth_entry" so the usage in the documentation matches the example
defined in Cargo.toml (ensure the code block lines showing --example ... use
"sign_soroban_auth_entry").
In `@src/domain/relayer/stellar/gas_abstraction.rs`:
- Around line 974-989: get_expiration_ledger currently does floor division so
very short validity_seconds can yield 0 ledgers added and immediate expiry;
change the math to use ceiling division of validity_seconds by
LEDGER_TIME_SECONDS and ensure at least 1 ledger is added, then perform a
saturating/clamped addition to current_ledger.sequence so it cannot overflow
u32. Update the computation that sets ledgers_to_add (referencing
LEDGER_TIME_SECONDS and get_expiration_ledger) to: compute ((validity_seconds +
LEDGER_TIME_SECONDS - 1) / LEDGER_TIME_SECONDS).max(1), do the addition in wider
integer space if needed, and clamp/saturate the final result into u32 (or use
u32::MAX on overflow) before returning.
- Around line 676-689: The build path is missing the Soroban contract-address
validation that quote performs: add the same validation for params.fee_token in
the build flow (where fee_forwarder and params.transaction_xdr are handled) so
invalid fee_token values return a RelayerError::ValidationError with a clear
message; reuse the same validation routine/logic used by quote (the code that
verifies fee_token is a Soroban contract address) and apply it to
params.fee_token before proceeding to construct the transaction.
In `@src/domain/transaction/stellar/prepare/soroban_gas_abstraction.rs`:
- Around line 25-44: The doc comment for the function soroban_gas_abstraction.rs
describes steps that don't match the implementation (it claims the relayer
signer signs an auth entry and that the signer is used), but the code does not
use the _signer parameter; update the top-level doc comment for the function
(the numbered steps) to remove or revise steps about signing with `_signer`,
reflect that the relayer's authorization entry is not signed in-function (or is
expected pre-signed), and ensure the list matches the actual behavior: parse
XDR, deserialize user signed auth entry, inject auth entries, re-simulate for
footprint, update sorobanData and sequence, and return prepared transaction
data; reference the function name and the `_signer` parameter so reviewers can
find the change.
- Around line 254-263: The apply_resource_buffer function currently multiplies
u32 fields and casts back to u32, which can silently truncate on overflow;
change it to perform the scaling in u64 using a BUFFER_MULTIPLIER: u64 constant,
use saturating_mul on the u64 value, divide by 100, then clamp the result to
u32::MAX before casting back to u32; apply this safe scaler to
resources.instructions, resources.disk_read_bytes, and resources.write_bytes
(referencing apply_resource_buffer and SorobanResources) so results never wrap
or truncate.
In `@src/services/signer/stellar/local_signer.rs`:
- Around line 34-38: The XDR import list in local_signer.rs includes unused
types; remove the unused symbols HashIdPreimage,
HashIdPreimageSorobanAuthorization, ScBytes, ScMap, ScMapEntry, ScSymbol, ScVal,
ScVec, SorobanAddressCredentials, SorobanAuthorizationEntry, SorobanCredentials,
and VecM from the use/import statement so only actually-used types like
DecoratedSignature, Hash, Limits, ReadXdr, Signature, SignatureHint,
Transaction, TransactionEnvelope, TransactionSignaturePayload,
TransactionSignaturePayloadTaggedTransaction, Uint256, and WriteXdr remain;
after editing the import list, run cargo check/build to ensure there are no
missing references and adjust any remaining references if they were accidentally
removed.
In `@src/services/stellar_dex/soroswap_service.rs`:
- Around line 384-422: In prepare_swap_transaction, do not return an empty
placeholder XDR; instead fail fast by returning an Err indicating swap
transaction construction is unimplemented (e.g., use a NotImplemented or
equivalent variant on StellarDexServiceError with a clear message like "Soroswap
swap transaction build not implemented") from the function (replace the
placeholder_xdr Ok branch), so callers receive an explicit error rather than an
invalid empty XDR; reference prepare_swap_transaction and StellarDexServiceError
when making the change.
In `@src/services/stellar_fee_forwarder/mod.rs`:
- Around line 609-616: The test function name test_parse_account_address_valid
is misleading because it passes an invalid address to
FeeForwarderService::<StellarProvider>::parse_account_address and expects Err;
rename the test to test_parse_account_address_invalid_format (or alternatively
supply a real valid G-address like
"GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF" and change the
assertion to is_ok()) so the test name matches the asserted behavior; update
only the function name or the input/assertion accordingly.
🧹 Nitpick comments (10)
helpers/sign_soroban_auth_entry.rs (1)
176-177: Minor: Comment numbering is inconsistent.The comments jump from step 8 (line 168) to step 10 (line 176), skipping step 9.
📝 Proposed fix
- // 10. Create the signature ScVal (Vec containing a Map with public_key and signature) + // 9. Create the signature ScVal (Vec containing a Map with public_key and signature)And similarly update subsequent step numbers (10 → 10, 11 → 10, 12 → 11).
src/services/stellar_fee_forwarder/mod.rs (6)
17-24: Movestd::timeimports to file header.
SystemTimeandUNIX_EPOCHare imported inside function bodies at lines 184, 243, and 515. Per coding guidelines, prefer header imports over function-level imports.Suggested fix
use crate::services::provider::StellarProviderTrait; use soroban_rs::xdr::{ ContractId, Hash, Int128Parts, InvokeContractArgs, Limits, Operation, OperationBody, ScAddress, ScSymbol, ScVal, ScVec, SorobanAddressCredentials, SorobanAuthorizationEntry, SorobanAuthorizedFunction, SorobanAuthorizedInvocation, SorobanCredentials, VecM, WriteXdr, }; use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; use thiserror::Error;Then remove the
use std::time::{SystemTime, UNIX_EPOCH};statements from lines 184, 243, and 515.
77-86: Consider derivingDebugforFeeForwarderService.Adding
#[derive(Debug)]would help with debugging and logging, consistent with coding guidelines recommending implementing common traits where appropriate.
183-189: Consolidate nonce generation to avoid duplication and fragile differentiation.Nonce generation logic is repeated three times (lines 183-189, 241-247, 514-520). The relayer nonce adds
+1to differentiate from user nonce, but this is fragile—if these methods are called at different times, nonces could collide or be inverted.Consider extracting a single
generate_noncehelper that accepts an optional offset or salt parameter, or use a more robust differentiation approach (e.g., include a type discriminator in the nonce computation).Suggested approach
/// Generate a unique nonce for authorization entries. /// Uses nanosecond timestamp with an optional salt for differentiation. fn generate_nonce_with_salt(salt: u64) -> i64 { SystemTime::now() .duration_since(UNIX_EPOCH) .map(|d| (d.as_nanos().wrapping_add(salt as u128)) as i64) .unwrap_or(salt as i64) }Then use
generate_nonce_with_salt(0)for user andgenerate_nonce_with_salt(1)for relayer consistently.Also applies to: 241-247, 510-520
299-302: Remove unused_fee_forwarder_addressparameter.This parameter is not used in the function body. If it's intended for future use, consider adding a TODO comment or removing it until needed.
371-396: Consider extracting shared utilities to reduce duplication.
parse_contract_addressandi128_to_scvalare nearly identical to implementations insoroswap_service.rs(lines 60-68 and 116-120). These could be extracted to a shared Soroban utilities module, with error conversion handled at call sites.Suggested approach
Create a shared module (e.g.,
src/services/stellar_utils.rs) with generic versions:pub fn parse_contract_address(address: &str) -> Result<ScAddress, String> { let contract = stellar_strkey::Contract::from_string(address) .map_err(|e| format!("Invalid contract '{address}': {e}"))?; Ok(ScAddress::Contract(ContractId(Hash(contract.0)))) } pub fn i128_to_scval(amount: i128) -> ScVal { let hi = (amount >> 64) as i64; let lo = amount as u64; ScVal::I128(Int128Parts { hi, lo }) }Then each service wraps the error appropriately.
419-508:build_user_auth_entryduplicatesbuild_user_auth_entry_standalonelogic.The instance method shares ~80% of its code with the standalone version. Consider having
build_user_auth_entrydelegate tobuild_user_auth_entry_standalone:Suggested refactor
pub fn build_user_auth_entry( &self, params: &FeeForwarderParams, requires_target_auth: bool, ) -> Result<SorobanAuthorizationEntry, FeeForwarderError> { - let fee_forwarder_addr = Self::parse_contract_address(&self.fee_forwarder_address)?; - // ... ~80 lines of duplicated logic ... + Self::build_user_auth_entry_standalone( + &self.fee_forwarder_address, + params, + requires_target_auth, + ) }src/services/signer/stellar/aws_kms_signer.rs (1)
20-23: Same observation: verify usage of new XDR imports.Same expanded imports as
local_signer.rs. These types don't appear to be used in the visible changed code. If they're for future use, consider adding them when actually needed to keep imports clean.src/domain/transaction/stellar/prepare/soroban_gas_abstraction.rs (1)
176-197: Consider failing fast when auth entries are missing.
Right now the code proceeds even with a single/empty auth list; surfacing a ValidationError earlier will produce clearer failures.Possible guard
- let mut auth_entries: Vec<SorobanAuthorizationEntry> = invoke_op.auth.to_vec(); - - if auth_entries.is_empty() { - // If there are no auth entries, just add the user's signed one - auth_entries.push(signed_user_auth); - } else { - // Replace the first auth entry (user's) with the signed version - auth_entries[0] = signed_user_auth; - - // Convert the relayer's auth entry (second entry) to use SourceAccount credentials. - // Since the relayer is the transaction source account, the transaction signature - // already authorizes this entry - no separate auth entry signature is needed. - if auth_entries.len() > 1 { - let relayer_auth = &auth_entries[1]; - let source_account_auth = SorobanAuthorizationEntry { - credentials: SorobanCredentials::SourceAccount, - root_invocation: relayer_auth.root_invocation.clone(), - }; - auth_entries[1] = source_account_auth; - debug!("Converted relayer auth entry to SourceAccount credentials"); - } - } + let mut auth_entries: Vec<SorobanAuthorizationEntry> = invoke_op.auth.to_vec(); + if auth_entries.len() < 2 { + return Err(TransactionError::ValidationError( + "Expected at least two authorization entries".to_string(), + )); + } + + // Replace the first auth entry (user's) with the signed version + auth_entries[0] = signed_user_auth; + + // Convert the relayer's auth entry (second entry) to use SourceAccount credentials. + // Since the relayer is the transaction source account, the transaction signature + // already authorizes this entry - no separate auth entry signature is needed. + let relayer_auth = &auth_entries[1]; + let source_account_auth = SorobanAuthorizationEntry { + credentials: SorobanCredentials::SourceAccount, + root_invocation: relayer_auth.root_invocation.clone(), + }; + auth_entries[1] = source_account_auth; + debug!("Converted relayer auth entry to SourceAccount credentials");src/domain/relayer/stellar/gas_abstraction.rs (1)
6-70: Alignsoroban_rsimports and avoid function-leveluse.The external
soroban_rs::xdrimport is split below local imports, andTransactionEnvelopeis re-imported inside the function. Consider consolidating these at the header for consistency and to follow the Rust import guidelines.Suggested import cleanup
-use soroban_rs::xdr::{Limits, Operation, TransactionEnvelope, WriteXdr}; +use soroban_rs::xdr::{ + HostFunction, Limits, Operation, OperationBody, ReadXdr, ScVal, TransactionEnvelope, WriteXdr, +}; ... -use soroban_rs::xdr::{HostFunction, OperationBody, ReadXdr, ScVal};-fn detect_soroban_invoke_from_xdr(xdr: &str) -> Result<Option<SorobanInvokeInfo>, RelayerError> { - use soroban_rs::xdr::TransactionEnvelope; +fn detect_soroban_invoke_from_xdr(xdr: &str) -> Result<Option<SorobanInvokeInfo>, RelayerError> {As per coding guidelines, Order imports alphabetically and group by: std, external crates, local crates; Prefer header imports over function-level imports of dependencies.
zeljkoX
left a comment
There was a problem hiding this comment.
Still reviewing.
Great job!
Added few comments
There was a problem hiding this comment.
Pull request overview
Adds Soroban gas abstraction support for Stellar by introducing a FeeForwarder transaction flow, Soroswap-based quoting/swaps for contract tokens, and request/response + signer updates to carry and submit Soroban authorization entries.
Changes:
- Introduces a new
stellar_fee_forwarderservice to build Soroban auth entries and FeeForwarder invoke operations. - Adds Soroswap DEX strategy and integrates it into the Stellar DEX routing layer (alongside classic orderbook).
- Extends Stellar transaction request/handling to support Soroban gas abstraction (XDR + signed auth entry), plus new server config options and an auth-entry signing helper.
Reviewed changes
Copilot reviewed 35 out of 36 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils/mocks.rs | Adds mock config fields for FeeForwarder/Soroswap addresses. |
| src/services/stellar_fee_forwarder/mod.rs | New FeeForwarder service for building Soroban auth entries and invoke ops (with tests). |
| src/services/stellar_dex/stellar_dex_service.rs | Adds Soroswap strategy wiring and expands unit tests for strategy selection/routing. |
| src/services/stellar_dex/soroswap_service.rs | New Soroswap service for Soroban token quotes and swap-tx XDR preparation (with tests). |
| src/services/stellar_dex/mod.rs | Exposes Soroswap module/service in the stellar_dex public API. |
| src/services/signer/stellar/turnkey_signer.rs | Extends signer to accept Soroban gas abstraction transaction input variant. |
| src/services/signer/stellar/local_signer.rs | Extends signer to accept Soroban gas abstraction transaction input variant; trims unused imports. |
| src/services/signer/stellar/google_cloud_kms_signer.rs | Extends signer to accept Soroban gas abstraction transaction input variant. |
| src/services/signer/stellar/aws_kms_signer.rs | Extends signer to accept Soroban gas abstraction transaction input variant; adds Soroban-related XDR imports. |
| src/services/mod.rs | Exposes the new stellar_fee_forwarder service module. |
| src/models/transaction/stellar/conversion.rs | Prevents conversion of Soroban gas abstraction XDR inputs into Transaction. |
| src/models/transaction/response.rs | Updates response enum docs to mention Soroban support. |
| src/models/transaction/request/stellar.rs | Adds signed_auth_entry field and validation rules for Soroban gas abstraction submissions. |
| src/models/transaction/request/mod.rs | Updates top-level request docs to describe Soroban gas abstraction usage for Stellar. |
| src/models/transaction/repository.rs | Adds TransactionInput::SorobanGasAbstraction and integrates request parsing/handling. |
| src/models/rpc/stellar/mod.rs | Adds Soroban-specific optional fields to fee/prepare result schemas and clarifies request docs. |
| src/models/relayer/mod.rs | Adjusts Stellar asset identifier validation to accept contract-address fee tokens. |
| src/models/relayer/config.rs | Documents how fee_payment_strategy=user ties into Soroban gas abstraction configuration. |
| src/domain/transaction/stellar/token.rs | Changes contract-token balance retrieval to call balance() via simulation instead of ledger storage reads; updates tests. |
| src/domain/transaction/stellar/test_helpers.rs | Introduces a combined Stellar mock signer for tests needing both Signer and StellarSignTrait. |
| src/domain/transaction/stellar/submit.rs | Updates trait bounds to require Stellar XDR signing capability. |
| src/domain/transaction/stellar/stellar_transaction.rs | Updates trait bounds and swaps default DEX type to the multi-strategy StellarDexService. |
| src/domain/transaction/stellar/status.rs | Updates trait bounds and test types to require Stellar XDR signing capability. |
| src/domain/transaction/stellar/prepare/unsigned_xdr.rs | Extends swap validation for Soroswap invoke ops; applies Soroban simulation outputs into envelopes; adds tests. |
| src/domain/transaction/stellar/prepare/mod.rs | Routes Soroban gas abstraction transaction inputs into a dedicated preparation path. |
| src/domain/transaction/mod.rs | Builds a multi-strategy Stellar DEX service based on policy + config/default addresses. |
| src/domain/relayer/stellar/token_swap.rs | Updates Stellar transaction requests to include the new signed_auth_entry field (None). |
| src/domain/relayer/stellar/stellar_relayer.rs | Makes signer accessible within crate for updated transaction flows; updates tests. |
| src/domain/relayer/stellar/mod.rs | Initializes Soroswap DEX service when configured (instead of skipping). |
| src/constants/stellar_transaction.rs | Adds default Soroswap/FeeForwarder addresses and helper getters. |
| src/config/server_config.rs | Adds env-driven config for FeeForwarder and Soroswap contract addresses. |
| openapi.json | Updates API descriptions/schemas for Soroban gas abstraction-related fields. |
| helpers/sign_soroban_auth_entry.rs | Adds an example CLI tool to sign Soroban authorization entries. |
| Cargo.toml | Registers the sign_soroban_auth_entry example binary. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "Updated transaction sorobanData with simulation results, preserved fee={}", | ||
| original_fee | ||
| "Updated transaction sorobanData with simulation results, fee={}", | ||
| fee_to_set |
There was a problem hiding this comment.
weird that it does not complain here
Summary
Adds gas abstraction support for Stellar Soroban transactions, allowing users to pay transaction fees using tokens other than XLM.
Changes
Key Files
Related PR:
OpenZeppelin/openzeppelin-relayer-sdk#249
Testing Process
Checklist
Note
If you are using Relayer in your stack, consider adding your team or organization to our list of Relayer Users in the Wild!
Summary by CodeRabbit
Release Notes
New Features
Chores