Skip to main content

tempo_precompiles/tip403_registry/
mod.rs

1//! [TIP-403] transfer policy registry precompile.
2//!
3//! Manages whitelist, blacklist, and compound transfer policies that TIP-20
4//! tokens reference to gate sender/recipient authorization.
5//!
6//! [TIP-403]: <https://docs.tempo.xyz/protocol/tip403>
7
8pub mod dispatch;
9
10// Re-export the generated slots module for external access to storage slot constants.
11pub use slots as tip403_registry_slots;
12
13use crate::{
14    StorageCtx, is_precompile_address,
15    receive_policy_guard::{RECOVERY_ORIGINATOR, RecoveryMode},
16};
17pub use tempo_contracts::precompiles::{
18    ITIP403Registry::{self, PolicyType},
19    TIP403RegistryError, TIP403RegistryEvent,
20};
21use tempo_precompiles_macros::{Storable, contract};
22
23use crate::{
24    RECEIVE_POLICY_GUARD_ADDRESS, TIP403_REGISTRY_ADDRESS,
25    error::{Result, TempoPrecompileError},
26    storage::{Handler, Mapping},
27};
28use alloy::primitives::Address;
29use tempo_chainspec::hardfork::TempoHardfork;
30use tempo_primitives::TempoAddressExt;
31
32/// Built-in policy ID that always rejects authorization.
33pub const REJECT_ALL_POLICY_ID: u64 = 0;
34
35/// Built-in policy ID that always allows authorization.
36pub const ALLOW_ALL_POLICY_ID: u64 = 1;
37
38/// System addresses that cannot be policed.
39pub const ALWAYS_AUTHORIZED: &[(TempoHardfork, &[Address])] =
40    &[(TempoHardfork::T6, &[RECEIVE_POLICY_GUARD_ADDRESS])];
41
42/// Registry for [TIP-403] transfer policies. TIP20 tokens reference an ID from this registry
43/// to police transfers between sender and receiver addresses.
44///
45/// [TIP-403]: <https://docs.tempo.xyz/protocol/tip403>
46///
47/// The struct fields define the on-chain storage layout; the `#[contract]` macro generates the
48/// storage handlers which provide an ergonomic way to interact with the EVM state.
49#[contract(addr = TIP403_REGISTRY_ADDRESS)]
50pub struct TIP403Registry {
51    /// Monotonically increasing counter for policy IDs. Starts at `2` because IDs `0`
52    /// ([`REJECT_ALL_POLICY_ID`]) and `1` ([`ALLOW_ALL_POLICY_ID`]) are reserved special
53    /// policies.
54    policy_id_counter: u64,
55    /// Maps a policy ID to its [`PolicyRecord`], which stores the base [`PolicyData`] and, for
56    /// compound policies, the [`CompoundPolicyData`] sub-policy references.
57    policy_records: Mapping<u64, PolicyRecord>,
58    /// Per-policy address set used by simple (non-compound) policies. For whitelists the
59    /// value is `true` when the address is allowed; for blacklists it is `true` when the
60    /// address is restricted.
61    policy_set: Mapping<u64, Mapping<Address, bool>>,
62    /// Account receive policy configuration.
63    receive_policies: Mapping<Address, ReceivePolicy>,
64}
65
66/// Per-account TIP-1028 receive policy configuration.
67#[derive(Debug, Clone, Default, Storable)]
68struct ReceivePolicy {
69    config: ReceivePolicyConfig,
70    recovery_address: Address,
71}
72
73/// Per-account TIP-1028 receive policy configuration. Packed in a single slot.
74#[derive(Debug, Clone, Default, Storable)]
75struct ReceivePolicyConfig {
76    /// Whether the account has configured a receive policy.
77    has_receive_policy: bool,
78    /// Policy ID authorizing senders for inbound transfers.
79    sender_policy_id: u64,
80    /// Type of the sender policy ID; either WHITELIST, or BLACKLIST.
81    sender_policy_type: u8,
82    /// Policy ID filtering which tokens may be received.
83    token_filter_id: u64,
84    /// Type of the token policy ID; either WHITELIST, or BLACKLIST.
85    token_filter_type: u8,
86    /// Recovery authority for blocked inbound funds.
87    recovery_mode: RecoveryMode,
88}
89
90/// Policy record containing base data and optional data for compound policies ([TIP-1015])
91///
92/// [TIP-1015]: <https://docs.tempo.xyz/protocol/tips/tip-1015>
93#[derive(Debug, Clone, Storable)]
94pub struct PolicyRecord {
95    /// Base policy data
96    pub base: PolicyData,
97    /// Compound policy data. Only relevant when `base.policy_type == COMPOUND`
98    pub compound: CompoundPolicyData,
99}
100
101/// Data for compound policies ([TIP-1015])
102///
103/// [TIP-1015]: <https://docs.tempo.xyz/protocol/tips/tip-1015>
104#[derive(Debug, Clone, Default, Storable)]
105pub struct CompoundPolicyData {
106    /// Sub-policy ID used to authorize the sender.
107    pub sender_policy_id: u64,
108    /// Sub-policy ID used to authorize the recipient.
109    pub recipient_policy_id: u64,
110    /// Sub-policy ID used to authorize mint recipients.
111    pub mint_recipient_policy_id: u64,
112}
113
114/// Authorization role for policy checks.
115///
116/// - `Transfer` (symmetric sender/recipient) available since `Genesis`.
117/// - Directional roles (`Sender`, `Recipient`, `MintRecipient`) for compound policies available since `T2`.
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub enum AuthRole {
120    /// Check both sender AND recipient. Used for `isAuthorized` calls (spec: pre T2).
121    Transfer,
122    /// Check sender authorization only (spec: +T2).
123    Sender,
124    /// Check recipient authorization only (spec: +T2).
125    Recipient,
126    /// Check mint recipient authorization only (spec: +T2).
127    MintRecipient,
128}
129
130/// TIP403 policy data
131#[derive(Debug, Clone, Storable)]
132pub struct PolicyData {
133    /// Policy type, either whitelist, blacklist or compound.
134    ///
135    /// IMPORTANT: `policy_type` is immutable after policy creation. TIP-1028 receive policies
136    /// cache this value and rely on immutability for correctness and gas savings.
137    pub policy_type: u8,
138    /// Address authorized to modify this policy.
139    pub admin: Address,
140}
141
142impl PolicyData {
143    /// Decodes the raw `policy_type` u8 to a `PolicyType` enum.
144    fn policy_type(&self) -> Result<PolicyType> {
145        let is_t2 = StorageCtx.spec().is_t2();
146
147        match self.policy_type.try_into() {
148            Ok(ty) if is_t2 || ty != PolicyType::COMPOUND => Ok(ty),
149            _ => Err(if is_t2 {
150                TIP403RegistryError::invalid_policy_type().into()
151            } else {
152                TempoPrecompileError::under_overflow()
153            }),
154        }
155    }
156
157    /// Returns `true` if the policy type is a simple policy (WHITELIST or BLACKLIST).
158    fn is_simple(&self) -> bool {
159        self.policy_type == PolicyType::WHITELIST as u8
160            || self.policy_type == PolicyType::BLACKLIST as u8
161    }
162
163    /// Returns `true` if the policy data indicates a compound policy
164    pub fn is_compound(&self) -> bool {
165        self.policy_type == PolicyType::COMPOUND as u8
166    }
167
168    /// Returns `true` if the policy data is the default (uninitialized) value.
169    fn is_default(&self) -> bool {
170        self.policy_type == 0 && self.admin == Address::ZERO
171    }
172}
173
174impl ReceivePolicyConfig {
175    fn sender_policy_data(&self) -> PolicyData {
176        PolicyData {
177            policy_type: self.sender_policy_type,
178            admin: Address::ZERO,
179        }
180    }
181
182    fn token_filter_data(&self) -> PolicyData {
183        PolicyData {
184            policy_type: self.token_filter_type,
185            admin: Address::ZERO,
186        }
187    }
188
189    fn sender_policy_type(&self) -> Result<PolicyType> {
190        self.sender_policy_type
191            .try_into()
192            .map_err(|_| TIP403RegistryError::invalid_receive_policy_type().into())
193    }
194
195    fn token_filter_type(&self) -> Result<PolicyType> {
196        self.token_filter_type
197            .try_into()
198            .map_err(|_| TIP403RegistryError::invalid_receive_policy_type().into())
199    }
200}
201
202impl TIP403Registry {
203    /// Initializes the TIP-403 registry precompile.
204    pub fn initialize(&mut self) -> Result<()> {
205        self.__initialize()
206    }
207
208    /// Returns the next policy ID to be assigned (always ≥ 2, since IDs 0 and 1 are reserved).
209    pub fn policy_id_counter(&self) -> Result<u64> {
210        // Skips the built-in policy IDs, when initializing the counter for the first time.
211        self.policy_id_counter.read().map(|counter| counter.max(2))
212    }
213
214    /// Returns `true` if the given policy ID exists (built-in or user-created).
215    pub fn policy_exists(&self, call: ITIP403Registry::policyExistsCall) -> Result<bool> {
216        // Built-in policies (0 and 1) always exist
217        if self.builtin_authorization(call.policyId).is_some() {
218            return Ok(true);
219        }
220
221        // Check if policy ID is within the range of created policies
222        let counter = self.policy_id_counter()?;
223        Ok(call.policyId < counter)
224    }
225
226    /// Returns the type and admin of a policy. Reverts if the policy does not exist or has an
227    /// invalid type.
228    ///
229    /// # Errors
230    /// - `PolicyNotFound` — the policy ID does not exist
231    /// - `InvalidPolicyType` — stored type cannot be decoded (e.g. pre-T1 `COMPOUND` on T2+)
232    pub fn policy_data(
233        &self,
234        call: ITIP403Registry::policyDataCall,
235    ) -> Result<ITIP403Registry::policyDataReturn> {
236        if self.storage.spec().is_t2() {
237            // Built-in policies are virtual (not stored), and match the `PolicyType`:
238            //  - 0: REJECT_ALL_POLICY_ID → WHITELIST
239            //  - 1: ALLOW_ALL_POLICY_ID  → BLACKLIST
240            if self.builtin_authorization(call.policyId).is_some() {
241                return Ok(ITIP403Registry::policyDataReturn {
242                    policyType: (call.policyId as u8)
243                        .try_into()
244                        .map_err(|_| TIP403RegistryError::invalid_policy_type())?,
245                    admin: Address::ZERO,
246                });
247            }
248        } else {
249            // Check if policy exists before reading the data (spec: pre-T2)
250            if !self.policy_exists(ITIP403Registry::policyExistsCall {
251                policyId: call.policyId,
252            })? {
253                return Err(TIP403RegistryError::policy_not_found().into());
254            }
255        }
256
257        // Get policy data and verify that the policy id exists (spec: +T2)
258        let data = self.get_policy_data(call.policyId)?;
259
260        Ok(ITIP403Registry::policyDataReturn {
261            policyType: data.policy_type()?,
262            admin: data.admin,
263        })
264    }
265
266    /// Returns the sub-policy IDs of a compound policy ([TIP-1015]).
267    ///
268    /// [TIP-1015]: <https://docs.tempo.xyz/protocol/tips/tip-1015>
269    ///
270    /// # Errors
271    /// - `IncompatiblePolicyType` — the policy exists but is not compound
272    /// - `PolicyNotFound` — the policy ID does not exist
273    pub fn compound_policy_data(
274        &self,
275        call: ITIP403Registry::compoundPolicyDataCall,
276    ) -> Result<ITIP403Registry::compoundPolicyDataReturn> {
277        let data = self.get_policy_data(call.policyId)?;
278
279        // Only compound policies have compound data
280        if !data.is_compound() {
281            // Check if the policy exists for error clarity
282            let err = if self.policy_exists(ITIP403Registry::policyExistsCall {
283                policyId: call.policyId,
284            })? {
285                TIP403RegistryError::incompatible_policy_type()
286            } else {
287                TIP403RegistryError::policy_not_found()
288            };
289            return Err(err.into());
290        }
291
292        let compound = self.policy_records[call.policyId].compound.read()?;
293        Ok(ITIP403Registry::compoundPolicyDataReturn {
294            senderPolicyId: compound.sender_policy_id,
295            recipientPolicyId: compound.recipient_policy_id,
296            mintRecipientPolicyId: compound.mint_recipient_policy_id,
297        })
298    }
299
300    /// Returns `account`'s receive-policy configuration.
301    pub fn receive_policy(&self, account: Address) -> Result<ITIP403Registry::receivePolicyReturn> {
302        let config = self.receive_policies[account].config.read()?;
303        Ok(ITIP403Registry::receivePolicyReturn {
304            hasReceivePolicy: config.has_receive_policy,
305            senderPolicyId: config.sender_policy_id,
306            senderPolicyType: config.sender_policy_type()?,
307            tokenFilterId: config.token_filter_id,
308            tokenFilterType: config.token_filter_type()?,
309            recoveryAuthority: self.receive_policy_recovery(account, config.recovery_mode)?,
310        })
311    }
312
313    /// Checks `receiver`'s receive policy for an inbound transfer. Returns the blocking
314    /// reason, or `None` if authorized.
315    pub fn validate_receive_policy(
316        &self,
317        token: Address,
318        sender: Address,
319        receiver: Address,
320    ) -> Result<Option<ITIP403Registry::BlockedReason>> {
321        Ok(self
322            .check_receive_policy(token, sender, receiver)?
323            .map(|(reason, _)| reason))
324    }
325
326    /// Checks the receive policy. If valid, returns `None`. Otherwise returns the
327    /// blocking reason and recovery address.
328    pub(crate) fn check_receive_policy(
329        &self,
330        token: Address,
331        sender: Address,
332        receiver: Address,
333    ) -> Result<Option<(ITIP403Registry::BlockedReason, Address)>> {
334        let config = self.receive_policies[receiver].config.read()?;
335        if !config.has_receive_policy {
336            return Ok(None);
337        }
338
339        let token_filter_data = config.token_filter_data();
340        if !self.is_authorized_simple(config.token_filter_id, token, Some(token_filter_data))? {
341            let recovery_address = self.receive_policy_recovery(receiver, config.recovery_mode)?;
342            return Ok(Some((
343                ITIP403Registry::BlockedReason::TOKEN_FILTER,
344                recovery_address,
345            )));
346        }
347
348        let sender_policy_data = config.sender_policy_data();
349        if !self.is_authorized_simple(config.sender_policy_id, sender, Some(sender_policy_data))? {
350            let recovery_address = self.receive_policy_recovery(receiver, config.recovery_mode)?;
351            return Ok(Some((
352                ITIP403Registry::BlockedReason::RECEIVE_POLICY,
353                recovery_address,
354            )));
355        }
356
357        Ok(None)
358    }
359
360    /// Returns the recovery authority encoded by `mode` for `account`.
361    /// Originator and receiver modes return sentinel addresses; third-party mode reads storage.
362    fn receive_policy_recovery(&self, account: Address, mode: RecoveryMode) -> Result<Address> {
363        match mode {
364            RecoveryMode::Originator => Ok(RECOVERY_ORIGINATOR),
365            RecoveryMode::Receiver => Ok(account),
366            RecoveryMode::ThirdParty => self.receive_policies[account].recovery_address.read(),
367        }
368    }
369
370    /// Creates a new simple (whitelist or blacklist) policy and returns its ID.
371    ///
372    /// # Errors
373    /// - `IncompatiblePolicyType` — `policyType` is not `WHITELIST` or `BLACKLIST` (T2+)
374    /// - `UnderOverflow` — policy ID counter overflows
375    pub fn create_policy(
376        &mut self,
377        msg_sender: Address,
378        call: ITIP403Registry::createPolicyCall,
379    ) -> Result<u64> {
380        let policy_type = call.policyType.ensure_is_simple()?;
381
382        let new_policy_id = self.policy_id_counter()?;
383
384        // Increment counter
385        self.policy_id_counter.write(
386            new_policy_id
387                .checked_add(1)
388                .ok_or(TempoPrecompileError::under_overflow())?,
389        )?;
390
391        // Store policy data
392        self.policy_records[new_policy_id].base.write(PolicyData {
393            policy_type,
394            admin: call.admin,
395        })?;
396
397        self.emit_event(TIP403RegistryEvent::policy_created(
398            new_policy_id,
399            msg_sender,
400            policy_type.try_into().unwrap_or(PolicyType::__Invalid),
401        ))?;
402
403        self.emit_event(TIP403RegistryEvent::policy_admin_updated(
404            new_policy_id,
405            msg_sender,
406            call.admin,
407        ))?;
408
409        Ok(new_policy_id)
410    }
411
412    /// Sets the caller's TIP-1028 receive policy.
413    pub fn set_receive_policy(
414        &mut self,
415        msg_sender: Address,
416        call: ITIP403Registry::setReceivePolicyCall,
417    ) -> Result<()> {
418        if msg_sender.is_virtual() {
419            return Err(TIP403RegistryError::virtual_address_not_allowed().into());
420        }
421
422        let recovery_address = call.recoveryAuthority;
423        let (recovery_mode, recovery_write) = RecoveryMode::encode(recovery_address, msg_sender);
424
425        if is_precompile_address(recovery_address, self.storage.spec())
426            || recovery_address.is_virtual()
427        {
428            return Err(TIP403RegistryError::invalid_recovery_authority().into());
429        }
430
431        let sender_policy_type = self.validate_receive_policy_id(call.senderPolicyId)?;
432        let token_filter_type = self.validate_receive_policy_id(call.tokenFilterId)?;
433
434        let config = ReceivePolicyConfig {
435            has_receive_policy: true,
436            sender_policy_id: call.senderPolicyId,
437            sender_policy_type,
438            token_filter_id: call.tokenFilterId,
439            token_filter_type,
440            recovery_mode,
441        };
442        self.receive_policies[msg_sender].config.write(config)?;
443        self.receive_policies[msg_sender]
444            .recovery_address
445            .write(recovery_write)?;
446
447        self.emit_event(TIP403RegistryEvent::ReceivePolicyUpdated(
448            ITIP403Registry::ReceivePolicyUpdated {
449                account: msg_sender,
450                senderPolicyId: call.senderPolicyId,
451                tokenFilterId: call.tokenFilterId,
452                recoveryAuthority: recovery_address,
453            },
454        ))
455    }
456
457    /// Creates a simple policy and pre-populates it with an initial set of accounts.
458    ///
459    /// # Errors
460    /// - `UnderOverflow` — policy ID counter overflows
461    /// - `IncompatiblePolicyType` — `policyType` is not `WHITELIST` or `BLACKLIST` (T2+), or
462    ///   accounts are non-empty for compound/invalid types (pre-T2)
463    /// - `VirtualAddressNotAllowed` — virtual addresses are forbidden (T3+)
464    pub fn create_policy_with_accounts(
465        &mut self,
466        msg_sender: Address,
467        call: ITIP403Registry::createPolicyWithAccountsCall,
468    ) -> Result<u64> {
469        let admin = call.admin;
470        let policy_type = call.policyType.ensure_is_simple()?;
471
472        // TIP-1022: reject virtual addresses in initial account set (spec T3+)
473        if self.storage.spec().is_t3() {
474            for account in call.accounts.iter() {
475                if account.is_virtual() {
476                    return Err(TIP403RegistryError::virtual_address_not_allowed().into());
477                }
478            }
479        }
480
481        let new_policy_id = self.policy_id_counter()?;
482
483        // Increment counter
484        self.policy_id_counter.write(
485            new_policy_id
486                .checked_add(1)
487                .ok_or(TempoPrecompileError::under_overflow())?,
488        )?;
489
490        // Store policy data
491        self.set_policy_data(new_policy_id, PolicyData { policy_type, admin })?;
492
493        // Set initial accounts - only emit events for valid policy types
494        // Pre-T2 with invalid types: accounts are added but no events emitted (matches original)
495        for account in call.accounts.iter() {
496            self.set_policy_set(new_policy_id, *account, true)?;
497
498            match call.policyType {
499                PolicyType::WHITELIST => {
500                    self.emit_event(TIP403RegistryEvent::whitelist_updated(
501                        new_policy_id,
502                        msg_sender,
503                        *account,
504                        true,
505                    ))?;
506                }
507                PolicyType::BLACKLIST => {
508                    self.emit_event(TIP403RegistryEvent::blacklist_updated(
509                        new_policy_id,
510                        msg_sender,
511                        *account,
512                        true,
513                    ))?;
514                }
515                ITIP403Registry::PolicyType::COMPOUND | ITIP403Registry::PolicyType::__Invalid => {
516                    // T2+: unreachable since `ensure_is_simple` already rejected
517                    return Err(TIP403RegistryError::incompatible_policy_type().into());
518                }
519            }
520        }
521
522        self.emit_event(TIP403RegistryEvent::policy_created(
523            new_policy_id,
524            msg_sender,
525            policy_type.try_into().unwrap_or(PolicyType::__Invalid),
526        ))?;
527
528        self.emit_event(TIP403RegistryEvent::policy_admin_updated(
529            new_policy_id,
530            msg_sender,
531            admin,
532        ))?;
533
534        Ok(new_policy_id)
535    }
536
537    /// Transfers admin control of a policy. Only callable by the current admin.
538    ///
539    /// # Errors
540    /// - `Unauthorized` — `msg_sender` is not the current admin
541    /// - `PolicyNotFound` — the policy ID does not exist (T2+)
542    pub fn set_policy_admin(
543        &mut self,
544        msg_sender: Address,
545        call: ITIP403Registry::setPolicyAdminCall,
546    ) -> Result<()> {
547        let data = self.get_policy_data(call.policyId)?;
548
549        // Check authorization
550        if data.admin != msg_sender {
551            return Err(TIP403RegistryError::unauthorized().into());
552        }
553
554        // Update admin policy ID
555        self.set_policy_data(
556            call.policyId,
557            PolicyData {
558                admin: call.admin,
559                ..data
560            },
561        )?;
562
563        self.emit_event(TIP403RegistryEvent::policy_admin_updated(
564            call.policyId,
565            msg_sender,
566            call.admin,
567        ))
568    }
569
570    /// Adds or removes an account from a whitelist policy. Admin-only.
571    ///
572    /// # Errors
573    /// - `Unauthorized` — `msg_sender` is not the policy admin
574    /// - `IncompatiblePolicyType` — the policy is not a whitelist
575    /// - `PolicyNotFound` — the policy ID does not exist (T2+)
576    /// - `VirtualAddressNotAllowed` — virtual addresses are forbidden (T3+)
577    pub fn modify_policy_whitelist(
578        &mut self,
579        msg_sender: Address,
580        call: ITIP403Registry::modifyPolicyWhitelistCall,
581    ) -> Result<()> {
582        // TIP-1022: virtual addresses are forwarding aliases, not valid policy members (spec: T3+)
583        if self.storage.spec().is_t3() && call.account.is_virtual() {
584            return Err(TIP403RegistryError::virtual_address_not_allowed().into());
585        }
586
587        let data = self.get_policy_data(call.policyId)?;
588
589        // Check authorization
590        if data.admin != msg_sender {
591            return Err(TIP403RegistryError::unauthorized().into());
592        }
593
594        // Check policy type
595        if !matches!(data.policy_type()?, PolicyType::WHITELIST) {
596            return Err(TIP403RegistryError::incompatible_policy_type().into());
597        }
598
599        self.set_policy_set(call.policyId, call.account, call.allowed)?;
600
601        self.emit_event(TIP403RegistryEvent::whitelist_updated(
602            call.policyId,
603            msg_sender,
604            call.account,
605            call.allowed,
606        ))
607    }
608
609    /// Adds or removes an account from a blacklist policy. Admin-only.
610    ///
611    /// # Errors
612    /// - `Unauthorized` — `msg_sender` is not the policy admin
613    /// - `IncompatiblePolicyType` — the policy is not a blacklist
614    /// - `PolicyNotFound` — the policy ID does not exist (T2+)
615    /// - `VirtualAddressNotAllowed` — virtual addresses are forbidden (T3+)
616    pub fn modify_policy_blacklist(
617        &mut self,
618        msg_sender: Address,
619        call: ITIP403Registry::modifyPolicyBlacklistCall,
620    ) -> Result<()> {
621        // TIP-1022: virtual addresses are forwarding aliases, not valid policy members (spec: T3+)
622        if self.storage.spec().is_t3() && call.account.is_virtual() {
623            return Err(TIP403RegistryError::virtual_address_not_allowed().into());
624        }
625
626        let data = self.get_policy_data(call.policyId)?;
627
628        // Check authorization
629        if data.admin != msg_sender {
630            return Err(TIP403RegistryError::unauthorized().into());
631        }
632
633        // Check policy type
634        if !matches!(data.policy_type()?, PolicyType::BLACKLIST) {
635            return Err(TIP403RegistryError::incompatible_policy_type().into());
636        }
637
638        self.set_policy_set(call.policyId, call.account, call.restricted)?;
639
640        self.emit_event(TIP403RegistryEvent::blacklist_updated(
641            call.policyId,
642            msg_sender,
643            call.account,
644            call.restricted,
645        ))
646    }
647
648    /// Creates a new compound policy that references three simple sub-policies ([TIP-1015]).
649    /// Compound policies have no admin and cannot be modified after creation.
650    ///
651    /// [TIP-1015]: <https://docs.tempo.xyz/protocol/tips/tip-1015>
652    ///
653    /// # Errors
654    /// - `PolicyNotFound` — a referenced sub-policy ID does not exist
655    /// - `PolicyNotSimple` — a referenced sub-policy is itself compound
656    /// - `UnderOverflow` — policy ID counter overflows
657    pub fn create_compound_policy(
658        &mut self,
659        msg_sender: Address,
660        call: ITIP403Registry::createCompoundPolicyCall,
661    ) -> Result<u64> {
662        // Validate all referenced policies exist and are simple (not compound)
663        self.validate_simple_policy(call.senderPolicyId)?;
664        self.validate_simple_policy(call.recipientPolicyId)?;
665        self.validate_simple_policy(call.mintRecipientPolicyId)?;
666
667        let new_policy_id = self.policy_id_counter()?;
668
669        // Increment counter
670        self.policy_id_counter.write(
671            new_policy_id
672                .checked_add(1)
673                .ok_or(TempoPrecompileError::under_overflow())?,
674        )?;
675
676        // Store policy record with COMPOUND type and compound data
677        self.policy_records[new_policy_id].write(PolicyRecord {
678            base: PolicyData {
679                policy_type: PolicyType::COMPOUND as u8,
680                admin: Address::ZERO,
681            },
682            compound: CompoundPolicyData {
683                sender_policy_id: call.senderPolicyId,
684                recipient_policy_id: call.recipientPolicyId,
685                mint_recipient_policy_id: call.mintRecipientPolicyId,
686            },
687        })?;
688
689        // Emit event
690        self.emit_event(TIP403RegistryEvent::compound_policy_created(
691            new_policy_id,
692            msg_sender,
693            call.senderPolicyId,
694            call.recipientPolicyId,
695            call.mintRecipientPolicyId,
696        ))?;
697
698        Ok(new_policy_id)
699    }
700
701    /// Core role-based authorization check ([TIP-1015]). Resolves built-in policies (0 = reject,
702    /// 1 = allow) immediately, delegates compound policies to their sub-policies, and evaluates
703    /// simple policies. (T6+) introduces protocol addresses that can't be policed.
704    ///
705    ///
706    /// [TIP-1015]: <https://docs.tempo.xyz/protocol/tips/tip-1015>
707    ///
708    /// # Errors
709    /// - `PolicyNotFound` — the policy ID does not exist (T2+)
710    /// - `InvalidPolicyType` — stored type cannot be decoded
711    /// - `IncompatiblePolicyType` — a compound policy was passed where a simple one is required
712    pub fn is_authorized_as(&self, policy_id: u64, user: Address, role: AuthRole) -> Result<bool> {
713        let hardfork = self.storage.spec();
714
715        // (spec: +T6) some protocol addresses can't be policed and are always authorized.
716        if ALWAYS_AUTHORIZED
717            .iter()
718            .any(|(fork, addrs)| hardfork >= *fork && addrs.contains(&user))
719        {
720            return Ok(true);
721        }
722
723        if let Some(auth) = self.builtin_authorization(policy_id) {
724            return Ok(auth);
725        }
726
727        let data = self.get_policy_data(policy_id)?;
728
729        if data.is_compound() {
730            let compound = self.policy_records[policy_id].compound.read()?;
731            return match role {
732                AuthRole::Sender => {
733                    self.is_authorized_simple(compound.sender_policy_id, user, None)
734                }
735                AuthRole::Recipient => {
736                    self.is_authorized_simple(compound.recipient_policy_id, user, None)
737                }
738                AuthRole::MintRecipient => {
739                    self.is_authorized_simple(compound.mint_recipient_policy_id, user, None)
740                }
741                AuthRole::Transfer => {
742                    // (spec: +T2) short-circuit and skip recipient check if sender fails
743                    let sender_auth =
744                        self.is_authorized_simple(compound.sender_policy_id, user, None)?;
745                    if hardfork.is_t2() && !sender_auth {
746                        return Ok(false);
747                    }
748                    let recipient_auth =
749                        self.is_authorized_simple(compound.recipient_policy_id, user, None)?;
750                    Ok(sender_auth && recipient_auth)
751                }
752            };
753        }
754
755        self.is_authorized_simple_inner(policy_id, user, &data)
756    }
757
758    /// Returns authorization result for built-in policies ([`REJECT_ALL_POLICY_ID`] / [`ALLOW_ALL_POLICY_ID`]).
759    /// Returns None for user-created policies.
760    #[inline]
761    fn builtin_authorization(&self, policy_id: u64) -> Option<bool> {
762        match policy_id {
763            ALLOW_ALL_POLICY_ID => Some(true),
764            REJECT_ALL_POLICY_ID => Some(false),
765            _ => None,
766        }
767    }
768
769    /// Authorization for simple (non-compound) policies only.
770    ///
771    /// **WARNING:** skips compound check - caller must guarantee policy is simple.
772    fn is_authorized_simple(
773        &self,
774        policy_id: u64,
775        user: Address,
776        cache: Option<PolicyData>,
777    ) -> Result<bool> {
778        if let Some(auth) = self.builtin_authorization(policy_id) {
779            return Ok(auth);
780        }
781        let data = match cache {
782            Some(data) => data,
783            None => self.get_policy_data(policy_id)?,
784        };
785        self.is_authorized_simple_inner(policy_id, user, &data)
786    }
787
788    /// Authorization check for simple (non-compound) policies.
789    fn is_authorized_simple_inner(
790        &self,
791        policy_id: u64,
792        user: Address,
793        data: &PolicyData,
794    ) -> Result<bool> {
795        // NOTE: read `policy_set` BEFORE checking policy type to match original gas consumption.
796        // Pre-T1: the old code read policy_set first, then failed on invalid policy types.
797        // This order must be preserved for block re-execution compatibility.
798        let is_in_set = self.policy_set[policy_id][user].read()?;
799
800        match data.policy_type()? {
801            PolicyType::WHITELIST => Ok(is_in_set),
802            PolicyType::BLACKLIST => Ok(!is_in_set),
803            PolicyType::COMPOUND => Err(TIP403RegistryError::incompatible_policy_type().into()),
804            PolicyType::__Invalid => unreachable!(),
805        }
806    }
807
808    /// Validates that a policy ID references an existing simple policy (not compound)
809    fn validate_simple_policy(&self, policy_id: u64) -> Result<()> {
810        // Built-in policies (0 and 1) are always valid simple policies
811        if self.builtin_authorization(policy_id).is_some() {
812            return Ok(());
813        }
814
815        // Check if policy exists
816        if policy_id >= self.policy_id_counter()? {
817            return Err(TIP403RegistryError::policy_not_found().into());
818        }
819
820        // Check if policy is simple (WHITELIST or BLACKLIST only)
821        let data = self.get_policy_data(policy_id)?;
822        if !data.is_simple() {
823            return Err(TIP403RegistryError::policy_not_simple().into());
824        }
825
826        Ok(())
827    }
828
829    /// Ensures `policy_id` is a built-in or an existing simple policy.
830    /// Returns the policy type so that the caller can use it.
831    fn validate_receive_policy_id(&self, policy_id: u64) -> Result<u8> {
832        if self.builtin_authorization(policy_id).is_some() {
833            return Ok(policy_id as u8); // safe downcast as it's either 0 or 1.
834        }
835        if policy_id >= self.policy_id_counter()? {
836            return Err(TIP403RegistryError::policy_not_found().into());
837        }
838        let data = self.get_policy_data(policy_id)?;
839        if !data.is_simple() {
840            return Err(TIP403RegistryError::invalid_receive_policy_type().into());
841        }
842        Ok(data.policy_type)
843    }
844
845    // Internal helper functions
846
847    /// Returns policy data for the given policy ID.
848    /// Errors with `PolicyNotFound` for invalid policy ids.
849    fn get_policy_data(&self, policy_id: u64) -> Result<PolicyData> {
850        let data = self.policy_records[policy_id].base.read()?;
851
852        // Verify that the policy id exists (spec: +T2).
853        // Skip the counter read (extra SLOAD) when policy data is non-default.
854        if self.storage.spec().is_t2()
855            && data.is_default()
856            && policy_id >= self.policy_id_counter()?
857        {
858            return Err(TIP403RegistryError::policy_not_found().into());
859        }
860
861        Ok(data)
862    }
863
864    /// Sets the policy data for `policy_id`.
865    ///
866    /// IMPORTANT: callers must not change `policy_type` for an existing policy. TIP-1028 receive
867    /// policies cache `policy_type` and rely on it being immutable after creation.
868    fn set_policy_data(&mut self, policy_id: u64, data: PolicyData) -> Result<()> {
869        self.policy_records[policy_id].base.write(data)
870    }
871
872    fn set_policy_set(&mut self, policy_id: u64, account: Address, value: bool) -> Result<()> {
873        self.policy_set[policy_id][account].write(value)
874    }
875}
876
877impl AuthRole {
878    #[inline]
879    fn transfer_or(t2_variant: Self) -> Self {
880        if StorageCtx.spec().is_t2() {
881            t2_variant
882        } else {
883            Self::Transfer
884        }
885    }
886
887    /// Hardfork-aware: always returns `Transfer`.
888    pub fn transfer() -> Self {
889        Self::Transfer
890    }
891
892    /// Hardfork-aware: returns `Sender` for T2+, `Transfer` for pre-T2.
893    pub fn sender() -> Self {
894        Self::transfer_or(Self::Sender)
895    }
896
897    /// Hardfork-aware: returns `Recipient` for T2+, `Transfer` for pre-T2.
898    pub fn recipient() -> Self {
899        Self::transfer_or(Self::Recipient)
900    }
901
902    /// Hardfork-aware: returns `MintRecipient` for T2+, `Transfer` for pre-T2.
903    pub fn mint_recipient() -> Self {
904        Self::transfer_or(Self::MintRecipient)
905    }
906}
907
908/// Returns `true` if the error indicates a failed policy lookup — the policy type is invalid
909/// or the policy doesn't exist.
910pub fn is_policy_lookup_error(e: &TempoPrecompileError) -> bool {
911    if StorageCtx.spec().is_t2() {
912        // T2+: typed TIP403 errors
913        *e == TIP403RegistryError::invalid_policy_type().into()
914            || *e == TIP403RegistryError::policy_not_found().into()
915    } else {
916        // Pre-T2: legacy Panic(UnderOverflow) sentinel
917        *e == TempoPrecompileError::under_overflow()
918    }
919}
920
921/// Extension trait for [`PolicyType`] validation.
922trait PolicyTypeExt {
923    /// Validates that this is a simple policy type and returns its `u8` discriminant.
924    fn ensure_is_simple(&self) -> Result<u8>;
925}
926
927impl PolicyTypeExt for PolicyType {
928    /// Validates and returns the policy type to store, handling backward compatibility.
929    ///
930    /// Pre-T2: Converts `COMPOUND` and `__Invalid` to 255 to match original ABI decoding behavior.
931    /// T2+: Only allows `WHITELIST` and `BLACKLIST`.
932    fn ensure_is_simple(&self) -> Result<u8> {
933        match self {
934            Self::WHITELIST | Self::BLACKLIST => Ok(*self as u8),
935            Self::COMPOUND | Self::__Invalid => {
936                if StorageCtx.spec().is_t2() {
937                    Err(TIP403RegistryError::incompatible_policy_type().into())
938                } else {
939                    Ok(Self::__Invalid as u8)
940                }
941            }
942        }
943    }
944}
945
946#[cfg(test)]
947mod tests {
948    use super::*;
949    use crate::{
950        SYSTEM_PRECOMPILES,
951        error::TempoPrecompileError,
952        storage::{ContractStorage, StorageCtx, hashmap::HashMapStorageProvider},
953    };
954    use alloy::{
955        primitives::{Address, Log},
956        sol_types::SolEvent,
957    };
958    use rand_08::Rng;
959    use tempo_contracts::precompiles::{PATH_USD_ADDRESS, TIP403_REGISTRY_ADDRESS};
960    use tempo_primitives::{MasterId, TempoAddressExt, UserTag};
961
962    #[test]
963    fn test_create_policy() -> eyre::Result<()> {
964        let mut storage = HashMapStorageProvider::new(1);
965        let admin = Address::random();
966        StorageCtx::enter(&mut storage, || {
967            let mut registry = TIP403Registry::new();
968
969            // Initial counter should be 2 (skipping special policies)
970            assert_eq!(registry.policy_id_counter()?, 2);
971
972            // Create a whitelist policy
973            let result = registry.create_policy(
974                admin,
975                ITIP403Registry::createPolicyCall {
976                    admin,
977                    policyType: ITIP403Registry::PolicyType::WHITELIST,
978                },
979            );
980            assert!(result.is_ok());
981            assert_eq!(result?, 2);
982
983            // Counter should be incremented
984            assert_eq!(registry.policy_id_counter()?, 3);
985
986            // Check policy data
987            let data = registry.policy_data(ITIP403Registry::policyDataCall { policyId: 2 })?;
988            assert_eq!(data.policyType, ITIP403Registry::PolicyType::WHITELIST);
989            assert_eq!(data.admin, admin);
990            Ok(())
991        })
992    }
993
994    #[test]
995    fn test_is_authorized_special_policies() -> eyre::Result<()> {
996        let mut storage = HashMapStorageProvider::new(1);
997        let user = Address::random();
998        StorageCtx::enter(&mut storage, || {
999            let registry = TIP403Registry::new();
1000
1001            // Policy 0 should always reject
1002            assert!(!registry.is_authorized_as(0, user, AuthRole::Transfer)?);
1003
1004            // Policy 1 should always allow
1005            assert!(registry.is_authorized_as(1, user, AuthRole::Transfer)?);
1006            Ok(())
1007        })
1008    }
1009
1010    #[test]
1011    fn test_always_authorized_t6_bypasses_policy_variants() -> eyre::Result<()> {
1012        const ROLES: &[AuthRole] = &[
1013            AuthRole::Transfer,
1014            AuthRole::Sender,
1015            AuthRole::Recipient,
1016            AuthRole::MintRecipient,
1017        ];
1018        let admin = Address::random();
1019
1020        // pre-T6 the address is NOT protected
1021        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
1022        StorageCtx::enter(&mut storage, || {
1023            let registry = TIP403Registry::new();
1024            assert!(!registry.is_authorized_as(
1025                REJECT_ALL_POLICY_ID,
1026                RECEIVE_POLICY_GUARD_ADDRESS,
1027                AuthRole::Transfer,
1028            )?);
1029            Ok::<(), TempoPrecompileError>(())
1030        })?;
1031
1032        // T6+ the address is protected
1033        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
1034        StorageCtx::enter(&mut storage, || {
1035            let mut registry = TIP403Registry::new();
1036            let whitelist_id = registry.create_policy(
1037                admin,
1038                ITIP403Registry::createPolicyCall {
1039                    admin,
1040                    policyType: ITIP403Registry::PolicyType::WHITELIST,
1041                },
1042            )?;
1043            let blacklist_id = registry.create_policy(
1044                admin,
1045                ITIP403Registry::createPolicyCall {
1046                    admin,
1047                    policyType: ITIP403Registry::PolicyType::BLACKLIST,
1048                },
1049            )?;
1050            let compound_id = registry.create_compound_policy(
1051                admin,
1052                ITIP403Registry::createCompoundPolicyCall {
1053                    senderPolicyId: REJECT_ALL_POLICY_ID,
1054                    recipientPolicyId: REJECT_ALL_POLICY_ID,
1055                    mintRecipientPolicyId: REJECT_ALL_POLICY_ID,
1056                },
1057            )?;
1058
1059            registry.modify_policy_blacklist(
1060                admin,
1061                ITIP403Registry::modifyPolicyBlacklistCall {
1062                    policyId: blacklist_id,
1063                    account: RECEIVE_POLICY_GUARD_ADDRESS,
1064                    restricted: true,
1065                },
1066            )?;
1067            for (policy_id, roles) in [
1068                (REJECT_ALL_POLICY_ID, ROLES),
1069                (whitelist_id, &[AuthRole::Transfer][..]),
1070                (blacklist_id, &[AuthRole::Transfer][..]),
1071                (compound_id, ROLES),
1072            ] {
1073                for role in roles {
1074                    assert!(registry.is_authorized_as(
1075                        policy_id,
1076                        RECEIVE_POLICY_GUARD_ADDRESS,
1077                        *role,
1078                    )?);
1079                }
1080            }
1081
1082            Ok(())
1083        })
1084    }
1085
1086    #[test]
1087    fn test_whitelist_policy() -> eyre::Result<()> {
1088        let mut storage = HashMapStorageProvider::new(1);
1089        let admin = Address::random();
1090        let user = Address::random();
1091        StorageCtx::enter(&mut storage, || {
1092            let mut registry = TIP403Registry::new();
1093
1094            // Create whitelist policy
1095            let policy_id = registry.create_policy(
1096                admin,
1097                ITIP403Registry::createPolicyCall {
1098                    admin,
1099                    policyType: ITIP403Registry::PolicyType::WHITELIST,
1100                },
1101            )?;
1102
1103            // User should not be authorized initially
1104            assert!(!registry.is_authorized_as(policy_id, user, AuthRole::Transfer)?);
1105
1106            // Add user to whitelist
1107            registry.modify_policy_whitelist(
1108                admin,
1109                ITIP403Registry::modifyPolicyWhitelistCall {
1110                    policyId: policy_id,
1111                    account: user,
1112                    allowed: true,
1113                },
1114            )?;
1115
1116            // User should now be authorized
1117            assert!(registry.is_authorized_as(policy_id, user, AuthRole::Transfer)?);
1118
1119            Ok(())
1120        })
1121    }
1122
1123    #[test]
1124    fn test_blacklist_policy() -> eyre::Result<()> {
1125        let mut storage = HashMapStorageProvider::new(1);
1126        let admin = Address::random();
1127        let user = Address::random();
1128        StorageCtx::enter(&mut storage, || {
1129            let mut registry = TIP403Registry::new();
1130
1131            // Create blacklist policy
1132            let policy_id = registry.create_policy(
1133                admin,
1134                ITIP403Registry::createPolicyCall {
1135                    admin,
1136                    policyType: ITIP403Registry::PolicyType::BLACKLIST,
1137                },
1138            )?;
1139
1140            // User should be authorized initially (not in blacklist)
1141            assert!(registry.is_authorized_as(policy_id, user, AuthRole::Transfer)?);
1142
1143            // Add user to blacklist
1144            registry.modify_policy_blacklist(
1145                admin,
1146                ITIP403Registry::modifyPolicyBlacklistCall {
1147                    policyId: policy_id,
1148                    account: user,
1149                    restricted: true,
1150                },
1151            )?;
1152
1153            // User should no longer be authorized
1154            assert!(!registry.is_authorized_as(policy_id, user, AuthRole::Transfer)?);
1155
1156            Ok(())
1157        })
1158    }
1159
1160    #[test]
1161    fn test_policy_data_reverts_for_non_existent_policy() -> eyre::Result<()> {
1162        let mut storage = HashMapStorageProvider::new(1);
1163        StorageCtx::enter(&mut storage, || {
1164            let registry = TIP403Registry::new();
1165
1166            // Test that querying a non-existent policy ID reverts
1167            let result = registry.policy_data(ITIP403Registry::policyDataCall { policyId: 100 });
1168            assert!(result.is_err());
1169
1170            // Verify the error is PolicyNotFound
1171            assert!(matches!(
1172                result.unwrap_err(),
1173                TempoPrecompileError::TIP403RegistryError(TIP403RegistryError::PolicyNotFound(_))
1174            ));
1175
1176            Ok(())
1177        })
1178    }
1179
1180    #[test]
1181    fn test_policy_data_builtin_policies_boundary() -> eyre::Result<()> {
1182        for (hardfork, expect_allow_all_type) in [
1183            // Pre-T2: reads uninitialized storage → both builtins decode as WHITELIST
1184            (TempoHardfork::T1C, ITIP403Registry::PolicyType::WHITELIST),
1185            // T2: virtual builtins return correct types
1186            (TempoHardfork::T2, ITIP403Registry::PolicyType::BLACKLIST),
1187        ] {
1188            let mut storage = HashMapStorageProvider::new_with_spec(1, hardfork);
1189            StorageCtx::enter(&mut storage, || {
1190                let registry = TIP403Registry::new();
1191
1192                // reject-all → WHITELIST on every fork (coincides with default storage)
1193                let reject = registry.policy_data(ITIP403Registry::policyDataCall {
1194                    policyId: REJECT_ALL_POLICY_ID,
1195                })?;
1196                assert_eq!(reject.policyType, ITIP403Registry::PolicyType::WHITELIST);
1197                assert_eq!(reject.admin, Address::ZERO);
1198
1199                // allow-all → WHITELIST pre-T2 (wrong), BLACKLIST from T2 (correct)
1200                let allow = registry.policy_data(ITIP403Registry::policyDataCall {
1201                    policyId: ALLOW_ALL_POLICY_ID,
1202                })?;
1203                assert_eq!(allow.policyType, expect_allow_all_type);
1204                assert_eq!(allow.admin, Address::ZERO);
1205
1206                Ok::<_, TempoPrecompileError>(())
1207            })?;
1208        }
1209        Ok(())
1210    }
1211
1212    #[test]
1213    fn test_receive_policy_defaults_to_none() -> eyre::Result<()> {
1214        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
1215        let account = Address::random();
1216        StorageCtx::enter(&mut storage, || {
1217            let registry = TIP403Registry::new();
1218
1219            let policy = registry.receive_policy(account)?;
1220            assert!(!policy.hasReceivePolicy);
1221            assert_eq!(policy.senderPolicyId, REJECT_ALL_POLICY_ID);
1222            assert_eq!(
1223                policy.senderPolicyType,
1224                ITIP403Registry::PolicyType::WHITELIST
1225            );
1226            assert_eq!(policy.tokenFilterId, REJECT_ALL_POLICY_ID);
1227            assert_eq!(
1228                policy.tokenFilterType,
1229                ITIP403Registry::PolicyType::WHITELIST
1230            );
1231            assert_eq!(policy.recoveryAuthority, Address::ZERO);
1232
1233            assert_eq!(
1234                registry.validate_receive_policy(Address::random(), Address::random(), account)?,
1235                None
1236            );
1237            assert_eq!(
1238                registry.receive_policies[account].recovery_address.read()?,
1239                Address::ZERO
1240            );
1241
1242            Ok(())
1243        })
1244    }
1245
1246    #[test]
1247    fn test_set_receive_policy_stores_config() -> eyre::Result<()> {
1248        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
1249        let account = Address::random();
1250        let recovery = Address::random();
1251        StorageCtx::enter(&mut storage, || {
1252            let mut registry = TIP403Registry::new();
1253
1254            registry.set_receive_policy(
1255                account,
1256                ITIP403Registry::setReceivePolicyCall {
1257                    senderPolicyId: REJECT_ALL_POLICY_ID,
1258                    tokenFilterId: ALLOW_ALL_POLICY_ID,
1259                    recoveryAuthority: recovery,
1260                },
1261            )?;
1262
1263            let policy = registry.receive_policy(account)?;
1264            assert!(policy.hasReceivePolicy);
1265            assert_eq!(policy.senderPolicyId, REJECT_ALL_POLICY_ID);
1266            assert_eq!(
1267                policy.senderPolicyType,
1268                ITIP403Registry::PolicyType::WHITELIST
1269            );
1270            assert_eq!(policy.tokenFilterId, ALLOW_ALL_POLICY_ID);
1271            assert_eq!(
1272                policy.tokenFilterType,
1273                ITIP403Registry::PolicyType::BLACKLIST
1274            );
1275            assert_eq!(policy.recoveryAuthority, recovery);
1276            assert_eq!(
1277                registry.receive_policies[account].recovery_address.read()?,
1278                recovery
1279            );
1280
1281            registry.assert_emitted_events(vec![TIP403RegistryEvent::ReceivePolicyUpdated(
1282                ITIP403Registry::ReceivePolicyUpdated {
1283                    account,
1284                    senderPolicyId: REJECT_ALL_POLICY_ID,
1285                    tokenFilterId: ALLOW_ALL_POLICY_ID,
1286                    recoveryAuthority: recovery,
1287                },
1288            )]);
1289
1290            Ok(())
1291        })
1292    }
1293
1294    #[test]
1295    fn test_set_receive_policy_rejects_virtual_account() -> eyre::Result<()> {
1296        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
1297        StorageCtx::enter(&mut storage, || {
1298            let mut registry = TIP403Registry::new();
1299
1300            let virtual_result = registry.set_receive_policy(
1301                Address::new_virtual(MasterId::ZERO, UserTag::ZERO),
1302                ITIP403Registry::setReceivePolicyCall {
1303                    senderPolicyId: REJECT_ALL_POLICY_ID,
1304                    tokenFilterId: ALLOW_ALL_POLICY_ID,
1305                    recoveryAuthority: Address::ZERO,
1306                },
1307            );
1308            assert!(matches!(
1309                virtual_result,
1310                Err(TempoPrecompileError::TIP403RegistryError(
1311                    TIP403RegistryError::VirtualAddressNotAllowed(_)
1312                ))
1313            ));
1314
1315            Ok(())
1316        })
1317    }
1318
1319    #[test]
1320    fn test_set_receive_policy_rejects_invalid_recovery_address() -> eyre::Result<()> {
1321        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
1322        let account = Address::random();
1323        let virtual_addr = Address::new_virtual(MasterId::random(), UserTag::random());
1324
1325        StorageCtx::enter(&mut storage, || {
1326            let mut registry = TIP403Registry::new();
1327
1328            let rejected = SYSTEM_PRECOMPILES
1329                .iter()
1330                .filter_map(|&(address, hf)| (hf <= StorageCtx.spec()).then_some(address))
1331                .chain([PATH_USD_ADDRESS, virtual_addr]);
1332
1333            for recovery_address in rejected {
1334                let result = registry.set_receive_policy(
1335                    account,
1336                    ITIP403Registry::setReceivePolicyCall {
1337                        senderPolicyId: REJECT_ALL_POLICY_ID,
1338                        tokenFilterId: ALLOW_ALL_POLICY_ID,
1339                        recoveryAuthority: recovery_address,
1340                    },
1341                );
1342                assert_eq!(
1343                    result.unwrap_err(),
1344                    TIP403RegistryError::invalid_recovery_authority().into()
1345                );
1346            }
1347
1348            // Zero is the originator-recovery sentinel; the caller's own address selects receiver recovery.
1349            registry.set_receive_policy(
1350                account,
1351                ITIP403Registry::setReceivePolicyCall {
1352                    senderPolicyId: REJECT_ALL_POLICY_ID,
1353                    tokenFilterId: ALLOW_ALL_POLICY_ID,
1354                    recoveryAuthority: Address::ZERO,
1355                },
1356            )?;
1357            registry.set_receive_policy(
1358                account,
1359                ITIP403Registry::setReceivePolicyCall {
1360                    senderPolicyId: REJECT_ALL_POLICY_ID,
1361                    tokenFilterId: ALLOW_ALL_POLICY_ID,
1362                    recoveryAuthority: account,
1363                },
1364            )?;
1365            let policy = registry.receive_policy(account)?;
1366            assert_eq!(policy.recoveryAuthority, account);
1367            assert_eq!(
1368                registry.receive_policies[account].recovery_address.read()?,
1369                Address::ZERO
1370            );
1371
1372            Ok(())
1373        })
1374    }
1375
1376    #[test]
1377    fn test_set_receive_policy_rejects_invalid_policy() -> eyre::Result<()> {
1378        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
1379        let account = Address::random();
1380        let creator = Address::random();
1381        StorageCtx::enter(&mut storage, || {
1382            let mut registry = TIP403Registry::new();
1383
1384            let missing_result = registry.set_receive_policy(
1385                account,
1386                ITIP403Registry::setReceivePolicyCall {
1387                    senderPolicyId: 99,
1388                    tokenFilterId: ALLOW_ALL_POLICY_ID,
1389                    recoveryAuthority: Address::ZERO,
1390                },
1391            );
1392            assert!(matches!(
1393                missing_result,
1394                Err(TempoPrecompileError::TIP403RegistryError(
1395                    TIP403RegistryError::PolicyNotFound(_)
1396                ))
1397            ));
1398
1399            let compound_id = registry.create_compound_policy(
1400                creator,
1401                ITIP403Registry::createCompoundPolicyCall {
1402                    senderPolicyId: REJECT_ALL_POLICY_ID,
1403                    recipientPolicyId: ALLOW_ALL_POLICY_ID,
1404                    mintRecipientPolicyId: ALLOW_ALL_POLICY_ID,
1405                },
1406            )?;
1407            let compound_result = registry.set_receive_policy(
1408                account,
1409                ITIP403Registry::setReceivePolicyCall {
1410                    senderPolicyId: compound_id,
1411                    tokenFilterId: ALLOW_ALL_POLICY_ID,
1412                    recoveryAuthority: Address::ZERO,
1413                },
1414            );
1415            assert!(matches!(
1416                compound_result,
1417                Err(TempoPrecompileError::TIP403RegistryError(
1418                    TIP403RegistryError::InvalidReceivePolicyType(_)
1419                ))
1420            ));
1421
1422            Ok(())
1423        })
1424    }
1425
1426    #[test]
1427    fn test_validate_receive_policy_reports_token_filter_first() -> eyre::Result<()> {
1428        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
1429        let receiver = Address::random();
1430        StorageCtx::enter(&mut storage, || {
1431            let mut registry = TIP403Registry::new();
1432            registry.set_receive_policy(
1433                receiver,
1434                ITIP403Registry::setReceivePolicyCall {
1435                    senderPolicyId: REJECT_ALL_POLICY_ID,
1436                    tokenFilterId: REJECT_ALL_POLICY_ID,
1437                    recoveryAuthority: Address::ZERO,
1438                },
1439            )?;
1440
1441            assert_eq!(
1442                registry.validate_receive_policy(Address::random(), Address::random(), receiver)?,
1443                Some(ITIP403Registry::BlockedReason::TOKEN_FILTER)
1444            );
1445
1446            Ok(())
1447        })
1448    }
1449
1450    #[test]
1451    fn test_validate_receive_policy_reports_sender_policy() -> eyre::Result<()> {
1452        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
1453        let receiver = Address::random();
1454        StorageCtx::enter(&mut storage, || {
1455            let mut registry = TIP403Registry::new();
1456            registry.set_receive_policy(
1457                receiver,
1458                ITIP403Registry::setReceivePolicyCall {
1459                    senderPolicyId: REJECT_ALL_POLICY_ID,
1460                    tokenFilterId: ALLOW_ALL_POLICY_ID,
1461                    recoveryAuthority: Address::ZERO,
1462                },
1463            )?;
1464
1465            assert_eq!(
1466                registry.validate_receive_policy(Address::random(), Address::random(), receiver)?,
1467                Some(ITIP403Registry::BlockedReason::RECEIVE_POLICY)
1468            );
1469
1470            Ok(())
1471        })
1472    }
1473
1474    #[test]
1475    fn test_policy_exists() -> eyre::Result<()> {
1476        let mut storage = HashMapStorageProvider::new(1);
1477        let admin = Address::random();
1478        StorageCtx::enter(&mut storage, || {
1479            let mut registry = TIP403Registry::new();
1480
1481            // Special policies 0 and 1 always exist
1482            assert!(registry.policy_exists(ITIP403Registry::policyExistsCall { policyId: 0 })?);
1483            assert!(registry.policy_exists(ITIP403Registry::policyExistsCall { policyId: 1 })?);
1484
1485            // Test 100 random policy IDs > 1 should not exist initially
1486            let mut rng = rand_08::thread_rng();
1487            for _ in 0..100 {
1488                let random_policy_id = rng.gen_range(2..u64::MAX);
1489                assert!(!registry.policy_exists(ITIP403Registry::policyExistsCall {
1490                    policyId: random_policy_id
1491                })?);
1492            }
1493
1494            // Create 50 policies
1495            let mut created_policy_ids = Vec::new();
1496            for i in 0..50 {
1497                let policy_id = registry.create_policy(
1498                    admin,
1499                    ITIP403Registry::createPolicyCall {
1500                        admin,
1501                        policyType: if i % 2 == 0 {
1502                            ITIP403Registry::PolicyType::WHITELIST
1503                        } else {
1504                            ITIP403Registry::PolicyType::BLACKLIST
1505                        },
1506                    },
1507                )?;
1508                created_policy_ids.push(policy_id);
1509            }
1510
1511            // All created policies should exist
1512            for policy_id in &created_policy_ids {
1513                assert!(registry.policy_exists(ITIP403Registry::policyExistsCall {
1514                    policyId: *policy_id
1515                })?);
1516            }
1517
1518            Ok(())
1519        })
1520    }
1521
1522    // =========================================================================
1523    //                      TIP-1015: Compound Policy Tests
1524    // =========================================================================
1525
1526    #[test]
1527    fn test_create_compound_policy() -> eyre::Result<()> {
1528        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
1529        let admin = Address::random();
1530        let creator = Address::random();
1531        StorageCtx::enter(&mut storage, || {
1532            let mut registry = TIP403Registry::new();
1533
1534            // Create two simple policies to reference
1535            let sender_policy = registry.create_policy(
1536                admin,
1537                ITIP403Registry::createPolicyCall {
1538                    admin,
1539                    policyType: ITIP403Registry::PolicyType::WHITELIST,
1540                },
1541            )?;
1542            let recipient_policy = registry.create_policy(
1543                admin,
1544                ITIP403Registry::createPolicyCall {
1545                    admin,
1546                    policyType: ITIP403Registry::PolicyType::BLACKLIST,
1547                },
1548            )?;
1549            let mint_recipient_policy = registry.create_policy(
1550                admin,
1551                ITIP403Registry::createPolicyCall {
1552                    admin,
1553                    policyType: ITIP403Registry::PolicyType::WHITELIST,
1554                },
1555            )?;
1556
1557            // Create compound policy
1558            let compound_id = registry.create_compound_policy(
1559                creator,
1560                ITIP403Registry::createCompoundPolicyCall {
1561                    senderPolicyId: sender_policy,
1562                    recipientPolicyId: recipient_policy,
1563                    mintRecipientPolicyId: mint_recipient_policy,
1564                },
1565            )?;
1566
1567            // Verify compound policy exists
1568            assert!(registry.policy_exists(ITIP403Registry::policyExistsCall {
1569                policyId: compound_id
1570            })?);
1571
1572            // Verify policy type is COMPOUND
1573            let data = registry.policy_data(ITIP403Registry::policyDataCall {
1574                policyId: compound_id,
1575            })?;
1576            assert_eq!(data.policyType, ITIP403Registry::PolicyType::COMPOUND);
1577            assert_eq!(data.admin, Address::ZERO); // Compound policies have no admin
1578
1579            // Verify compound policy data
1580            let compound_data =
1581                registry.compound_policy_data(ITIP403Registry::compoundPolicyDataCall {
1582                    policyId: compound_id,
1583                })?;
1584            assert_eq!(compound_data.senderPolicyId, sender_policy);
1585            assert_eq!(compound_data.recipientPolicyId, recipient_policy);
1586            assert_eq!(compound_data.mintRecipientPolicyId, mint_recipient_policy);
1587
1588            Ok(())
1589        })
1590    }
1591
1592    #[test]
1593    fn test_compound_policy_rejects_non_existent_refs() -> eyre::Result<()> {
1594        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1);
1595        let creator = Address::random();
1596        StorageCtx::enter(&mut storage, || {
1597            let mut registry = TIP403Registry::new();
1598
1599            // Try to create compound policy with non-existent policy IDs
1600            let result = registry.create_compound_policy(
1601                creator,
1602                ITIP403Registry::createCompoundPolicyCall {
1603                    senderPolicyId: 999,
1604                    recipientPolicyId: 1,
1605                    mintRecipientPolicyId: 1,
1606                },
1607            );
1608            assert!(result.is_err());
1609
1610            Ok(())
1611        })
1612    }
1613
1614    #[test]
1615    fn test_compound_policy_rejects_compound_refs() -> eyre::Result<()> {
1616        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1);
1617        let admin = Address::random();
1618        let creator = Address::random();
1619        StorageCtx::enter(&mut storage, || {
1620            let mut registry = TIP403Registry::new();
1621
1622            // Create a simple policy
1623            let simple_policy = registry.create_policy(
1624                admin,
1625                ITIP403Registry::createPolicyCall {
1626                    admin,
1627                    policyType: ITIP403Registry::PolicyType::WHITELIST,
1628                },
1629            )?;
1630
1631            // Create a compound policy
1632            let compound_id = registry.create_compound_policy(
1633                creator,
1634                ITIP403Registry::createCompoundPolicyCall {
1635                    senderPolicyId: 1,
1636                    recipientPolicyId: simple_policy,
1637                    mintRecipientPolicyId: 1,
1638                },
1639            )?;
1640
1641            // Try to create another compound policy referencing the first compound
1642            let result = registry.create_compound_policy(
1643                creator,
1644                ITIP403Registry::createCompoundPolicyCall {
1645                    senderPolicyId: compound_id, // This should fail - can't reference compound
1646                    recipientPolicyId: 1,
1647                    mintRecipientPolicyId: 1,
1648                },
1649            );
1650            assert!(result.is_err());
1651
1652            Ok(())
1653        })
1654    }
1655
1656    #[test]
1657    fn test_compound_policy_sender_recipient_differentiation() -> eyre::Result<()> {
1658        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1);
1659        let admin = Address::random();
1660        let creator = Address::random();
1661        let alice = Address::random();
1662        let bob = Address::random();
1663        StorageCtx::enter(&mut storage, || {
1664            let mut registry = TIP403Registry::new();
1665
1666            // Create sender whitelist (only Alice can send)
1667            let sender_policy = registry.create_policy(
1668                admin,
1669                ITIP403Registry::createPolicyCall {
1670                    admin,
1671                    policyType: ITIP403Registry::PolicyType::WHITELIST,
1672                },
1673            )?;
1674            registry.modify_policy_whitelist(
1675                admin,
1676                ITIP403Registry::modifyPolicyWhitelistCall {
1677                    policyId: sender_policy,
1678                    account: alice,
1679                    allowed: true,
1680                },
1681            )?;
1682
1683            // Create recipient whitelist (only Bob can receive)
1684            let recipient_policy = registry.create_policy(
1685                admin,
1686                ITIP403Registry::createPolicyCall {
1687                    admin,
1688                    policyType: ITIP403Registry::PolicyType::WHITELIST,
1689                },
1690            )?;
1691            registry.modify_policy_whitelist(
1692                admin,
1693                ITIP403Registry::modifyPolicyWhitelistCall {
1694                    policyId: recipient_policy,
1695                    account: bob,
1696                    allowed: true,
1697                },
1698            )?;
1699
1700            // Create compound policy
1701            let compound_id = registry.create_compound_policy(
1702                creator,
1703                ITIP403Registry::createCompoundPolicyCall {
1704                    senderPolicyId: sender_policy,
1705                    recipientPolicyId: recipient_policy,
1706                    mintRecipientPolicyId: 1, // anyone can receive mints
1707                },
1708            )?;
1709
1710            // Alice can send (is in sender whitelist)
1711            assert!(registry.is_authorized_as(compound_id, alice, AuthRole::Sender)?);
1712
1713            // Bob cannot send (not in sender whitelist)
1714            assert!(!registry.is_authorized_as(compound_id, bob, AuthRole::Sender)?);
1715
1716            // Bob can receive (is in recipient whitelist)
1717            assert!(registry.is_authorized_as(compound_id, bob, AuthRole::Recipient)?);
1718
1719            // Alice cannot receive (not in recipient whitelist)
1720            assert!(!registry.is_authorized_as(compound_id, alice, AuthRole::Recipient)?);
1721
1722            // Anyone can receive mints (mintRecipientPolicyId = 1 = always-allow)
1723            assert!(registry.is_authorized_as(compound_id, alice, AuthRole::MintRecipient)?);
1724            assert!(registry.is_authorized_as(compound_id, bob, AuthRole::MintRecipient)?);
1725
1726            Ok(())
1727        })
1728    }
1729
1730    #[test]
1731    fn test_compound_policy_is_authorized_behavior() -> eyre::Result<()> {
1732        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1);
1733        let admin = Address::random();
1734        let creator = Address::random();
1735        let user = Address::random();
1736        StorageCtx::enter(&mut storage, || {
1737            let mut registry = TIP403Registry::new();
1738
1739            // Create sender whitelist with user
1740            let sender_policy = registry.create_policy(
1741                admin,
1742                ITIP403Registry::createPolicyCall {
1743                    admin,
1744                    policyType: ITIP403Registry::PolicyType::WHITELIST,
1745                },
1746            )?;
1747            registry.modify_policy_whitelist(
1748                admin,
1749                ITIP403Registry::modifyPolicyWhitelistCall {
1750                    policyId: sender_policy,
1751                    account: user,
1752                    allowed: true,
1753                },
1754            )?;
1755
1756            // Create recipient whitelist WITHOUT user
1757            let recipient_policy = registry.create_policy(
1758                admin,
1759                ITIP403Registry::createPolicyCall {
1760                    admin,
1761                    policyType: ITIP403Registry::PolicyType::WHITELIST,
1762                },
1763            )?;
1764
1765            // Create compound policy
1766            let compound_id = registry.create_compound_policy(
1767                creator,
1768                ITIP403Registry::createCompoundPolicyCall {
1769                    senderPolicyId: sender_policy,
1770                    recipientPolicyId: recipient_policy,
1771                    mintRecipientPolicyId: 1,
1772                },
1773            )?;
1774
1775            // isAuthorized should be sender && recipient
1776            // User is sender-authorized but NOT recipient-authorized
1777            assert!(registry.is_authorized_as(compound_id, user, AuthRole::Sender)?);
1778            assert!(!registry.is_authorized_as(compound_id, user, AuthRole::Recipient)?);
1779
1780            // isAuthorized = sender && recipient = true && false = false
1781            assert!(!registry.is_authorized_as(compound_id, user, AuthRole::Transfer)?);
1782
1783            // Now add user to recipient whitelist
1784            registry.modify_policy_whitelist(
1785                admin,
1786                ITIP403Registry::modifyPolicyWhitelistCall {
1787                    policyId: recipient_policy,
1788                    account: user,
1789                    allowed: true,
1790                },
1791            )?;
1792
1793            // Now isAuthorized = sender && recipient = true && true = true
1794            assert!(registry.is_authorized_as(compound_id, user, AuthRole::Transfer)?);
1795
1796            Ok(())
1797        })
1798    }
1799
1800    #[test]
1801    fn test_compound_policy_is_authorized_transfer() -> eyre::Result<()> {
1802        let admin = Address::random();
1803        let creator = Address::random();
1804        let user = Address::random();
1805
1806        for hardfork in [TempoHardfork::T0, TempoHardfork::T1] {
1807            let mut storage = HashMapStorageProvider::new_with_spec(1, hardfork);
1808
1809            StorageCtx::enter(&mut storage, || {
1810                let mut registry = TIP403Registry::new();
1811
1812                // Create sender and recipient whitelists
1813                let sender_policy = registry.create_policy(
1814                    admin,
1815                    ITIP403Registry::createPolicyCall {
1816                        admin,
1817                        policyType: ITIP403Registry::PolicyType::WHITELIST,
1818                    },
1819                )?;
1820                let recipient_policy = registry.create_policy(
1821                    admin,
1822                    ITIP403Registry::createPolicyCall {
1823                        admin,
1824                        policyType: ITIP403Registry::PolicyType::WHITELIST,
1825                    },
1826                )?;
1827
1828                // Create compound policy
1829                let compound_id = registry.create_compound_policy(
1830                    creator,
1831                    ITIP403Registry::createCompoundPolicyCall {
1832                        senderPolicyId: sender_policy,
1833                        recipientPolicyId: recipient_policy,
1834                        mintRecipientPolicyId: 1,
1835                    },
1836                )?;
1837
1838                // User not in sender whitelist, but in recipient whitelist
1839                registry.modify_policy_whitelist(
1840                    admin,
1841                    ITIP403Registry::modifyPolicyWhitelistCall {
1842                        policyId: recipient_policy,
1843                        account: user,
1844                        allowed: true,
1845                    },
1846                )?;
1847                assert!(!registry.is_authorized_as(compound_id, user, AuthRole::Transfer)?);
1848
1849                // User in sender whitelist, not in recipient whitelist
1850                registry.modify_policy_whitelist(
1851                    admin,
1852                    ITIP403Registry::modifyPolicyWhitelistCall {
1853                        policyId: sender_policy,
1854                        account: user,
1855                        allowed: true,
1856                    },
1857                )?;
1858                registry.modify_policy_whitelist(
1859                    admin,
1860                    ITIP403Registry::modifyPolicyWhitelistCall {
1861                        policyId: recipient_policy,
1862                        account: user,
1863                        allowed: false,
1864                    },
1865                )?;
1866                assert!(!registry.is_authorized_as(compound_id, user, AuthRole::Transfer)?);
1867
1868                // User in both whitelists
1869                registry.modify_policy_whitelist(
1870                    admin,
1871                    ITIP403Registry::modifyPolicyWhitelistCall {
1872                        policyId: recipient_policy,
1873                        account: user,
1874                        allowed: true,
1875                    },
1876                )?;
1877                assert!(registry.is_authorized_as(compound_id, user, AuthRole::Transfer)?);
1878
1879                Ok::<_, TempoPrecompileError>(())
1880            })?;
1881        }
1882
1883        Ok(())
1884    }
1885
1886    #[test]
1887    fn test_simple_policy_equivalence() -> eyre::Result<()> {
1888        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1);
1889        let admin = Address::random();
1890        let user = Address::random();
1891        StorageCtx::enter(&mut storage, || {
1892            let mut registry = TIP403Registry::new();
1893
1894            // Create a simple whitelist policy with user
1895            let policy_id = registry.create_policy(
1896                admin,
1897                ITIP403Registry::createPolicyCall {
1898                    admin,
1899                    policyType: ITIP403Registry::PolicyType::WHITELIST,
1900                },
1901            )?;
1902            registry.modify_policy_whitelist(
1903                admin,
1904                ITIP403Registry::modifyPolicyWhitelistCall {
1905                    policyId: policy_id,
1906                    account: user,
1907                    allowed: true,
1908                },
1909            )?;
1910
1911            // For simple policies, all four authorization functions should return the same result
1912            let is_authorized = registry.is_authorized_as(policy_id, user, AuthRole::Transfer)?;
1913            let is_sender = registry.is_authorized_as(policy_id, user, AuthRole::Sender)?;
1914            let is_recipient = registry.is_authorized_as(policy_id, user, AuthRole::Recipient)?;
1915            let is_mint_recipient =
1916                registry.is_authorized_as(policy_id, user, AuthRole::MintRecipient)?;
1917
1918            assert!(is_authorized);
1919            assert_eq!(is_authorized, is_sender);
1920            assert_eq!(is_sender, is_recipient);
1921            assert_eq!(is_recipient, is_mint_recipient);
1922
1923            Ok(())
1924        })
1925    }
1926
1927    #[test]
1928    fn test_compound_policy_with_builtin_policies() -> eyre::Result<()> {
1929        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1);
1930        let creator = Address::random();
1931        let user = Address::random();
1932        StorageCtx::enter(&mut storage, || {
1933            let mut registry = TIP403Registry::new();
1934
1935            // Create compound policy using built-in policies
1936            // senderPolicyId = 1 (always-allow)
1937            // recipientPolicyId = 0 (always-reject)
1938            // mintRecipientPolicyId = 1 (always-allow)
1939            let compound_id = registry.create_compound_policy(
1940                creator,
1941                ITIP403Registry::createCompoundPolicyCall {
1942                    senderPolicyId: 1,
1943                    recipientPolicyId: 0,
1944                    mintRecipientPolicyId: 1,
1945                },
1946            )?;
1947
1948            // Anyone can send (policy 1 = always-allow)
1949            assert!(registry.is_authorized_as(compound_id, user, AuthRole::Sender)?);
1950
1951            // No one can receive transfers (policy 0 = always-reject)
1952            assert!(!registry.is_authorized_as(compound_id, user, AuthRole::Recipient)?);
1953
1954            // Anyone can receive mints (policy 1 = always-allow)
1955            assert!(registry.is_authorized_as(compound_id, user, AuthRole::MintRecipient)?);
1956
1957            // isAuthorized = sender && recipient = true && false = false
1958            assert!(!registry.is_authorized_as(compound_id, user, AuthRole::Transfer)?);
1959
1960            Ok(())
1961        })
1962    }
1963
1964    #[test]
1965    fn test_vendor_credits_use_case() -> eyre::Result<()> {
1966        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1);
1967        let admin = Address::random();
1968        let creator = Address::random();
1969        let vendor = Address::random();
1970        let customer = Address::random();
1971        StorageCtx::enter(&mut storage, || {
1972            let mut registry = TIP403Registry::new();
1973
1974            // Create vendor whitelist (only vendor can receive transfers)
1975            let vendor_whitelist = registry.create_policy(
1976                admin,
1977                ITIP403Registry::createPolicyCall {
1978                    admin,
1979                    policyType: ITIP403Registry::PolicyType::WHITELIST,
1980                },
1981            )?;
1982            registry.modify_policy_whitelist(
1983                admin,
1984                ITIP403Registry::modifyPolicyWhitelistCall {
1985                    policyId: vendor_whitelist,
1986                    account: vendor,
1987                    allowed: true,
1988                },
1989            )?;
1990
1991            // Create compound policy for vendor credits:
1992            // - Anyone can send (senderPolicyId = 1)
1993            // - Only vendor can receive transfers (recipientPolicyId = vendor_whitelist)
1994            // - Anyone can receive mints (mintRecipientPolicyId = 1)
1995            let compound_id = registry.create_compound_policy(
1996                creator,
1997                ITIP403Registry::createCompoundPolicyCall {
1998                    senderPolicyId: 1,                   // anyone can send
1999                    recipientPolicyId: vendor_whitelist, // only vendor receives
2000                    mintRecipientPolicyId: 1,            // anyone can receive mints
2001                },
2002            )?;
2003
2004            // Minting: anyone can receive mints (customer gets credits)
2005            assert!(registry.is_authorized_as(compound_id, customer, AuthRole::MintRecipient)?);
2006
2007            // Transfer: customer can send
2008            assert!(registry.is_authorized_as(compound_id, customer, AuthRole::Sender)?);
2009
2010            // Transfer: only vendor can receive
2011            assert!(registry.is_authorized_as(compound_id, vendor, AuthRole::Recipient)?);
2012            // customer cannot receive transfers (no P2P)
2013            assert!(!registry.is_authorized_as(compound_id, customer, AuthRole::Recipient)?);
2014
2015            Ok(())
2016        })
2017    }
2018
2019    #[test]
2020    fn test_policy_data_rejects_compound_policy_on_pre_t1() -> eyre::Result<()> {
2021        let creator = Address::random();
2022
2023        // First, create a compound policy on T1
2024        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1);
2025        let compound_id = StorageCtx::enter(&mut storage, || {
2026            let mut registry = TIP403Registry::new();
2027            registry.create_compound_policy(
2028                creator,
2029                ITIP403Registry::createCompoundPolicyCall {
2030                    senderPolicyId: 1,
2031                    recipientPolicyId: 1,
2032                    mintRecipientPolicyId: 1,
2033                },
2034            )
2035        })?;
2036
2037        // Now downgrade to T0 and try to read the compound policy data
2038        let mut storage = storage.with_spec(TempoHardfork::T0);
2039        StorageCtx::enter(&mut storage, || {
2040            let registry = TIP403Registry::new();
2041
2042            let result = registry.policy_data(ITIP403Registry::policyDataCall {
2043                policyId: compound_id,
2044            });
2045            assert!(result.is_err());
2046            assert_eq!(result.unwrap_err(), TempoPrecompileError::under_overflow());
2047
2048            Ok(())
2049        })
2050    }
2051
2052    #[test]
2053    fn test_create_policy_rejects_non_simple_policy_types() -> eyre::Result<()> {
2054        let admin = Address::random();
2055
2056        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
2057        StorageCtx::enter(&mut storage, || {
2058            let mut registry = TIP403Registry::new();
2059
2060            for policy_type in [
2061                ITIP403Registry::PolicyType::COMPOUND,
2062                ITIP403Registry::PolicyType::__Invalid,
2063            ] {
2064                let result = registry.create_policy(
2065                    admin,
2066                    ITIP403Registry::createPolicyCall {
2067                        admin,
2068                        policyType: policy_type,
2069                    },
2070                );
2071                assert!(matches!(
2072                    result.unwrap_err(),
2073                    TempoPrecompileError::TIP403RegistryError(
2074                        TIP403RegistryError::IncompatiblePolicyType(_)
2075                    )
2076                ));
2077            }
2078
2079            Ok(())
2080        })
2081    }
2082
2083    #[test]
2084    fn test_create_policy_with_accounts_rejects_non_simple_policy_types() -> eyre::Result<()> {
2085        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1);
2086        let admin = Address::random();
2087        let account = Address::random();
2088        StorageCtx::enter(&mut storage, || {
2089            let mut registry = TIP403Registry::new();
2090
2091            for policy_type in [
2092                ITIP403Registry::PolicyType::COMPOUND,
2093                ITIP403Registry::PolicyType::__Invalid,
2094            ] {
2095                let result = registry.create_policy_with_accounts(
2096                    admin,
2097                    ITIP403Registry::createPolicyWithAccountsCall {
2098                        admin,
2099                        policyType: policy_type,
2100                        accounts: vec![account],
2101                    },
2102                );
2103                assert!(matches!(
2104                    result.unwrap_err(),
2105                    TempoPrecompileError::TIP403RegistryError(
2106                        TIP403RegistryError::IncompatiblePolicyType(_)
2107                    )
2108                ));
2109            }
2110
2111            Ok(())
2112        })
2113    }
2114
2115    // =========================================================================
2116    //                Pre-T1 Backward Compatibility Tests
2117    // =========================================================================
2118
2119    #[test]
2120    fn test_pre_t1_create_policy_with_invalid_type_stores_255() -> eyre::Result<()> {
2121        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T0);
2122        let admin = Address::random();
2123        StorageCtx::enter(&mut storage, || {
2124            let mut registry = TIP403Registry::new();
2125
2126            // Pre-T1: COMPOUND and __Invalid should succeed but store as 255
2127            for policy_type in [
2128                ITIP403Registry::PolicyType::COMPOUND,
2129                ITIP403Registry::PolicyType::__Invalid,
2130            ] {
2131                let policy_id = registry.create_policy(
2132                    admin,
2133                    ITIP403Registry::createPolicyCall {
2134                        admin,
2135                        policyType: policy_type,
2136                    },
2137                )?;
2138
2139                // Verify policy was created
2140                assert!(registry.policy_exists(ITIP403Registry::policyExistsCall {
2141                    policyId: policy_id
2142                })?);
2143
2144                // Verify the stored policy_type is 255 (__Invalid)
2145                let data = registry.get_policy_data(policy_id)?;
2146                assert_eq!(data.policy_type, 255u8);
2147            }
2148
2149            Ok(())
2150        })
2151    }
2152
2153    #[test]
2154    fn test_pre_t1_create_policy_with_valid_types_stores_correct_value() -> eyre::Result<()> {
2155        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T0);
2156        let admin = Address::random();
2157        StorageCtx::enter(&mut storage, || {
2158            let mut registry = TIP403Registry::new();
2159
2160            // WHITELIST should store as 0
2161            let whitelist_id = registry.create_policy(
2162                admin,
2163                ITIP403Registry::createPolicyCall {
2164                    admin,
2165                    policyType: ITIP403Registry::PolicyType::WHITELIST,
2166                },
2167            )?;
2168            let data = registry.get_policy_data(whitelist_id)?;
2169            assert_eq!(data.policy_type, 0u8);
2170
2171            // BLACKLIST should store as 1
2172            let blacklist_id = registry.create_policy(
2173                admin,
2174                ITIP403Registry::createPolicyCall {
2175                    admin,
2176                    policyType: ITIP403Registry::PolicyType::BLACKLIST,
2177                },
2178            )?;
2179            let data = registry.get_policy_data(blacklist_id)?;
2180            assert_eq!(data.policy_type, 1u8);
2181
2182            Ok(())
2183        })
2184    }
2185
2186    #[test]
2187    fn test_pre_t1_create_policy_with_accounts_invalid_type_behavior() -> eyre::Result<()> {
2188        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T0);
2189        let (admin, account) = (Address::random(), Address::random());
2190
2191        StorageCtx::enter(&mut storage, || {
2192            let mut registry = TIP403Registry::new();
2193
2194            // With non-empty accounts: reverts with IncompatiblePolicyType
2195            for policy_type in [
2196                ITIP403Registry::PolicyType::COMPOUND,
2197                ITIP403Registry::PolicyType::__Invalid,
2198            ] {
2199                let result = registry.create_policy_with_accounts(
2200                    admin,
2201                    ITIP403Registry::createPolicyWithAccountsCall {
2202                        admin,
2203                        policyType: policy_type,
2204                        accounts: vec![account],
2205                    },
2206                );
2207                assert!(matches!(
2208                    result.unwrap_err(),
2209                    TempoPrecompileError::TIP403RegistryError(
2210                        TIP403RegistryError::IncompatiblePolicyType(_)
2211                    )
2212                ));
2213            }
2214
2215            // With empty accounts: succeeds (loop never enters revert path)
2216            let policy_id = registry.create_policy_with_accounts(
2217                admin,
2218                ITIP403Registry::createPolicyWithAccountsCall {
2219                    admin,
2220                    policyType: ITIP403Registry::PolicyType::__Invalid,
2221                    accounts: vec![],
2222                },
2223            )?;
2224            let data = registry.get_policy_data(policy_id)?;
2225            assert_eq!(data.policy_type, 255u8);
2226
2227            Ok(())
2228        })
2229    }
2230
2231    #[test]
2232    fn test_pre_t1_policy_data_reverts_for_any_policy_type_gte_2() -> eyre::Result<()> {
2233        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T0);
2234        let admin = Address::random();
2235        StorageCtx::enter(&mut storage, || {
2236            let mut registry = TIP403Registry::new();
2237
2238            // Create a policy with COMPOUND type (will be stored as 255)
2239            let policy_id = registry.create_policy(
2240                admin,
2241                ITIP403Registry::createPolicyCall {
2242                    admin,
2243                    policyType: ITIP403Registry::PolicyType::COMPOUND,
2244                },
2245            )?;
2246
2247            // policy_data should revert for policy_type >= 2 on pre-T1
2248            let result = registry.policy_data(ITIP403Registry::policyDataCall {
2249                policyId: policy_id,
2250            });
2251            assert!(result.is_err());
2252            assert_eq!(result.unwrap_err(), TempoPrecompileError::under_overflow());
2253
2254            Ok(())
2255        })
2256    }
2257
2258    #[test]
2259    fn test_pre_t1_is_authorized_reverts_for_invalid_policy_type() -> eyre::Result<()> {
2260        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T0);
2261        let admin = Address::random();
2262        let user = Address::random();
2263        StorageCtx::enter(&mut storage, || {
2264            let mut registry = TIP403Registry::new();
2265
2266            // Create a policy with COMPOUND type (stored as 255)
2267            let policy_id = registry.create_policy(
2268                admin,
2269                ITIP403Registry::createPolicyCall {
2270                    admin,
2271                    policyType: ITIP403Registry::PolicyType::COMPOUND,
2272                },
2273            )?;
2274
2275            // is_authorized should revert for policy_type >= 2 on pre-T1
2276            let result = registry.is_authorized_as(policy_id, user, AuthRole::Transfer);
2277            assert!(result.is_err());
2278            assert_eq!(result.unwrap_err(), TempoPrecompileError::under_overflow());
2279
2280            Ok(())
2281        })
2282    }
2283
2284    #[test]
2285    fn test_pre_t2_to_t2_migration_invalid_policy_still_fails() -> eyre::Result<()> {
2286        // Create a policy with invalid type on pre-T2
2287        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T0);
2288        let admin = Address::random();
2289        let user = Address::random();
2290
2291        let policy_id = StorageCtx::enter(&mut storage, || {
2292            let mut registry = TIP403Registry::new();
2293            registry.create_policy(
2294                admin,
2295                ITIP403Registry::createPolicyCall {
2296                    admin,
2297                    policyType: ITIP403Registry::PolicyType::COMPOUND,
2298                },
2299            )
2300        })?;
2301
2302        // Upgrade to T2 and try to use the policy
2303        let mut storage = storage.with_spec(TempoHardfork::T2);
2304        StorageCtx::enter(&mut storage, || {
2305            let registry = TIP403Registry::new();
2306
2307            // policy_data should fail with InvalidPolicyType on T2
2308            let result = registry.policy_data(ITIP403Registry::policyDataCall {
2309                policyId: policy_id,
2310            });
2311            assert!(result.is_err());
2312            assert_eq!(
2313                result.unwrap_err(),
2314                TIP403RegistryError::invalid_policy_type().into()
2315            );
2316
2317            // is_authorized should also fail with InvalidPolicyType on T2
2318            let result = registry.is_authorized_as(policy_id, user, AuthRole::Transfer);
2319            assert!(result.is_err());
2320            assert_eq!(
2321                result.unwrap_err(),
2322                TIP403RegistryError::invalid_policy_type().into()
2323            );
2324
2325            Ok(())
2326        })
2327    }
2328
2329    #[test]
2330    fn test_t2_compound_policy_rejects_legacy_invalid_255_policy() -> eyre::Result<()> {
2331        // Create a policy with invalid type on pre-T1 (stored as 255)
2332        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T0);
2333        let admin = Address::random();
2334        let creator = Address::random();
2335
2336        let invalid_policy_id = StorageCtx::enter(&mut storage, || {
2337            let mut registry = TIP403Registry::new();
2338            registry.create_policy(
2339                admin,
2340                ITIP403Registry::createPolicyCall {
2341                    admin,
2342                    policyType: ITIP403Registry::PolicyType::__Invalid,
2343                },
2344            )
2345        })?;
2346
2347        // Upgrade to T2 and create a valid simple policy
2348        let mut storage = storage.with_spec(TempoHardfork::T2);
2349        StorageCtx::enter(&mut storage, || {
2350            let mut registry = TIP403Registry::new();
2351
2352            let valid_policy_id = registry.create_policy(
2353                admin,
2354                ITIP403Registry::createPolicyCall {
2355                    admin,
2356                    policyType: ITIP403Registry::PolicyType::WHITELIST,
2357                },
2358            )?;
2359
2360            // Attempting to create a compound policy referencing the legacy 255 policy should fail
2361            let result = registry.create_compound_policy(
2362                creator,
2363                ITIP403Registry::createCompoundPolicyCall {
2364                    senderPolicyId: invalid_policy_id,
2365                    recipientPolicyId: valid_policy_id,
2366                    mintRecipientPolicyId: valid_policy_id,
2367                },
2368            );
2369            assert!(matches!(
2370                result.unwrap_err(),
2371                TempoPrecompileError::TIP403RegistryError(TIP403RegistryError::PolicyNotSimple(_))
2372            ));
2373
2374            Ok(())
2375        })
2376    }
2377
2378    #[test]
2379    fn test_t2_validate_policy_type_returns_correct_u8() -> eyre::Result<()> {
2380        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
2381        let admin = Address::random();
2382        StorageCtx::enter(&mut storage, || {
2383            let mut registry = TIP403Registry::new();
2384
2385            // WHITELIST should store as 0
2386            let whitelist_id = registry.create_policy(
2387                admin,
2388                ITIP403Registry::createPolicyCall {
2389                    admin,
2390                    policyType: ITIP403Registry::PolicyType::WHITELIST,
2391                },
2392            )?;
2393            let data = registry.get_policy_data(whitelist_id)?;
2394            assert_eq!(data.policy_type, 0u8);
2395
2396            // BLACKLIST should store as 1
2397            let blacklist_id = registry.create_policy(
2398                admin,
2399                ITIP403Registry::createPolicyCall {
2400                    admin,
2401                    policyType: ITIP403Registry::PolicyType::BLACKLIST,
2402                },
2403            )?;
2404            let data = registry.get_policy_data(blacklist_id)?;
2405            assert_eq!(data.policy_type, 1u8);
2406
2407            Ok(())
2408        })
2409    }
2410
2411    #[test]
2412    fn test_is_authorized_simple_inner_errors_on_invalid_policy_type_t2() -> eyre::Result<()> {
2413        // This test verifies that is_authorized_simple_inner explicitly errors for __Invalid
2414        // rather than returning false. We need to manually create a policy
2415        // with an invalid type to test this edge case.
2416        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T0);
2417        let admin = Address::random();
2418        let user = Address::random();
2419
2420        // Create policy with COMPOUND on pre-T2 (stores as 255)
2421        let policy_id = StorageCtx::enter(&mut storage, || {
2422            let mut registry = TIP403Registry::new();
2423            registry.create_policy(
2424                admin,
2425                ITIP403Registry::createPolicyCall {
2426                    admin,
2427                    policyType: ITIP403Registry::PolicyType::COMPOUND,
2428                },
2429            )
2430        })?;
2431
2432        // Now on T2, is_authorized should error with InvalidPolicyType
2433        let mut storage = storage.with_spec(TempoHardfork::T2);
2434        StorageCtx::enter(&mut storage, || {
2435            let registry = TIP403Registry::new();
2436
2437            let result = registry.is_authorized_as(policy_id, user, AuthRole::Transfer);
2438            assert_eq!(
2439                result.unwrap_err(),
2440                TIP403RegistryError::invalid_policy_type().into()
2441            );
2442
2443            Ok(())
2444        })
2445    }
2446
2447    #[test]
2448    fn test_pre_t1_whitelist_and_blacklist_work_normally() -> eyre::Result<()> {
2449        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T0);
2450        let admin = Address::random();
2451        let user = Address::random();
2452        StorageCtx::enter(&mut storage, || {
2453            let mut registry = TIP403Registry::new();
2454
2455            // Create and test whitelist on pre-T1
2456            let whitelist_id = registry.create_policy(
2457                admin,
2458                ITIP403Registry::createPolicyCall {
2459                    admin,
2460                    policyType: ITIP403Registry::PolicyType::WHITELIST,
2461                },
2462            )?;
2463
2464            // User not authorized initially
2465            assert!(!registry.is_authorized_as(whitelist_id, user, AuthRole::Transfer)?);
2466
2467            // Add to whitelist
2468            registry.modify_policy_whitelist(
2469                admin,
2470                ITIP403Registry::modifyPolicyWhitelistCall {
2471                    policyId: whitelist_id,
2472                    account: user,
2473                    allowed: true,
2474                },
2475            )?;
2476
2477            // Now authorized
2478            assert!(registry.is_authorized_as(whitelist_id, user, AuthRole::Transfer)?);
2479
2480            // Create and test blacklist on pre-T1
2481            let blacklist_id = registry.create_policy(
2482                admin,
2483                ITIP403Registry::createPolicyCall {
2484                    admin,
2485                    policyType: ITIP403Registry::PolicyType::BLACKLIST,
2486                },
2487            )?;
2488
2489            // User authorized initially (not in blacklist)
2490            assert!(registry.is_authorized_as(blacklist_id, user, AuthRole::Transfer)?);
2491
2492            // Add to blacklist
2493            registry.modify_policy_blacklist(
2494                admin,
2495                ITIP403Registry::modifyPolicyBlacklistCall {
2496                    policyId: blacklist_id,
2497                    account: user,
2498                    restricted: true,
2499                },
2500            )?;
2501
2502            // Now not authorized
2503            assert!(!registry.is_authorized_as(blacklist_id, user, AuthRole::Transfer)?);
2504
2505            Ok(())
2506        })
2507    }
2508
2509    #[test]
2510    fn test_pre_t1_create_policy_event_emits_invalid() -> eyre::Result<()> {
2511        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T0);
2512        let admin = Address::random();
2513
2514        StorageCtx::enter(&mut storage, || {
2515            let mut registry = TIP403Registry::new();
2516
2517            let policy_id = registry.create_policy(
2518                admin,
2519                ITIP403Registry::createPolicyCall {
2520                    admin,
2521                    policyType: ITIP403Registry::PolicyType::COMPOUND,
2522                },
2523            )?;
2524
2525            let data = registry.get_policy_data(policy_id)?;
2526            assert_eq!(data.policy_type, 255u8);
2527
2528            Ok::<_, TempoPrecompileError>(())
2529        })?;
2530
2531        let events = storage.events.get(&TIP403_REGISTRY_ADDRESS).unwrap();
2532        let policy_created_log = Log::new_unchecked(
2533            TIP403_REGISTRY_ADDRESS,
2534            events[0].topics().to_vec(),
2535            events[0].data.clone(),
2536        );
2537        let decoded = ITIP403Registry::PolicyCreated::decode_log(&policy_created_log)?;
2538
2539        // should emit 255, not 2
2540        assert_eq!(decoded.policyType, ITIP403Registry::PolicyType::__Invalid);
2541
2542        Ok(())
2543    }
2544
2545    #[test]
2546    fn test_t2_create_policy_rejects_invalid_types() -> eyre::Result<()> {
2547        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
2548        let admin = Address::random();
2549
2550        StorageCtx::enter(&mut storage, || {
2551            let mut registry = TIP403Registry::new();
2552
2553            for policy_type in [
2554                ITIP403Registry::PolicyType::COMPOUND,
2555                ITIP403Registry::PolicyType::__Invalid,
2556            ] {
2557                let result = registry.create_policy(
2558                    admin,
2559                    ITIP403Registry::createPolicyCall {
2560                        admin,
2561                        policyType: policy_type,
2562                    },
2563                );
2564                assert!(matches!(
2565                    result.unwrap_err(),
2566                    TempoPrecompileError::TIP403RegistryError(
2567                        TIP403RegistryError::IncompatiblePolicyType(_)
2568                    )
2569                ));
2570            }
2571
2572            Ok(())
2573        })
2574    }
2575
2576    #[test]
2577    fn test_t2_create_policy_emits_correct_type() -> eyre::Result<()> {
2578        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
2579        let admin = Address::random();
2580
2581        StorageCtx::enter(&mut storage, || {
2582            let mut registry = TIP403Registry::new();
2583
2584            registry.create_policy(
2585                admin,
2586                ITIP403Registry::createPolicyCall {
2587                    admin,
2588                    policyType: ITIP403Registry::PolicyType::WHITELIST,
2589                },
2590            )?;
2591
2592            registry.create_policy(
2593                admin,
2594                ITIP403Registry::createPolicyCall {
2595                    admin,
2596                    policyType: ITIP403Registry::PolicyType::BLACKLIST,
2597                },
2598            )?;
2599
2600            Ok::<_, TempoPrecompileError>(())
2601        })?;
2602
2603        let events = storage.events.get(&TIP403_REGISTRY_ADDRESS).unwrap();
2604
2605        // events[0] = PolicyCreated, events[1] = PolicyAdminUpdated, events[2] = PolicyCreated
2606        let whitelist_log = Log::new_unchecked(
2607            TIP403_REGISTRY_ADDRESS,
2608            events[0].topics().to_vec(),
2609            events[0].data.clone(),
2610        );
2611        let whitelist_decoded = ITIP403Registry::PolicyCreated::decode_log(&whitelist_log)?;
2612        assert_eq!(
2613            whitelist_decoded.policyType,
2614            ITIP403Registry::PolicyType::WHITELIST
2615        );
2616
2617        let blacklist_log = Log::new_unchecked(
2618            TIP403_REGISTRY_ADDRESS,
2619            events[2].topics().to_vec(),
2620            events[2].data.clone(),
2621        );
2622        let blacklist_decoded = ITIP403Registry::PolicyCreated::decode_log(&blacklist_log)?;
2623        assert_eq!(
2624            blacklist_decoded.policyType,
2625            ITIP403Registry::PolicyType::BLACKLIST
2626        );
2627
2628        Ok(())
2629    }
2630
2631    #[test]
2632    fn test_compound_policy_data_error_cases() -> eyre::Result<()> {
2633        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
2634        let admin = Address::random();
2635        StorageCtx::enter(&mut storage, || {
2636            let mut registry = TIP403Registry::new();
2637
2638            // Non-existent policy should return PolicyNotFound
2639            let result = registry
2640                .compound_policy_data(ITIP403Registry::compoundPolicyDataCall { policyId: 999 });
2641            assert!(matches!(
2642                result.unwrap_err(),
2643                TempoPrecompileError::TIP403RegistryError(TIP403RegistryError::PolicyNotFound(_))
2644            ));
2645
2646            // Simple policy should return IncompatiblePolicyType
2647            let simple_policy_id = registry.create_policy(
2648                admin,
2649                ITIP403Registry::createPolicyCall {
2650                    admin,
2651                    policyType: ITIP403Registry::PolicyType::WHITELIST,
2652                },
2653            )?;
2654            let result = registry.compound_policy_data(ITIP403Registry::compoundPolicyDataCall {
2655                policyId: simple_policy_id,
2656            });
2657            assert!(matches!(
2658                result.unwrap_err(),
2659                TempoPrecompileError::TIP403RegistryError(
2660                    TIP403RegistryError::IncompatiblePolicyType(_)
2661                )
2662            ));
2663
2664            Ok(())
2665        })
2666    }
2667
2668    #[test]
2669    fn test_invalid_policy_type() -> eyre::Result<()> {
2670        // Create a policy with __Invalid type
2671        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T0);
2672        let admin = Address::random();
2673        let user = Address::random();
2674
2675        let policy_id = StorageCtx::enter(&mut storage, || {
2676            let mut registry = TIP403Registry::new();
2677            registry.create_policy(
2678                admin,
2679                ITIP403Registry::createPolicyCall {
2680                    admin,
2681                    policyType: ITIP403Registry::PolicyType::__Invalid,
2682                },
2683            )
2684        })?;
2685
2686        // Pre-T2: should return under_overflow error
2687        StorageCtx::enter(&mut storage, || {
2688            let registry = TIP403Registry::new();
2689
2690            let result = registry.policy_data(ITIP403Registry::policyDataCall {
2691                policyId: policy_id,
2692            });
2693            assert_eq!(result.unwrap_err(), TempoPrecompileError::under_overflow());
2694
2695            let result = registry.is_authorized_as(policy_id, user, AuthRole::Transfer);
2696            assert_eq!(result.unwrap_err(), TempoPrecompileError::under_overflow());
2697
2698            Ok::<_, TempoPrecompileError>(())
2699        })?;
2700
2701        // T2+: should return InvalidPolicyType error
2702        let mut storage = storage.with_spec(TempoHardfork::T2);
2703        StorageCtx::enter(&mut storage, || {
2704            let registry = TIP403Registry::new();
2705
2706            let result = registry.policy_data(ITIP403Registry::policyDataCall {
2707                policyId: policy_id,
2708            });
2709            assert_eq!(
2710                result.unwrap_err(),
2711                TIP403RegistryError::invalid_policy_type().into()
2712            );
2713
2714            let result = registry.is_authorized_as(policy_id, user, AuthRole::Transfer);
2715            assert_eq!(
2716                result.unwrap_err(),
2717                TIP403RegistryError::invalid_policy_type().into()
2718            );
2719
2720            Ok(())
2721        })
2722    }
2723
2724    #[test]
2725    fn test_initialize_sets_storage_state() -> eyre::Result<()> {
2726        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
2727        StorageCtx::enter(&mut storage, || {
2728            let mut registry = TIP403Registry::new();
2729
2730            // Before init, should not be initialized
2731            assert!(!registry.is_initialized()?);
2732
2733            // Initialize
2734            registry.initialize()?;
2735
2736            // After init, should be initialized
2737            assert!(registry.is_initialized()?);
2738
2739            // New handle should still see initialized state
2740            let registry2 = TIP403Registry::new();
2741            assert!(registry2.is_initialized()?);
2742
2743            Ok(())
2744        })
2745    }
2746
2747    #[test]
2748    fn test_policy_exists_boundary_at_counter() -> eyre::Result<()> {
2749        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
2750        let admin = Address::random();
2751        StorageCtx::enter(&mut storage, || {
2752            let mut registry = TIP403Registry::new();
2753
2754            // Create a policy to get policy_id = 2 (counter starts at 2)
2755            let policy_id = registry.create_policy(
2756                admin,
2757                ITIP403Registry::createPolicyCall {
2758                    admin,
2759                    policyType: ITIP403Registry::PolicyType::WHITELIST,
2760                },
2761            )?;
2762
2763            // The counter should now be 3
2764            let counter = registry.policy_id_counter()?;
2765            assert_eq!(counter, 3);
2766
2767            // Policy at counter - 1 should exist
2768            assert!(registry.policy_exists(ITIP403Registry::policyExistsCall {
2769                policyId: policy_id,
2770            })?);
2771
2772            // Policy at exactly counter should NOT exist (tests < vs <=)
2773            assert!(
2774                !registry.policy_exists(ITIP403Registry::policyExistsCall { policyId: counter })?
2775            );
2776
2777            // Policy at counter + 1 should NOT exist
2778            assert!(!registry.policy_exists(ITIP403Registry::policyExistsCall {
2779                policyId: counter + 1,
2780            })?);
2781
2782            Ok(())
2783        })
2784    }
2785
2786    #[test]
2787    fn test_nonexistent_policy_behavior() -> eyre::Result<()> {
2788        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1);
2789        let user = Address::random();
2790        let nonexistent_id = 999;
2791
2792        // Pre-T2: silently returns default data / false
2793        StorageCtx::enter(&mut storage, || -> Result<()> {
2794            let registry = TIP403Registry::new();
2795            let data = registry.get_policy_data(nonexistent_id)?;
2796            assert!(data.is_default());
2797            assert!(!registry.is_authorized_as(nonexistent_id, user, AuthRole::Transfer)?);
2798            Ok(())
2799        })?;
2800
2801        // T2: reverts with `PolicyNotFound`
2802        let mut storage = storage.with_spec(TempoHardfork::T2);
2803        StorageCtx::enter(&mut storage, || {
2804            let registry = TIP403Registry::new();
2805            assert_eq!(
2806                registry.get_policy_data(nonexistent_id).unwrap_err(),
2807                TIP403RegistryError::policy_not_found().into()
2808            );
2809            assert_eq!(
2810                registry
2811                    .is_authorized_as(nonexistent_id, user, AuthRole::Transfer)
2812                    .unwrap_err(),
2813                TIP403RegistryError::policy_not_found().into()
2814            );
2815            Ok(())
2816        })
2817    }
2818
2819    // ────────────────── TIP-1022 Virtual Address Rejection ──────────────────
2820
2821    #[test]
2822    fn test_modify_whitelist_rejects_virtual_address() -> eyre::Result<()> {
2823        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
2824        let admin = Address::random();
2825
2826        StorageCtx::enter(&mut storage, || {
2827            let mut registry = TIP403Registry::new();
2828            let policy_id = registry.create_policy(
2829                admin,
2830                ITIP403Registry::createPolicyCall {
2831                    admin,
2832                    policyType: PolicyType::WHITELIST,
2833                },
2834            )?;
2835
2836            let result = registry.modify_policy_whitelist(
2837                admin,
2838                ITIP403Registry::modifyPolicyWhitelistCall {
2839                    policyId: policy_id,
2840                    account: Address::new_virtual(MasterId::ZERO, UserTag::ZERO),
2841                    allowed: true,
2842                },
2843            );
2844            assert!(matches!(
2845                result.unwrap_err(),
2846                TempoPrecompileError::TIP403RegistryError(
2847                    TIP403RegistryError::VirtualAddressNotAllowed(_)
2848                )
2849            ));
2850
2851            Ok(())
2852        })
2853    }
2854
2855    #[test]
2856    fn test_modify_blacklist_rejects_virtual_address() -> eyre::Result<()> {
2857        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
2858        let admin = Address::random();
2859
2860        StorageCtx::enter(&mut storage, || {
2861            let mut registry = TIP403Registry::new();
2862            let policy_id = registry.create_policy(
2863                admin,
2864                ITIP403Registry::createPolicyCall {
2865                    admin,
2866                    policyType: PolicyType::BLACKLIST,
2867                },
2868            )?;
2869
2870            let result = registry.modify_policy_blacklist(
2871                admin,
2872                ITIP403Registry::modifyPolicyBlacklistCall {
2873                    policyId: policy_id,
2874                    account: Address::new_virtual(MasterId::ZERO, UserTag::ZERO),
2875                    restricted: true,
2876                },
2877            );
2878            assert!(matches!(
2879                result.unwrap_err(),
2880                TempoPrecompileError::TIP403RegistryError(
2881                    TIP403RegistryError::VirtualAddressNotAllowed(_)
2882                )
2883            ));
2884
2885            Ok(())
2886        })
2887    }
2888
2889    #[test]
2890    fn test_create_policy_with_accounts_rejects_virtual_address() -> eyre::Result<()> {
2891        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
2892        let admin = Address::random();
2893
2894        StorageCtx::enter(&mut storage, || {
2895            let mut registry = TIP403Registry::new();
2896
2897            let result = registry.create_policy_with_accounts(
2898                admin,
2899                ITIP403Registry::createPolicyWithAccountsCall {
2900                    admin,
2901                    policyType: PolicyType::WHITELIST,
2902                    accounts: vec![
2903                        Address::random(),
2904                        Address::new_virtual(MasterId::ZERO, UserTag::ZERO),
2905                    ],
2906                },
2907            );
2908            assert!(matches!(
2909                result.unwrap_err(),
2910                TempoPrecompileError::TIP403RegistryError(
2911                    TIP403RegistryError::VirtualAddressNotAllowed(_)
2912                )
2913            ));
2914
2915            // Verify counter was not incremented (no policy created)
2916            assert_eq!(registry.policy_id_counter()?, 2);
2917
2918            Ok(())
2919        })
2920    }
2921}