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