Skip to content

Commit 3a48d1a

Browse files
committed
Add ExactEvmConfig for smart wallet deployment control and support EOA v,r,s signature overload
* Add ExactEvmConfig struct with deploy_erc4337_with_eip6492 flag to control smart wallet deployment behavior * Implement Default trait for ExactEvmConfig with deployment disabled by default * Add with_config and update with_networks constructors to accept custom configuration * Reject undeployed smart wallet settlements when deployment is disabled via config * Add transferWithAuthorizationVRS function
1 parent 2e610c0 commit 3a48d1a

File tree

4 files changed

+141
-25
lines changed

4 files changed

+141
-25
lines changed

r402-evm/src/exact/facilitator.rs

Lines changed: 77 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,31 @@ use crate::chain::{NetworkConfig, parse_caip2};
1717
use crate::exact::types::{
1818
ExactPayload, ExactRequirementsExtra, SCHEME_EXACT, TransferWithAuthorization,
1919
authorizationStateCall, balanceOfCall, isValidSignatureCall, transferWithAuthorizationCall,
20+
transferWithAuthorizationVRSCall,
2021
};
2122
use crate::networks::known_networks;
2223

24+
/// Configuration options for the EVM exact scheme facilitator.
25+
///
26+
/// Corresponds to Python SDK's `ExactEvmSchemeConfig` in `exact/facilitator.py`.
27+
#[derive(Debug, Clone, Copy)]
28+
pub struct ExactEvmConfig {
29+
/// Whether to deploy ERC-4337 smart wallets via ERC-6492 factory calls
30+
/// during settlement. When `false`, payments from undeployed smart wallets
31+
/// will be rejected at settlement time.
32+
///
33+
/// Default: `false`.
34+
pub deploy_erc4337_with_eip6492: bool,
35+
}
36+
37+
impl Default for ExactEvmConfig {
38+
fn default() -> Self {
39+
Self {
40+
deploy_erc4337_with_eip6492: false,
41+
}
42+
}
43+
}
44+
2345
/// EVM facilitator implementation for the "exact" payment scheme.
2446
///
2547
/// Verifies EIP-3009 authorization signatures and settles payments by
@@ -34,6 +56,7 @@ use crate::networks::known_networks;
3456
pub struct ExactEvmFacilitator<P> {
3557
provider: P,
3658
signer_address: Address,
59+
config: ExactEvmConfig,
3760
#[allow(dead_code)]
3861
networks: Vec<NetworkConfig>,
3962
}
@@ -45,24 +68,37 @@ where
4568
/// Creates a new facilitator with the given provider and signer address.
4669
///
4770
/// The `signer_address` is the facilitator's wallet that will submit
48-
/// settlement transactions.
71+
/// settlement transactions. Uses default configuration.
4972
pub fn new(provider: P, signer_address: Address) -> Self {
5073
Self {
5174
provider,
5275
signer_address,
76+
config: ExactEvmConfig::default(),
77+
networks: known_networks(),
78+
}
79+
}
80+
81+
/// Creates a facilitator with custom configuration.
82+
pub fn with_config(provider: P, signer_address: Address, config: ExactEvmConfig) -> Self {
83+
Self {
84+
provider,
85+
signer_address,
86+
config,
5387
networks: known_networks(),
5488
}
5589
}
5690

57-
/// Creates a facilitator with custom network configurations.
91+
/// Creates a facilitator with custom network configurations and config.
5892
pub fn with_networks(
5993
provider: P,
6094
signer_address: Address,
95+
config: ExactEvmConfig,
6196
networks: Vec<NetworkConfig>,
6297
) -> Self {
6398
Self {
6499
provider,
65100
signer_address,
101+
config,
66102
networks,
67103
}
68104
}
@@ -352,6 +388,13 @@ where
352388
if has_deployment_info(&sig_data) {
353389
let code = self.get_code(from).await.unwrap_or_default();
354390
if code.is_empty() {
391+
if !self.config.deploy_erc4337_with_eip6492 {
392+
return SettleResponse::error(
393+
"undeployed_smart_wallet",
394+
"Smart wallet deployment is disabled by configuration",
395+
&network,
396+
);
397+
}
355398
// Smart wallet not deployed — attempt factory deployment
356399
let deploy_tx = alloy_rpc_types_eth::TransactionRequest::default()
357400
.from(self.signer_address)
@@ -390,19 +433,39 @@ where
390433

391434
// Use inner signature (stripped of ERC-6492 wrapper) for settlement
392435
let inner_sig = &sig_data.inner_signature;
393-
394-
// Build transferWithAuthorization calldata (always use bytes overload
395-
// for compatibility with both EOA and smart wallet signatures)
396-
let call = transferWithAuthorizationCall {
397-
from,
398-
to,
399-
value,
400-
validAfter: valid_after,
401-
validBefore: valid_before,
402-
nonce: nonce_bytes.into(),
403-
signature: Bytes::from(inner_sig.clone()),
436+
let is_ecdsa = inner_sig.len() == 65;
437+
438+
// Build transferWithAuthorization calldata.
439+
// EOA (65-byte): use v,r,s overload for maximum compatibility.
440+
// Smart wallet (non-65-byte): use bytes overload.
441+
let calldata = if is_ecdsa {
442+
let r = B256::from_slice(&inner_sig[..32]);
443+
let s = B256::from_slice(&inner_sig[32..64]);
444+
let v = inner_sig[64];
445+
let call = transferWithAuthorizationVRSCall {
446+
from,
447+
to,
448+
value,
449+
validAfter: valid_after,
450+
validBefore: valid_before,
451+
nonce: nonce_bytes.into(),
452+
v,
453+
r,
454+
s,
455+
};
456+
call.abi_encode()
457+
} else {
458+
let call = transferWithAuthorizationCall {
459+
from,
460+
to,
461+
value,
462+
validAfter: valid_after,
463+
validBefore: valid_before,
464+
nonce: nonce_bytes.into(),
465+
signature: Bytes::from(inner_sig.clone()),
466+
};
467+
call.abi_encode()
404468
};
405-
let calldata = call.abi_encode();
406469

407470
// Submit settlement transaction
408471
let tx = alloy_rpc_types_eth::TransactionRequest::default()

r402-evm/src/exact/types.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ pub struct ExactRequirementsExtra {
6464
}
6565

6666
sol! {
67-
/// ERC-3009 `transferWithAuthorization` function.
67+
/// ERC-3009 `transferWithAuthorization` function (bytes overload).
68+
/// Used for smart wallet signatures (EIP-1271).
6869
#[derive(Debug, PartialEq, Eq)]
6970
function transferWithAuthorization(
7071
address from,
@@ -76,6 +77,22 @@ sol! {
7677
bytes memory signature
7778
) external;
7879

80+
/// ERC-3009 `transferWithAuthorization` function (v,r,s overload).
81+
/// Used for EOA signatures (65 bytes decomposed into v, r, s).
82+
#[derive(Debug, PartialEq, Eq)]
83+
#[sol(rename = "transferWithAuthorization")]
84+
function transferWithAuthorizationVRS(
85+
address from,
86+
address to,
87+
uint256 value,
88+
uint256 validAfter,
89+
uint256 validBefore,
90+
bytes32 nonce,
91+
uint8 v,
92+
bytes32 r,
93+
bytes32 s
94+
) external;
95+
7996
/// ERC-3009 `authorizationState` view function.
8097
#[derive(Debug, PartialEq, Eq)]
8198
function authorizationState(

r402-evm/src/networks.rs

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,17 @@ pub const ETHEREUM_MAINNET: ChainId = 1;
3030
/// Celo Mainnet chain ID.
3131
pub const CELO_MAINNET: ChainId = 42220;
3232

33-
/// `MegaETH` Testnet chain ID.
34-
pub const MEGAETH_TESTNET: ChainId = 6342;
33+
/// `MegaETH` Mainnet (Frontier) chain ID.
34+
pub const MEGAETH_MAINNET: ChainId = 4326;
35+
36+
/// `MegaETH` Testnet v2 chain ID.
37+
pub const MEGAETH_TESTNET: ChainId = 6343;
38+
39+
/// Monad Mainnet chain ID.
40+
pub const MONAD_MAINNET: ChainId = 143;
41+
42+
/// Monad Testnet chain ID.
43+
pub const MONAD_TESTNET: ChainId = 10143;
3544

3645
/// USDC contract address on Base Mainnet.
3746
pub const USDC_BASE: Address = address!("833589fCD6eDb6E08f4c7C32D4f71b54bdA02913");
@@ -57,8 +66,15 @@ pub const USDC_AVALANCHE_FUJI: Address = address!("5425890298aed601595a70AB815c9
5766
/// USDC contract address on Celo.
5867
pub const USDC_CELO: Address = address!("cebA9300f2b948710d2653dD7B07f33A8B32118C");
5968

60-
/// USDC contract address on `MegaETH` Testnet.
61-
pub const USDC_MEGAETH: Address = address!("2F24De1820e846B6C14EB8ED4dDfc4fdF7cc5149");
69+
/// USDM contract address on `MegaETH` Mainnet (Frontier).
70+
/// MegaETH uses USDM (MegaUSD) instead of Circle USDC.
71+
pub const USDM_MEGAETH: Address = address!("FAfDdbb3FC7688494971a79cc65DCa3EF82079E7");
72+
73+
/// USDC contract address on Monad Mainnet (Circle native).
74+
pub const USDC_MONAD: Address = address!("754704Bc059F8C67012fEd69BC8A327a5aafb603");
75+
76+
/// USDC contract address on Monad Testnet (Circle native).
77+
pub const USDC_MONAD_TESTNET: Address = address!("534b2f3A21130d7a60830c2Df862319e593943A3");
6278

6379
/// Default EIP-712 domain name for USDC.
6480
pub const DEFAULT_USDC_NAME: &str = "USD Coin";
@@ -146,10 +162,29 @@ pub fn known_networks() -> Vec<NetworkConfig> {
146162
)],
147163
},
148164
NetworkConfig {
149-
network: format!("eip155:{MEGAETH_TESTNET}"),
150-
chain_id: MEGAETH_TESTNET,
165+
network: format!("eip155:{MEGAETH_MAINNET}"),
166+
chain_id: MEGAETH_MAINNET,
167+
assets: vec![AssetInfo {
168+
address: USDM_MEGAETH,
169+
decimals: 18,
170+
name: "MegaUSD".to_owned(),
171+
version: "1".to_owned(),
172+
}],
173+
},
174+
NetworkConfig {
175+
network: format!("eip155:{MONAD_MAINNET}"),
176+
chain_id: MONAD_MAINNET,
177+
assets: vec![usdc_asset(
178+
USDC_MONAD,
179+
DEFAULT_USDC_NAME,
180+
DEFAULT_USDC_VERSION,
181+
)],
182+
},
183+
NetworkConfig {
184+
network: format!("eip155:{MONAD_TESTNET}"),
185+
chain_id: MONAD_TESTNET,
151186
assets: vec![usdc_asset(
152-
USDC_MEGAETH,
187+
USDC_MONAD_TESTNET,
153188
DEFAULT_USDC_NAME,
154189
DEFAULT_USDC_VERSION,
155190
)],

r402/src/server.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,9 +185,10 @@ impl X402ResourceServer {
185185
#[must_use]
186186
pub fn has_registered_scheme(&self, network: &str, scheme: &str) -> bool {
187187
if let Some(schemes) = self.schemes.get(network)
188-
&& schemes.contains_key(scheme) {
189-
return true;
190-
}
188+
&& schemes.contains_key(scheme)
189+
{
190+
return true;
191+
}
191192
let prefix = network.split(':').next().unwrap_or("");
192193
let wildcard = format!("{prefix}:*");
193194
self.schemes

0 commit comments

Comments
 (0)