Skip to main content

foundry_evm_traces/decoder/
base.rs

1//! Base precompile ABIs for trace decoding.
2//!
3//! `base-common-precompiles` declares these interfaces without `#[sol(abi)]`, so it exposes no
4//! [`JsonAbi`](alloy_json_abi::JsonAbi) for `register_address_abi` to consume. These declarations
5//! mirror the canonical ones so traces name Base precompile calls, events, and errors, following
6//! the same approach `super::monad` takes for its staking interfaces.
7//!
8//! The `abi_matches_canonical_selectors` tests below pin every selector against the canonical
9//! types, so an interface change in Base fails here instead of silently decoding traces wrong.
10
11use alloy_sol_types::sol;
12
13sol! {
14    /// Mirror of `base_common_precompiles::IActivationRegistry`.
15    #[sol(abi)]
16    interface IActivationRegistry {
17        event FeatureActivated(bytes32 indexed feature, address indexed caller);
18        event FeatureDeactivated(bytes32 indexed feature, address indexed caller);
19        event AdminChanged(
20            address indexed previousAdmin,
21            address indexed newAdmin,
22            address indexed caller
23        );
24
25        error Unauthorized(address caller);
26        error AlreadyActivated(bytes32 feature);
27        error FeatureNotActivated(bytes32 feature);
28        error DelegateCallNotAllowed();
29        error StaticCallNotAllowed();
30        error NonPayable();
31        error AdminStorageNotEnabled();
32        error ZeroAdminAddress();
33
34        function isActivated(bytes32 feature) external view returns (bool);
35        function checkActivated(bytes32 feature) external view;
36        function admin() external view returns (address);
37        function setAdmin(address newAdmin) external;
38        function activate(bytes32 feature) external;
39        function deactivate(bytes32 feature) external;
40    }
41}
42
43sol! {
44    /// Mirror of `base_common_precompiles::IB20Factory`, the surface frozen at Beryl.
45    #[sol(abi)]
46    interface IB20Factory {
47        enum B20Variant {
48            ASSET,
49            STABLECOIN
50        }
51
52        struct B20StablecoinCreateParams {
53            uint8 version;
54            string name;
55            string symbol;
56            address initialAdmin;
57            string currency;
58        }
59
60        struct B20AssetCreateParams {
61            uint8 version;
62            string name;
63            string symbol;
64            address initialAdmin;
65            uint8 decimals;
66        }
67
68        error NonPayable();
69        error TokenAlreadyExists(address token);
70        error InvalidVariant();
71        error UnsupportedVersion(uint8 version, B20Variant variant);
72        error MissingRequiredField(string field);
73        error InvalidCurrency(string code);
74        error InvalidDecimals(uint8 decimals);
75        error InitCallFailed(uint256 index);
76
77        event B20Created(
78            address indexed token,
79            B20Variant indexed variant,
80            string name,
81            string symbol,
82            uint8 decimals,
83            bytes variantParams
84        );
85
86        struct B20StablecoinEventParams {
87            uint8 version;
88            string currency;
89        }
90
91        function createB20(
92            B20Variant variant,
93            bytes32 salt,
94            bytes calldata params,
95            bytes[] calldata initCalls
96        ) external returns (address token);
97        function getB20Address(B20Variant variant, address sender, bytes32 salt) external view returns (address);
98        function isB20(address token) external view returns (bool);
99        function isB20Initialized(address token) external view returns (bool);
100    }
101}
102
103sol! {
104    /// **Deliberately partial** mirror of `base_common_precompiles::IB20`, the B-20 token surface.
105    ///
106    /// B-20 tokens are created by the factory at derived addresses, so they cannot be registered
107    /// per address like the fixed precompiles. These members are therefore available as a
108    /// Base-only fallback after address-scoped and caller-supplied metadata.
109    ///
110    /// Only Base-specific members belong here. The ERC-20, EIP-2612 and AccessControl portions are
111    /// omitted on purpose: the global function map is not network-scoped, so adding competing
112    /// candidates for `transfer`, `balanceOf`, `approve` or `permit` could change how ordinary
113    /// token traces decode in any Base-enabled build. Foundry already has to order
114    /// `IStorageCredits` against `ITIP20` to keep `balanceOf`'s return type stable, which is the
115    /// same hazard. `b20_surface_excludes_erc20_members` pins this boundary.
116    #[sol(abi)]
117    interface IB20Extensions {
118        enum PausableFeature {
119            TRANSFER,
120            MINT,
121            BURN,
122            SEIZE
123        }
124
125        event Memo(address indexed caller, bytes32 indexed memo);
126        event BurnedBlocked(address indexed caller, address indexed from, uint256 amount);
127        event Seized(address indexed caller, address indexed from, address indexed to, uint256 amount);
128        event LastAdminRenounced(address indexed previousAdmin);
129        event Paused(address indexed updater, PausableFeature[] features);
130        event Unpaused(address indexed updater, PausableFeature[] features);
131        event PolicyUpdated(bytes32 indexed policyScope, uint64 oldPolicyId, uint64 newPolicyId);
132
133        function mintWithMemo(address to, uint256 amount, bytes32 memo) external;
134        function burnWithMemo(uint256 amount, bytes32 memo) external;
135        function burnBlocked(address from, uint256 amount) external;
136        function transferWithMemo(address to, uint256 amount, bytes32 memo) external returns (bool);
137        function seizeWithMemo(address from, address to, uint256 amount, bytes32 memo) external;
138        function pausedFeatures() external view returns (PausableFeature[] memory);
139        function isPaused(PausableFeature feature) external view returns (bool);
140        function pause(PausableFeature[] calldata features) external;
141        function unpause(PausableFeature[] calldata features) external;
142        function policyId(bytes32 policyScope) external view returns (uint64);
143        function updatePolicy(bytes32 policyScope, uint64 newPolicyId) external;
144        function contractURI() external view returns (string);
145        function updateContractURI(string calldata newURI) external;
146    }
147}
148
149sol! {
150    /// Mirror of `base_common_precompiles::IPolicyRegistry`, the canonical Cobalt surface.
151    ///
152    /// Only this surface is mirrored: its call set is a strict superset of Beryl's V1, so it also
153    /// names Beryl-era policy traces. `policy_registry_covers_v1_selectors` pins that.
154    #[sol(abi)]
155    interface IPolicyRegistry {
156        enum PolicyType {
157            BLOCKLIST,
158            ALLOWLIST,
159            UNION,
160            INTERSECT
161        }
162
163        error NonPayable();
164        error Unauthorized();
165        error PolicyNotFound();
166        error IncompatiblePolicyType();
167        error ZeroAddress();
168        error BatchSizeTooLarge(uint256 maxBatchSize);
169        error NoPendingAdmin();
170        error ChildPoliciesOutsideOfRange();
171        error InvalidChildPolicy(uint64 childPolicyId);
172
173        event PolicyCreated(uint64 indexed policyId, address indexed creator, PolicyType policyType);
174        event PolicyAdminStaged(uint64 indexed policyId, address indexed currentAdmin, address indexed pendingAdmin);
175        event PolicyAdminUpdated(uint64 indexed policyId, address indexed previousAdmin, address indexed newAdmin);
176        event AllowlistUpdated(uint64 indexed policyId, address indexed updater, bool allowed, address[] accounts);
177        event BlocklistUpdated(uint64 indexed policyId, address indexed updater, bool blocked, address[] accounts);
178        event CompositePolicyUpdated(uint64 indexed policyId, address indexed updater, uint64[] childPolicyIds);
179
180        function createPolicy(address admin, PolicyType policyType) external returns (uint64);
181        function createPolicyWithAccounts(address admin, PolicyType policyType, address[] calldata accounts) external returns (uint64);
182        function createCompositePolicy(address admin, PolicyType policyType, uint64[] calldata childPolicyIds) external returns (uint64);
183        function updateComposite(uint64 policyId, uint64[] calldata childPolicyIds) external;
184        function stageUpdateAdmin(uint64 policyId, address newAdmin) external;
185        function finalizeUpdateAdmin(uint64 policyId) external;
186        function renounceAdmin(uint64 policyId) external;
187        function updateAllowlist(uint64 policyId, bool allowed, address[] calldata accounts) external;
188        function updateBlocklist(uint64 policyId, bool blocked, address[] calldata accounts) external;
189        function isAuthorized(uint64 policyId, address account) external view returns (bool);
190        function MIN_COMPOSITE_CHILD_POLICIES() external view returns (uint256);
191        function MAX_COMPOSITE_CHILD_POLICIES() external view returns (uint256);
192        function policyExists(uint64 policyId) external view returns (bool);
193        function policyAdmin(uint64 policyId) external view returns (address);
194        function pendingPolicyAdmin(uint64 policyId) external view returns (address);
195        function compositePolicyChildIds(uint64 policyId) external view returns (uint64[] memory);
196    }
197}
198
199sol! {
200    /// Mirror of `base_common_precompiles::INonceManager`.
201    #[sol(abi)]
202    interface INonceManager {
203        error DelegateCallNotAllowed();
204        error NonPayable();
205        error ProtocolNonceNotSupported();
206        error InvalidNonceKey();
207        error NonceOverflow();
208        error InvalidExpiringNonceExpiry();
209        error ExpiringNonceReplay();
210        error ExpiringNonceSetFull();
211
212        event NonceIncremented(address indexed account, uint256 indexed nonceKey, uint64 newNonce);
213
214        function getNonce(address account, uint256 nonceKey) external view returns (uint64);
215    }
216}
217
218sol! {
219    /// Mirror of `base_common_precompiles::ITransactionContext`.
220    #[sol(abi)]
221    interface ITransactionContext {
222        error DelegateCallNotAllowed();
223        error NonPayable();
224
225        function getTransactionSender() external view returns (address);
226        function getTransactionPayer() external view returns (address);
227        function getTransactionSenderActorId() external view returns (bytes32);
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use crate::{CallTrace, CallTraceDecoderBuilder};
234    use alloy_dyn_abi::{DynSolValue, FunctionExt};
235    use alloy_primitives::{Address, B256, Bytes, U256};
236    use alloy_sol_types::{SolCall, SolEnum, SolError, SolEvent, SolInterface};
237    use base_common_precompiles::{
238        self as canonical, ActivationRegistryStorage, B20FactoryStorage, NonceManagerStorage,
239        TxContextStorage,
240    };
241    use foundry_evm_hardforks::{BaseUpgrade, FoundryHardfork};
242    use foundry_evm_networks::NetworkConfigs;
243    use revm::interpreter::InstructionResult;
244
245    #[tokio::test]
246    async fn registered_abis_decode_base_precompile_calls() {
247        let decoder = CallTraceDecoderBuilder::new()
248            .with_networks(NetworkConfigs::with_base())
249            .with_hardfork(Some(FoundryHardfork::Base(BaseUpgrade::Cobalt)))
250            .build();
251
252        let activate =
253            super::IActivationRegistry::activateCall { feature: B256::repeat_byte(0x11) };
254        let trace = CallTrace {
255            address: ActivationRegistryStorage::ADDRESS,
256            data: activate.abi_encode().into(),
257            success: true,
258            ..Default::default()
259        };
260        let decoded = decoder.decode_function(&trace).await;
261        assert_eq!(decoded.label.as_deref(), Some("ActivationRegistry"));
262        assert_eq!(
263            decoded.call_data.expect("activate should decode").signature,
264            "activate(bytes32)"
265        );
266
267        let get_nonce = super::INonceManager::getNonceCall {
268            account: Address::repeat_byte(0x22),
269            nonceKey: U256::from(7),
270        };
271        let trace = CallTrace {
272            address: NonceManagerStorage::ADDRESS,
273            data: get_nonce.abi_encode().into(),
274            success: true,
275            ..Default::default()
276        };
277        let decoded = decoder.decode_function(&trace).await;
278        assert_eq!(
279            decoded.call_data.expect("getNonce should decode").signature,
280            "getNonce(address,uint256)"
281        );
282
283        let create = super::IB20Factory::createB20Call {
284            variant: super::IB20Factory::B20Variant::ASSET,
285            salt: B256::repeat_byte(0x33),
286            params: Bytes::new(),
287            initCalls: Vec::new(),
288        };
289        let trace = CallTrace {
290            address: B20FactoryStorage::ADDRESS,
291            data: create.abi_encode().into(),
292            success: true,
293            ..Default::default()
294        };
295        let decoded = decoder.decode_function(&trace).await;
296        assert_eq!(decoded.label.as_deref(), Some("B20Factory"));
297        let signature = decoded.call_data.expect("createB20 should decode").signature;
298        assert!(signature.starts_with("createB20("), "{signature}");
299    }
300
301    #[tokio::test]
302    async fn b20_transfer_with_memo_decodes_bool_output() {
303        let call = super::IB20Extensions::transferWithMemoCall {
304            to: Address::repeat_byte(0x11),
305            amount: U256::from(1),
306            memo: B256::repeat_byte(0x22),
307        };
308        let abi = super::IB20Extensions::abi::contract();
309        let function = abi.functions.get("transferWithMemo").unwrap().first().unwrap();
310        let trace = CallTrace {
311            address: Address::repeat_byte(0x33),
312            data: call.abi_encode().into(),
313            output: function.abi_encode_output(&[DynSolValue::Bool(true)]).unwrap().into(),
314            success: true,
315            ..Default::default()
316        };
317
318        let base =
319            CallTraceDecoderBuilder::new().with_networks(NetworkConfigs::with_base()).build();
320        let decoded = base.decode_function(&trace).await;
321        assert_eq!(
322            decoded.call_data.unwrap().signature,
323            "transferWithMemo(address,uint256,bytes32)"
324        );
325        assert_eq!(decoded.return_data.as_deref(), Some("true"));
326
327        let custom_abi = alloy_json_abi::JsonAbi::parse([
328            "function transferWithMemo(address,uint256,bytes32) returns (uint256)",
329        ])
330        .unwrap();
331        let custom_base = CallTraceDecoderBuilder::new()
332            .with_networks(NetworkConfigs::with_base())
333            .with_abi(&custom_abi)
334            .build();
335        assert_eq!(custom_base.decode_function(&trace).await.return_data.as_deref(), Some("1"));
336
337        let tempo =
338            CallTraceDecoderBuilder::new().with_networks(NetworkConfigs::with_tempo()).build();
339        assert_eq!(tempo.decode_function(&trace).await.return_data, None);
340    }
341
342    #[tokio::test]
343    async fn base_precompile_errors_are_address_scoped() {
344        let trace = CallTrace {
345            address: ActivationRegistryStorage::ADDRESS,
346            output: super::IActivationRegistry::NonPayable {}.abi_encode().into(),
347            success: false,
348            status: Some(InstructionResult::Revert),
349            ..Default::default()
350        };
351        let base = CallTraceDecoderBuilder::new()
352            .with_networks(NetworkConfigs::with_base())
353            .with_hardfork(Some(FoundryHardfork::Base(BaseUpgrade::Beryl)))
354            .build();
355        assert_eq!(base.decode_function(&trace).await.return_data.as_deref(), Some("NonPayable()"));
356
357        let ethereum = CallTraceDecoderBuilder::new()
358            .with_networks(NetworkConfigs::with_ethereum())
359            .with_hardfork(Some(FoundryHardfork::Base(BaseUpgrade::Beryl)))
360            .build();
361        assert_ne!(
362            ethereum.decode_function(&trace).await.return_data.as_deref(),
363            Some("NonPayable()")
364        );
365    }
366
367    /// The transaction-context precompile only exists from Cobalt, so a Beryl decoder must not
368    /// name its calls. This uses a Base-unique selector on purpose: generic names like
369    /// `getNonce(address,uint256)` also exist in Tempo's globally registered nonce ABI and would
370    /// decode regardless of the Base upgrade.
371    #[tokio::test]
372    async fn pre_cobalt_decoder_does_not_register_eip8130_surfaces() {
373        let call = super::ITransactionContext::getTransactionSenderActorIdCall {};
374        let trace = CallTrace {
375            address: TxContextStorage::ADDRESS,
376            data: call.abi_encode().into(),
377            success: true,
378            ..Default::default()
379        };
380
381        let beryl = CallTraceDecoderBuilder::new()
382            .with_networks(NetworkConfigs::with_base())
383            .with_hardfork(Some(FoundryHardfork::Base(BaseUpgrade::Beryl)))
384            .build();
385        assert!(
386            beryl.decode_function(&trace).await.call_data.is_none(),
387            "Beryl must not decode a Cobalt-only precompile"
388        );
389
390        let cobalt = CallTraceDecoderBuilder::new()
391            .with_networks(NetworkConfigs::with_base())
392            .with_hardfork(Some(FoundryHardfork::Base(BaseUpgrade::Cobalt)))
393            .build();
394        assert_eq!(
395            cobalt.decode_function(&trace).await.call_data.expect("Cobalt should decode").signature,
396            "getTransactionSenderActorId()"
397        );
398    }
399
400    #[test]
401    fn activation_registry_abi_matches_canonical_selectors() {
402        assert_eq!(
403            super::IActivationRegistry::activateCall::SELECTOR,
404            canonical::IActivationRegistry::activateCall::SELECTOR
405        );
406        assert_eq!(
407            super::IActivationRegistry::deactivateCall::SELECTOR,
408            canonical::IActivationRegistry::deactivateCall::SELECTOR
409        );
410        assert_eq!(
411            super::IActivationRegistry::isActivatedCall::SELECTOR,
412            canonical::IActivationRegistry::isActivatedCall::SELECTOR
413        );
414        assert_eq!(
415            super::IActivationRegistry::checkActivatedCall::SELECTOR,
416            canonical::IActivationRegistry::checkActivatedCall::SELECTOR
417        );
418        assert_eq!(
419            super::IActivationRegistry::adminCall::SELECTOR,
420            canonical::IActivationRegistry::adminCall::SELECTOR
421        );
422        assert_eq!(
423            super::IActivationRegistry::setAdminCall::SELECTOR,
424            canonical::IActivationRegistry::setAdminCall::SELECTOR
425        );
426        assert_eq!(
427            super::IActivationRegistry::Unauthorized::SELECTOR,
428            canonical::IActivationRegistry::Unauthorized::SELECTOR
429        );
430        assert_eq!(
431            super::IActivationRegistry::StaticCallNotAllowed::SELECTOR,
432            canonical::IActivationRegistry::StaticCallNotAllowed::SELECTOR
433        );
434        assert_eq!(
435            super::IActivationRegistry::NonPayable::SELECTOR,
436            canonical::IActivationRegistry::NonPayable::SELECTOR
437        );
438        assert_eq!(
439            super::IActivationRegistry::FeatureActivated::SIGNATURE_HASH,
440            canonical::IActivationRegistry::FeatureActivated::SIGNATURE_HASH
441        );
442        assert_eq!(
443            super::IActivationRegistry::AdminChanged::SIGNATURE_HASH,
444            canonical::IActivationRegistry::AdminChanged::SIGNATURE_HASH
445        );
446    }
447
448    /// Base pins this surface with an `AbiFingerprint`, but that type is only exported under its
449    /// `test-utils` feature, so this compares against the canonical types directly instead.
450    /// `B20Variant`'s ordinals are asserted explicitly because they are the one thing selectors
451    /// cannot catch: Solidity encodes enums as `uint8`, so a reorder moves no signature, yet the
452    /// ordinal is byte `[10]` of every token address the factory deploys.
453    #[test]
454    fn b20_factory_abi_matches_canonical_surface() {
455        assert_eq!(
456            super::IB20Factory::createB20Call::SELECTOR,
457            canonical::IB20Factory::createB20Call::SELECTOR
458        );
459        assert_eq!(
460            super::IB20Factory::getB20AddressCall::SELECTOR,
461            canonical::IB20Factory::getB20AddressCall::SELECTOR
462        );
463        assert_eq!(
464            super::IB20Factory::isB20Call::SELECTOR,
465            canonical::IB20Factory::isB20Call::SELECTOR
466        );
467        assert_eq!(
468            super::IB20Factory::isB20InitializedCall::SELECTOR,
469            canonical::IB20Factory::isB20InitializedCall::SELECTOR
470        );
471        assert_eq!(
472            super::IB20Factory::TokenAlreadyExists::SELECTOR,
473            canonical::IB20Factory::TokenAlreadyExists::SELECTOR
474        );
475        assert_eq!(
476            super::IB20Factory::UnsupportedVersion::SELECTOR,
477            canonical::IB20Factory::UnsupportedVersion::SELECTOR
478        );
479        assert_eq!(
480            super::IB20Factory::B20Created::SIGNATURE_HASH,
481            canonical::IB20Factory::B20Created::SIGNATURE_HASH
482        );
483
484        assert_eq!(
485            super::IB20Factory::B20Variant::COUNT,
486            canonical::IB20Factory::B20Variant::COUNT
487        );
488        assert_eq!(
489            super::IB20Factory::B20Variant::ASSET as u8,
490            canonical::IB20Factory::B20Variant::ASSET as u8
491        );
492        assert_eq!(
493            super::IB20Factory::B20Variant::STABLECOIN as u8,
494            canonical::IB20Factory::B20Variant::STABLECOIN as u8
495        );
496    }
497
498    /// `PolicyType` ordinals ride the top byte of every policy ID, so they are asserted alongside
499    /// the selectors for the same reason as `B20Variant`.
500    #[test]
501    fn policy_registry_abi_matches_canonical_surface() {
502        assert_eq!(
503            super::IPolicyRegistry::createPolicyCall::SELECTOR,
504            canonical::IPolicyRegistry::createPolicyCall::SELECTOR
505        );
506        assert_eq!(
507            super::IPolicyRegistry::createCompositePolicyCall::SELECTOR,
508            canonical::IPolicyRegistry::createCompositePolicyCall::SELECTOR
509        );
510        assert_eq!(
511            super::IPolicyRegistry::isAuthorizedCall::SELECTOR,
512            canonical::IPolicyRegistry::isAuthorizedCall::SELECTOR
513        );
514        assert_eq!(
515            super::IPolicyRegistry::updateAllowlistCall::SELECTOR,
516            canonical::IPolicyRegistry::updateAllowlistCall::SELECTOR
517        );
518        assert_eq!(
519            super::IPolicyRegistry::InvalidChildPolicy::SELECTOR,
520            canonical::IPolicyRegistry::InvalidChildPolicy::SELECTOR
521        );
522        assert_eq!(
523            super::IPolicyRegistry::PolicyCreated::SIGNATURE_HASH,
524            canonical::IPolicyRegistry::PolicyCreated::SIGNATURE_HASH
525        );
526        assert_eq!(
527            super::IPolicyRegistry::CompositePolicyUpdated::SIGNATURE_HASH,
528            canonical::IPolicyRegistry::CompositePolicyUpdated::SIGNATURE_HASH
529        );
530
531        assert_eq!(
532            super::IPolicyRegistry::PolicyType::COUNT,
533            canonical::IPolicyRegistry::PolicyType::COUNT
534        );
535        for (mirror, expected) in [
536            (
537                super::IPolicyRegistry::PolicyType::BLOCKLIST as u8,
538                canonical::IPolicyRegistry::PolicyType::BLOCKLIST as u8,
539            ),
540            (
541                super::IPolicyRegistry::PolicyType::ALLOWLIST as u8,
542                canonical::IPolicyRegistry::PolicyType::ALLOWLIST as u8,
543            ),
544            (
545                super::IPolicyRegistry::PolicyType::UNION as u8,
546                canonical::IPolicyRegistry::PolicyType::UNION as u8,
547            ),
548            (
549                super::IPolicyRegistry::PolicyType::INTERSECT as u8,
550                canonical::IPolicyRegistry::PolicyType::INTERSECT as u8,
551            ),
552        ] {
553            assert_eq!(mirror, expected);
554        }
555    }
556
557    /// Mirroring only the Cobalt surface is safe because it is a strict superset of Beryl's. If
558    /// Base ever diverges a shared signature, this fails and V1 needs its own mirror.
559    #[test]
560    fn policy_registry_covers_v1_selectors() {
561        let mirrored: Vec<[u8; 4]> =
562            super::IPolicyRegistry::IPolicyRegistryCalls::selectors().collect();
563        for selector in canonical::IPolicyRegistryV1::IPolicyRegistryCalls::selectors() {
564            assert!(
565                mirrored.contains(&selector),
566                "Beryl selector {selector:?} is absent from the mirrored Cobalt surface"
567            );
568        }
569    }
570
571    #[test]
572    fn b20_extensions_abi_matches_canonical_surface() {
573        assert_eq!(
574            super::IB20Extensions::mintWithMemoCall::SELECTOR,
575            canonical::IB20::mintWithMemoCall::SELECTOR
576        );
577        assert_eq!(
578            super::IB20Extensions::burnWithMemoCall::SELECTOR,
579            canonical::IB20::burnWithMemoCall::SELECTOR
580        );
581        assert_eq!(
582            super::IB20Extensions::burnBlockedCall::SELECTOR,
583            canonical::IB20::burnBlockedCall::SELECTOR
584        );
585        assert_eq!(
586            super::IB20Extensions::seizeWithMemoCall::SELECTOR,
587            canonical::IB20::seizeWithMemoCall::SELECTOR
588        );
589        assert_eq!(
590            super::IB20Extensions::policyIdCall::SELECTOR,
591            canonical::IB20::policyIdCall::SELECTOR
592        );
593        assert_eq!(
594            super::IB20Extensions::pauseCall::SELECTOR,
595            canonical::IB20::pauseCall::SELECTOR
596        );
597        assert_eq!(
598            super::IB20Extensions::contractURICall::SELECTOR,
599            canonical::IB20::contractURICall::SELECTOR
600        );
601        assert_eq!(
602            super::IB20Extensions::Seized::SIGNATURE_HASH,
603            canonical::IB20::Seized::SIGNATURE_HASH
604        );
605        assert_eq!(
606            super::IB20Extensions::Memo::SIGNATURE_HASH,
607            canonical::IB20::Memo::SIGNATURE_HASH
608        );
609        assert_eq!(
610            super::IB20Extensions::PolicyUpdated::SIGNATURE_HASH,
611            canonical::IB20::PolicyUpdated::SIGNATURE_HASH
612        );
613
614        // Pausable ordinals are load-bearing in `pause`/`unpause` calldata.
615        assert_eq!(
616            super::IB20Extensions::PausableFeature::COUNT,
617            canonical::IB20::PausableFeature::COUNT
618        );
619        assert_eq!(
620            super::IB20Extensions::PausableFeature::SEIZE as u8,
621            canonical::IB20::PausableFeature::SEIZE as u8
622        );
623    }
624
625    /// This surface is registered globally rather than per address, so it must never carry the
626    /// ERC-20, EIP-2612 or AccessControl members: a competing candidate for those selectors could
627    /// change how ordinary token traces decode on every network in a Base-enabled build.
628    #[test]
629    fn b20_surface_excludes_erc20_members() {
630        let mirrored: Vec<[u8; 4]> =
631            super::IB20Extensions::IB20ExtensionsCalls::selectors().collect();
632        for excluded in [
633            canonical::IB20::transferCall::SELECTOR,
634            canonical::IB20::transferFromCall::SELECTOR,
635            canonical::IB20::approveCall::SELECTOR,
636            canonical::IB20::balanceOfCall::SELECTOR,
637            canonical::IB20::allowanceCall::SELECTOR,
638            canonical::IB20::permitCall::SELECTOR,
639            canonical::IB20::hasRoleCall::SELECTOR,
640            canonical::IB20::grantRoleCall::SELECTOR,
641        ] {
642            assert!(
643                !mirrored.contains(&excluded),
644                "standard selector {excluded:?} must stay out of the global map"
645            );
646        }
647    }
648
649    #[test]
650    fn nonce_manager_abi_matches_canonical_selectors() {
651        assert_eq!(
652            super::INonceManager::getNonceCall::SELECTOR,
653            canonical::INonceManager::getNonceCall::SELECTOR
654        );
655        assert_eq!(
656            super::INonceManager::ExpiringNonceReplay::SELECTOR,
657            canonical::INonceManager::ExpiringNonceReplay::SELECTOR
658        );
659        assert_eq!(
660            super::INonceManager::InvalidExpiringNonceExpiry::SELECTOR,
661            canonical::INonceManager::InvalidExpiringNonceExpiry::SELECTOR
662        );
663        assert_eq!(
664            super::INonceManager::NonceIncremented::SIGNATURE_HASH,
665            canonical::INonceManager::NonceIncremented::SIGNATURE_HASH
666        );
667    }
668
669    #[test]
670    fn transaction_context_abi_matches_canonical_selectors() {
671        assert_eq!(
672            super::ITransactionContext::getTransactionSenderCall::SELECTOR,
673            canonical::ITransactionContext::getTransactionSenderCall::SELECTOR
674        );
675        assert_eq!(
676            super::ITransactionContext::getTransactionPayerCall::SELECTOR,
677            canonical::ITransactionContext::getTransactionPayerCall::SELECTOR
678        );
679        assert_eq!(
680            super::ITransactionContext::getTransactionSenderActorIdCall::SELECTOR,
681            canonical::ITransactionContext::getTransactionSenderActorIdCall::SELECTOR
682        );
683    }
684}