Skip to content

Commit 367a6e9

Browse files
authored
fix: Fixes solana allowed_tokens policy validation (#467)
1 parent 44aaad1 commit 367a6e9

File tree

3 files changed

+359
-26
lines changed

3 files changed

+359
-26
lines changed

docs/modules/ROOT/pages/solana.adoc

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,12 +125,17 @@ For more configuration examples, visit the link:https://github.com/OpenZeppelin/
125125

126126
In addition to standard relayer configuration and policies, Solana relayers support additional options:
127127

128-
- `fee_payment_strategy`: `"user"` or `"relayer"` (who pays transaction fees)
129-
- `allowed_tokens`: List of SPL tokens supported for swaps and fee payments
128+
- `fee_payment_strategy`: `"user"` or `"relayer"` (who pays transaction fees). "user" is default value.
129+
* `"user"`: Users pay transaction fees in tokens (relayer receives fee payment from user)
130+
* `"relayer"`: **Relayer pays for all transaction fees** using SOL from the relayer's account
131+
- `allowed_tokens`: List of SPL tokens supported for swaps and fee payments. Restrict relayer operations to specific tokens. Optional.
132+
* **When not set or empty, all tokens are allowed** for transactions and fee payments
133+
* When configured, only tokens in this list can be used for transfers and fee payments
130134
- `allowed_programs`, `allowed_accounts`, `disallowed_accounts`: Restrict relayer operations to specific programs/accounts
131135
- `swap_config`: Automated token swap settings (see below)
132136

133137

138+
134139
You can check all options in xref:index.adoc#3_relayers[User Documentation - Relayers].
135140

136141
=== Automated token swap configuration options:
@@ -176,6 +181,15 @@ Common endpoints:
176181
- `getSupportedTokens`
177182
- `getSupportedFeatures`
178183

184+
[NOTE]
185+
====
186+
**Fee Token Parameter Behavior:**
187+
188+
When using `fee_payment_strategy: "relayer"`, the `fee_token` parameter in RPC methods becomes **informational only**. The relayer pays all transaction fees in SOL regardless of the specified fee token. In this mode, you can use either `"So11111111111111111111111111111112"` (WSOL) or `"11111111111111111111111111111111"` (native SOL) as the fee_token value.
189+
190+
When using `fee_payment_strategy: "user"`, the `fee_token` parameter determines which token the user will pay fees in, and must be a supported token from the `allowed_tokens` list (if configured).
191+
====
192+
179193
Example: Estimate fee for a transaction
180194
[source,bash]
181195
----

src/domain/relayer/solana/rpc/methods/utils.rs

Lines changed: 188 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ use crate::{
5151
services::{JupiterServiceTrait, SolanaProviderTrait, SolanaSignTrait},
5252
};
5353

54+
#[derive(Debug)]
5455
pub struct FeeQuote {
5556
pub fee_in_spl: u64,
5657
pub fee_in_spl_ui: String,
@@ -77,6 +78,56 @@ where
7778
JP: JobProducerTrait + Send + Sync,
7879
TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
7980
{
81+
/// Fetches token decimals from the blockchain using string mint address
82+
async fn fetch_token_decimals_from_chain(
83+
&self,
84+
token_mint_str: &str,
85+
) -> Result<u8, SolanaRpcError> {
86+
let token_mint = Pubkey::from_str(token_mint_str)
87+
.map_err(|e| SolanaRpcError::Internal(format!("Invalid mint address: {}", e)))?;
88+
89+
self.fetch_token_decimals_from_chain_pubkey(&token_mint)
90+
.await
91+
}
92+
93+
/// Fetches token decimals from the blockchain using Pubkey
94+
async fn fetch_token_decimals_from_chain_pubkey(
95+
&self,
96+
token_mint: &Pubkey,
97+
) -> Result<u8, SolanaRpcError> {
98+
let mint_account = self
99+
.provider
100+
.get_account_from_pubkey(token_mint)
101+
.await
102+
.map_err(|e| {
103+
SolanaRpcError::Internal(format!("Failed to fetch mint account: {}", e))
104+
})?;
105+
106+
let mint_info = spl_token::state::Mint::unpack(&mint_account.data)
107+
.map_err(|e| SolanaRpcError::Internal(format!("Failed to unpack mint data: {}", e)))?;
108+
109+
Ok(mint_info.decimals)
110+
}
111+
112+
/// Gets token decimals from policy first, then fetches from blockchain if not found (Pubkey version)
113+
async fn get_token_decimals_from_policy_or_fetch_pubkey(
114+
&self,
115+
token_mint: &Pubkey,
116+
) -> Result<u8, SolanaRpcError> {
117+
match self
118+
.relayer
119+
.policies
120+
.get_solana_policy()
121+
.get_allowed_token_decimals(&token_mint.to_string())
122+
{
123+
Some(decimals) => Ok(decimals),
124+
None => {
125+
self.fetch_token_decimals_from_chain_pubkey(token_mint)
126+
.await
127+
}
128+
}
129+
}
130+
80131
/// Signs a transaction with the relayer's keypair and returns both the signed transaction and
81132
/// signature.
82133
///
@@ -288,27 +339,39 @@ where
288339
});
289340
}
290341

291-
// Get token policy
292-
let token_entry = self
293-
.relayer
294-
.policies
295-
.get_solana_policy()
296-
.get_allowed_token_entry(token)
297-
.ok_or_else(|| {
342+
let policy = self.relayer.policies.get_solana_policy();
343+
344+
// Check if allowed tokens are configured
345+
let no_allowed_tokens_configured = match &policy.allowed_tokens {
346+
None => true, // No tokens configured
347+
Some(tokens) => tokens.is_empty(), // Tokens configured but empty
348+
};
349+
350+
let (decimals, slippage) = if no_allowed_tokens_configured {
351+
// No allowed tokens configured - allow all tokens and fetch decimals from blockchain
352+
let decimals = self.fetch_token_decimals_from_chain(token).await?;
353+
(decimals, DEFAULT_CONVERSION_SLIPPAGE_PERCENTAGE)
354+
} else {
355+
// Allowed tokens are configured - check if token is in the list
356+
let token_entry = policy.get_allowed_token_entry(token).ok_or_else(|| {
298357
SolanaRpcError::UnsupportedFeeToken(format!("Token {} not allowed", token))
299358
})?;
300359

301-
// Get token decimals
302-
let decimals = token_entry.decimals.ok_or_else(|| {
303-
SolanaRpcError::Estimation("Token decimals not configured".to_string())
304-
})?;
360+
// Get token decimals from policy first, then fetch from blockchain
361+
let decimals = match token_entry.decimals {
362+
Some(decimals) => decimals,
363+
None => self.fetch_token_decimals_from_chain(token).await?,
364+
};
365+
366+
// Get slippage from policy
367+
let slippage = token_entry
368+
.swap_config
369+
.as_ref()
370+
.and_then(|config| config.slippage_percentage)
371+
.unwrap_or(DEFAULT_CONVERSION_SLIPPAGE_PERCENTAGE);
305372

306-
// Get slippage from policy
307-
let slippage = token_entry
308-
.swap_config
309-
.as_ref()
310-
.and_then(|config| config.slippage_percentage)
311-
.unwrap_or(DEFAULT_CONVERSION_SLIPPAGE_PERCENTAGE);
373+
(decimals, slippage)
374+
};
312375

313376
// Get Jupiter quote
314377
let quote = self
@@ -469,14 +532,11 @@ where
469532
token_mint,
470533
));
471534
}
535+
536+
// Try to get token decimals from policy first, then fetch from blockchain
472537
let token_decimals = self
473-
.relayer
474-
.policies
475-
.get_solana_policy()
476-
.get_allowed_token_decimals(&token_mint.to_string())
477-
.ok_or_else(|| {
478-
SolanaRpcError::UnsupportedFeeToken("Token not found in allowed tokens".to_string())
479-
})?;
538+
.get_token_decimals_from_policy_or_fetch_pubkey(token_mint)
539+
.await?;
480540

481541
instructions.push(SolanaTokenProgram::create_transfer_checked_instruction(
482542
&program_id,
@@ -2731,4 +2791,108 @@ mod tests {
27312791
// Assert success
27322792
assert!(result.is_ok(), "Schedule status check should succeed");
27332793
}
2794+
2795+
#[tokio::test]
2796+
async fn test_get_fee_token_quote_fetches_decimals_from_chain_when_no_allowed_tokens() {
2797+
let (relayer, signer, mut provider, mut jupiter_service, _, job_producer, network) =
2798+
setup_test_context();
2799+
2800+
let test_token = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; // USDC mint
2801+
let total_fee = 5000u64; // 5000 lamports
2802+
2803+
// Mock the provider to return mint account data when get_account_from_pubkey is called
2804+
let mint_data = {
2805+
let mint_info = spl_token::state::Mint {
2806+
mint_authority: None.into(),
2807+
supply: 1_000_000_000_000,
2808+
decimals: 6, // USDC decimals
2809+
is_initialized: true,
2810+
freeze_authority: None.into(),
2811+
};
2812+
let mut data = vec![0u8; spl_token::state::Mint::LEN];
2813+
spl_token::state::Mint::pack(mint_info, &mut data).unwrap();
2814+
data
2815+
};
2816+
2817+
provider
2818+
.expect_get_account_from_pubkey()
2819+
.returning(move |_| {
2820+
let mint_data_clone = mint_data.clone();
2821+
Box::pin(async move {
2822+
Ok(solana_sdk::account::Account {
2823+
lamports: 1000000,
2824+
data: mint_data_clone,
2825+
owner: spl_token::id(),
2826+
executable: false,
2827+
rent_epoch: 0,
2828+
})
2829+
})
2830+
});
2831+
2832+
// Mock Jupiter service to return a quote
2833+
jupiter_service
2834+
.expect_get_sol_to_token_quote()
2835+
.returning(|_token, _amount, _slippage| {
2836+
Box::pin(async move {
2837+
Ok(QuoteResponse {
2838+
input_mint: WRAPPED_SOL_MINT.to_string(),
2839+
output_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
2840+
in_amount: 5000, // Input SOL amount
2841+
out_amount: 5000, // Output token amount (1:1 for simplicity)
2842+
other_amount_threshold: 4750,
2843+
swap_mode: "ExactIn".to_string(),
2844+
slippage_bps: 50,
2845+
price_impact_pct: 0.1,
2846+
route_plan: vec![RoutePlan {
2847+
swap_info: SwapInfo {
2848+
amm_key: "test-amm".to_string(),
2849+
label: "test-swap".to_string(),
2850+
input_mint: WRAPPED_SOL_MINT.to_string(),
2851+
output_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
2852+
.to_string(),
2853+
in_amount: "5000".to_string(),
2854+
out_amount: "5000".to_string(),
2855+
fee_amount: "25".to_string(),
2856+
fee_mint: WRAPPED_SOL_MINT.to_string(),
2857+
},
2858+
percent: 100,
2859+
}],
2860+
})
2861+
})
2862+
});
2863+
2864+
let rpc = SolanaRpcMethodsImpl::new_mock(
2865+
relayer,
2866+
network,
2867+
Arc::new(provider),
2868+
Arc::new(signer),
2869+
Arc::new(jupiter_service),
2870+
Arc::new(job_producer),
2871+
Arc::new(MockTransactionRepository::new()),
2872+
);
2873+
2874+
let result = rpc.get_fee_token_quote(test_token, total_fee).await;
2875+
2876+
// Assert success
2877+
assert!(result.is_ok(), "Fee quote should succeed: {:?}", result);
2878+
2879+
let quote = result.unwrap();
2880+
2881+
assert_eq!(quote.fee_in_spl, 5000, "SPL fee amount should match");
2882+
assert_eq!(
2883+
quote.fee_in_lamports, total_fee,
2884+
"Lamports fee should match"
2885+
);
2886+
2887+
assert_eq!(
2888+
quote.fee_in_spl_ui, "0.005",
2889+
"UI amount should use correct decimals from chain"
2890+
);
2891+
2892+
assert!(
2893+
(quote.conversion_rate - 1000.0).abs() < 0.001,
2894+
"Conversion rate should be approximately 1000.0, got {}",
2895+
quote.conversion_rate
2896+
);
2897+
}
27342898
}

0 commit comments

Comments
 (0)