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