Skip to main content

tempo_precompiles/address_registry/
mod.rs

1//! [TIP-1022] virtual address registry precompile. Enabled on `TempoHardfork::T3`.
2//!
3//! Provides on-chain registration of virtual-address masters and resolution of
4//! [TIP-1022] virtual addresses back to their registered master EOA/contract.
5//!
6//! [TIP-1022]: <https://docs.tempo.xyz/protocol/tip1022>
7
8pub mod dispatch;
9
10use crate::{
11    ADDRESS_REGISTRY_ADDRESS,
12    error::Result,
13    storage::{Handler, Mapping},
14};
15use alloy::{
16    primitives::{Address, FixedBytes, keccak256},
17    sol_types::SolValue,
18};
19use tempo_chainspec::hardfork::TempoHardfork;
20pub use tempo_contracts::precompiles::{
21    AddrRegistryError, AddrRegistryEvent, IAddressRegistry, STABLECOIN_DEX_ADDRESS,
22    TIP_FEE_MANAGER_ADDRESS, TIP20_CHANNEL_RESERVE_ADDRESS,
23};
24use tempo_precompiles_macros::{Storable, contract};
25pub use tempo_primitives::{MasterId, TempoAddressExt, UserTag};
26
27/// TIP-1035 Implicit Approval List.
28///
29/// Precompiles on this list are authorized to call
30/// [`crate::tip20::TIP20Token::system_transfer_from`], pulling TIP-20 tokens from a user without a
31/// prior `approve()`. The list is gated on `TempoHardfork::T5`; before activation it is empty.
32pub const IMPLICIT_APPROVAL_LIST: &[Address] = &[
33    TIP_FEE_MANAGER_ADDRESS,
34    STABLECOIN_DEX_ADDRESS,
35    TIP20_CHANNEL_RESERVE_ADDRESS,
36];
37
38/// Returns `true` iff `addr` is on the [`IMPLICIT_APPROVAL_LIST`] for the given hardfork.
39///
40/// Before `TempoHardfork::T5` (TIP-1035 activation), returns `false` for all addresses.
41pub fn is_implicitly_approved(addr: Address, hardfork: TempoHardfork) -> bool {
42    if !hardfork.is_t5() {
43        return false;
44    }
45    IMPLICIT_APPROVAL_LIST.contains(&addr)
46}
47
48/// [TIP-1022] virtual address registry contract.
49///
50/// Maps a 4-byte [`MasterId`] to its registered master address and metadata.
51/// Registration requires a 32-bit proof-of-work to prevent squatting.
52///
53/// The struct fields define the on-chain storage layout; the `#[contract]` macro generates the
54/// storage handlers which provide an ergonomic way to interact with the EVM state.
55///
56/// [TIP-1022]: <https://docs.tempo.xyz/protocol/tip1022>
57#[contract(addr = ADDRESS_REGISTRY_ADDRESS)]
58pub struct AddressRegistry {
59    /// Maps `masterId → RegistryData` (master address + metadata).
60    data: Mapping<MasterId, RegistryData>,
61}
62
63/// Storage record for a registered master. Packed into a single 32-byte slot.
64#[derive(Debug, Clone, Default, Storable)]
65struct RegistryData {
66    /// The EOA or contract that owns this `masterId`.
67    master_address: Address,
68    /// Reserved bytes for future use.
69    reserved: FixedBytes<11>,
70    /// Master type discriminator (currently unused, always `0`).
71    ty: u8,
72}
73
74impl RegistryData {
75    /// Returns the master address, or `None` if the slot is empty (`address(0)`).
76    fn master_address(&self) -> Option<Address> {
77        match self.master_address {
78            Address::ZERO => None,
79            master => Some(master),
80        }
81    }
82}
83
84impl AddressRegistry {
85    /// Initializes the registry contract by setting its bytecode marker.
86    pub fn initialize(&mut self) -> Result<()> {
87        self.__initialize()
88    }
89
90    // ────────────────── Registration ──────────────────
91
92    /// Registers `msg_sender` as a virtual-address master.
93    ///
94    /// The registration hash is `keccak256(abi.encodePacked(msg.sender, salt))`.
95    /// The first 4 bytes MUST be zero (32-bit proof-of-work). `masterId` is bytes `[4:8]`.
96    ///
97    /// # Errors
98    /// - `InvalidMasterAddress` — `msg_sender` is zero, a virtual address, or a TIP-20 token
99    /// - `ProofOfWorkFailed` — the first 4 bytes of the registration hash are not zero
100    /// - `MasterIdCollision` — the derived `masterId` is already registered
101    pub fn register_virtual_master(
102        &mut self,
103        msg_sender: Address,
104        call: IAddressRegistry::registerVirtualMasterCall,
105    ) -> Result<MasterId> {
106        // Validate master address
107        if !msg_sender.is_valid_master() {
108            return Err(AddrRegistryError::invalid_master_address().into());
109        }
110
111        // Compute registration hash: keccak256(abi.encodePacked(msg.sender, salt))
112        let registration_hash = keccak256((msg_sender, call.salt).abi_encode_packed());
113
114        // 32-bit PoW: first 4 bytes must be zero
115        if registration_hash[0..4] != [0u8; 4] {
116            return Err(AddrRegistryError::proof_of_work_failed().into());
117        }
118
119        // masterId = bytes [4:8]
120        let master_id = MasterId::from_slice(&registration_hash[4..8]);
121
122        // Ensure no collisions
123        if let Some(master) = self.data[master_id].read()?.master_address() {
124            return Err(AddrRegistryError::master_id_collision(master).into());
125        }
126
127        // Store the registration
128        self.data[master_id].write(RegistryData {
129            master_address: msg_sender,
130            reserved: FixedBytes::ZERO,
131            ty: 0,
132        })?;
133
134        // Emit event
135        self.emit_event(AddrRegistryEvent::master_registered(master_id, msg_sender))?;
136
137        Ok(master_id)
138    }
139
140    // ────────────────── View Functions ──────────────────
141
142    /// Returns the registered master address for `master_id`, or `None` if unregistered.
143    pub fn get_master(&self, master_id: MasterId) -> Result<Option<Address>> {
144        Ok(self.data[master_id].read()?.master_address())
145    }
146
147    /// Resolves a transfer recipient using virtual address semantics.
148    ///
149    /// Non-virtual addresses are returned unchanged.
150    /// Virtual addresses are resolved to their registered master.
151    ///
152    /// # Errors
153    /// - `VirtualAddressUnregistered` — `to` is a virtual address whose `masterId` is not registered
154    pub fn resolve_recipient(&self, to: Address) -> Result<Address> {
155        // Explicit check because it isn't exclusively a view function.
156        // It is also used by `tip20::Recipient`.
157        if !self.storage.spec().is_t3() {
158            return Ok(to);
159        }
160
161        match to.decode_virtual() {
162            None => Ok(to),
163            Some((master_id, _)) => self
164                .get_master(master_id)?
165                .ok_or(AddrRegistryError::virtual_address_unregistered().into()),
166        }
167    }
168
169    /// Resolves a virtual address to its registered master.
170    ///
171    /// Returns `address(0)` if the address is not virtual or the [`MasterId`] is unregistered.
172    pub fn resolve_virtual_address(&self, addr: Address) -> Result<Address> {
173        match addr.decode_virtual() {
174            None => Ok(Address::ZERO),
175            Some((master_id, _)) => Ok(self.get_master(master_id)?.unwrap_or(Address::ZERO)),
176        }
177    }
178
179    /// Returns `true` iff `addr` is on the TIP-1035 [`IMPLICIT_APPROVAL_LIST`] for the active
180    /// hardfork. Returns `false` for all addresses before `TempoHardfork::T5`.
181    pub fn is_implicitly_approved(&self, addr: Address) -> bool {
182        is_implicitly_approved(addr, self.storage.spec())
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189    use crate::{
190        error::TempoPrecompileError,
191        storage::{StorageCtx, hashmap::HashMapStorageProvider},
192        test_util::{VIRTUAL_MASTER, VIRTUAL_SALT},
193    };
194    use alloy_primitives::hex_literal::hex;
195    use tempo_chainspec::hardfork::TempoHardfork;
196
197    #[test]
198    fn test_is_implicitly_approved_pre_t5_returns_false() -> eyre::Result<()> {
199        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T4);
200        StorageCtx::enter(&mut storage, || {
201            let registry = AddressRegistry::new();
202            assert!(!registry.is_implicitly_approved(TIP_FEE_MANAGER_ADDRESS));
203            assert!(!registry.is_implicitly_approved(STABLECOIN_DEX_ADDRESS));
204            assert!(!registry.is_implicitly_approved(TIP20_CHANNEL_RESERVE_ADDRESS));
205            assert!(!registry.is_implicitly_approved(Address::random()));
206            Ok(())
207        })
208    }
209
210    #[test]
211    fn test_is_implicitly_approved_t5_lists_initial_set() -> eyre::Result<()> {
212        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
213        StorageCtx::enter(&mut storage, || {
214            let registry = AddressRegistry::new();
215            assert!(registry.is_implicitly_approved(TIP_FEE_MANAGER_ADDRESS));
216            assert!(registry.is_implicitly_approved(STABLECOIN_DEX_ADDRESS));
217            assert!(registry.is_implicitly_approved(TIP20_CHANNEL_RESERVE_ADDRESS));
218            assert!(!registry.is_implicitly_approved(Address::random()));
219            Ok(())
220        })
221    }
222
223    #[test]
224    fn test_register_virtual_master() -> eyre::Result<()> {
225        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
226        let (master, salt) = (VIRTUAL_MASTER, VIRTUAL_SALT.into());
227
228        StorageCtx::enter(&mut storage, || {
229            let mut registry = AddressRegistry::new();
230
231            let master_id = registry.register_virtual_master(
232                master,
233                IAddressRegistry::registerVirtualMasterCall { salt },
234            )?;
235
236            assert_eq!(registry.get_master(master_id)?, Some(master));
237
238            Ok(())
239        })
240    }
241
242    #[test]
243    fn test_register_rejects_bad_pow() -> eyre::Result<()> {
244        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
245        let master = Address::random();
246        let bad_salt = FixedBytes::<32>::ZERO;
247
248        StorageCtx::enter(&mut storage, || {
249            let mut registry = AddressRegistry::new();
250
251            let result = registry.register_virtual_master(
252                master,
253                IAddressRegistry::registerVirtualMasterCall { salt: bad_salt },
254            );
255            assert!(matches!(
256                result.unwrap_err(),
257                TempoPrecompileError::AddrRegistryError(AddrRegistryError::ProofOfWorkFailed(_))
258            ));
259
260            Ok(())
261        })
262    }
263
264    #[test]
265    fn test_register_rejects_zero_address() -> eyre::Result<()> {
266        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
267
268        StorageCtx::enter(&mut storage, || {
269            let mut registry = AddressRegistry::new();
270
271            let result = registry.register_virtual_master(
272                Address::ZERO,
273                IAddressRegistry::registerVirtualMasterCall {
274                    salt: FixedBytes::ZERO,
275                },
276            );
277            assert!(matches!(
278                result.unwrap_err(),
279                TempoPrecompileError::AddrRegistryError(AddrRegistryError::InvalidMasterAddress(_))
280            ));
281
282            Ok(())
283        })
284    }
285
286    #[test]
287    fn test_register_rejects_virtual_address_as_master() -> eyre::Result<()> {
288        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
289
290        StorageCtx::enter(&mut storage, || {
291            let mut registry = AddressRegistry::new();
292
293            let result = registry.register_virtual_master(
294                Address::new_virtual(MasterId::ZERO, UserTag::ZERO),
295                IAddressRegistry::registerVirtualMasterCall {
296                    salt: FixedBytes::ZERO,
297                },
298            );
299            assert!(matches!(
300                result.unwrap_err(),
301                TempoPrecompileError::AddrRegistryError(AddrRegistryError::InvalidMasterAddress(_))
302            ));
303
304            Ok(())
305        })
306    }
307
308    #[test]
309    fn test_register_rejects_tip20_address_as_master() -> eyre::Result<()> {
310        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
311        let tip20_addr = crate::PATH_USD_ADDRESS;
312
313        StorageCtx::enter(&mut storage, || {
314            let mut registry = AddressRegistry::new();
315
316            let result = registry.register_virtual_master(
317                tip20_addr,
318                IAddressRegistry::registerVirtualMasterCall {
319                    salt: FixedBytes::ZERO,
320                },
321            );
322            assert!(matches!(
323                result.unwrap_err(),
324                TempoPrecompileError::AddrRegistryError(AddrRegistryError::InvalidMasterAddress(_))
325            ));
326
327            Ok(())
328        })
329    }
330
331    #[test]
332    fn test_register_duplicate_reverts_with_collision() -> eyre::Result<()> {
333        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
334        let (master, salt) = (VIRTUAL_MASTER, VIRTUAL_SALT.into());
335
336        StorageCtx::enter(&mut storage, || {
337            let mut registry = AddressRegistry::new();
338
339            // First registration succeeds
340            registry.register_virtual_master(
341                master,
342                IAddressRegistry::registerVirtualMasterCall { salt },
343            )?;
344
345            // Second registration with same (address, salt) reverts
346            let result = registry.register_virtual_master(
347                master,
348                IAddressRegistry::registerVirtualMasterCall { salt },
349            );
350            assert!(matches!(
351                result.unwrap_err(),
352                TempoPrecompileError::AddrRegistryError(AddrRegistryError::MasterIdCollision(_))
353            ));
354
355            Ok(())
356        })
357    }
358
359    #[test]
360    fn test_is_virtual_address() {
361        assert!(!Address::random().is_virtual());
362        assert!(Address::new_virtual(MasterId::random(), UserTag::random()).is_virtual());
363    }
364
365    #[test]
366    fn test_decode_virtual_address() {
367        let mid = MasterId::random();
368        let tag = UserTag::random();
369        let addr = Address::new_virtual(mid, tag);
370
371        let (master_id, user_tag) = addr.decode_virtual().unwrap();
372        assert_eq!(master_id, mid);
373        assert_eq!(user_tag, tag);
374
375        assert!(Address::random().decode_virtual().is_none());
376    }
377
378    #[test]
379    fn test_resolve_recipient_non_virtual() -> eyre::Result<()> {
380        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
381        let normal_addr = Address::random();
382
383        StorageCtx::enter(&mut storage, || {
384            let registry = AddressRegistry::new();
385
386            let resolved = registry.resolve_recipient(normal_addr)?;
387            assert_eq!(resolved, normal_addr);
388
389            Ok(())
390        })
391    }
392
393    #[test]
394    fn test_resolve_recipient_virtual_unregistered_reverts() -> eyre::Result<()> {
395        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
396        let virtual_addr = Address::new_virtual(MasterId::ZERO, UserTag::ZERO);
397
398        StorageCtx::enter(&mut storage, || {
399            let registry = AddressRegistry::new();
400
401            let result = registry.resolve_recipient(virtual_addr);
402            assert!(matches!(
403                result.unwrap_err(),
404                TempoPrecompileError::AddrRegistryError(
405                    AddrRegistryError::VirtualAddressUnregistered(_)
406                )
407            ));
408
409            Ok(())
410        })
411    }
412
413    #[test]
414    fn test_resolve_recipient_virtual_registered() -> eyre::Result<()> {
415        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
416        let (master, salt) = (VIRTUAL_MASTER, VIRTUAL_SALT.into());
417
418        StorageCtx::enter(&mut storage, || {
419            let mut registry = AddressRegistry::new();
420
421            let master_id = registry.register_virtual_master(
422                master,
423                IAddressRegistry::registerVirtualMasterCall { salt },
424            )?;
425
426            let virtual_addr = Address::new_virtual(master_id, UserTag::new(hex!("010203040506")));
427
428            let resolved = registry.resolve_recipient(virtual_addr)?;
429            assert_eq!(resolved, master);
430
431            Ok(())
432        })
433    }
434
435    #[test]
436    fn test_resolve_virtual_address_view() -> eyre::Result<()> {
437        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
438        let (master, salt) = (VIRTUAL_MASTER, VIRTUAL_SALT.into());
439
440        StorageCtx::enter(&mut storage, || {
441            let mut registry = AddressRegistry::new();
442
443            // Non-virtual → zero
444            assert_eq!(
445                registry.resolve_virtual_address(Address::random())?,
446                Address::ZERO
447            );
448
449            // Unregistered virtual → zero
450            let unregistered = Address::new_virtual(MasterId::ZERO, UserTag::ZERO);
451            assert_eq!(
452                registry.resolve_virtual_address(unregistered)?,
453                Address::ZERO
454            );
455
456            // Registered virtual → master
457            let master_id = registry.register_virtual_master(
458                master,
459                IAddressRegistry::registerVirtualMasterCall { salt },
460            )?;
461            let virtual_addr = Address::new_virtual(master_id, UserTag::new(hex!("aabbccddeeff")));
462            assert_eq!(registry.resolve_virtual_address(virtual_addr)?, master);
463
464            Ok(())
465        })
466    }
467
468    #[test]
469    fn test_resolve_recipient_pre_t3_returns_literal() -> eyre::Result<()> {
470        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
471        let virtual_addr = Address::new_virtual(MasterId::ZERO, UserTag::ZERO);
472
473        StorageCtx::enter(&mut storage, || {
474            let registry = AddressRegistry::new();
475            assert_eq!(registry.resolve_recipient(virtual_addr)?, virtual_addr);
476            Ok(())
477        })
478    }
479
480    #[test]
481    fn test_is_valid_master_address() {
482        assert!(!Address::ZERO.is_valid_master());
483        assert!(!Address::new_virtual(MasterId::ZERO, UserTag::ZERO).is_valid_master());
484        assert!(!crate::PATH_USD_ADDRESS.is_valid_master());
485        assert!(Address::repeat_byte(0x42).is_valid_master());
486    }
487}