1pub mod dispatch;
4
5pub use tempo_contracts::precompiles::IReceivePolicyGuard::{self, InboundKind};
6use tempo_contracts::precompiles::{
7 IReceivePolicyGuard::ClaimReceiptV1, ITIP403Registry::BlockedReason, ReceivePolicyGuardError,
8};
9
10use crate::{
11 RECEIVE_POLICY_GUARD_ADDRESS,
12 address_registry::AddressRegistry,
13 error::{Result, TempoPrecompileError},
14 storage::{Handler, Mapping},
15 tip20::{Recipient, TIP20Token},
16};
17use alloy::{
18 primitives::{Address, B256, Bytes, U256},
19 sol_types::SolValue,
20};
21use tempo_precompiles_macros::{Storable, contract};
22use tempo_primitives::TempoAddressExt;
23
24pub const BLOCKED_RECEIPT_VERSION: u8 = 1;
26
27pub const RECOVERY_ORIGINATOR: Address = Address::ZERO;
29
30#[contract(addr = RECEIVE_POLICY_GUARD_ADDRESS)]
32pub struct ReceivePolicyGuard {
33 nonce: u64,
34 balances: Mapping<B256, U256>,
35}
36
37impl ReceivePolicyGuard {
38 pub fn initialize(&mut self) -> Result<()> {
40 self.__initialize()
41 }
42
43 pub fn balance_of(&self, receipt: Bytes) -> Result<U256> {
45 let receipt = ClaimReceiptV1::try_from(receipt)?;
46 self.balances[self.receipt_key(&receipt)?].read()
47 }
48
49 #[allow(clippy::too_many_arguments)]
52 pub(crate) fn store_blocked(
53 &mut self,
54 token: Address,
55 originator: Address,
56 to: &Recipient,
57 recovery_address: Address,
58 amount: U256,
59 blocked_reason: BlockedReason,
60 kind: InboundKind,
61 memo: B256,
62 ) -> Result<(u64, u64)> {
63 debug_assert!(
64 token.is_tip20(),
65 "ReceivePolicyGuard only accepts TIP20 tokens"
66 );
67
68 let is_invalid_reason = match blocked_reason {
69 BlockedReason::RECEIVE_POLICY | BlockedReason::TOKEN_FILTER => false,
70 BlockedReason::NONE | BlockedReason::__Invalid => true,
71 };
72 if is_invalid_reason || matches!(kind, InboundKind::__Invalid) {
73 return Err(ReceivePolicyGuardError::invalid_receipt().into());
74 }
75
76 let receiver = to.target;
77 let recipient = to.virtual_addr.unwrap_or(to.target);
78
79 let blocked_nonce = self.next_receipt_nonce()?;
80 let blocked_at = self.storage.timestamp().saturating_to::<u64>();
81 let receipt = IReceivePolicyGuard::ClaimReceiptV1::new(
82 token,
83 recovery_address,
84 originator,
85 recipient,
86 blocked_at,
87 blocked_nonce,
88 blocked_reason as u8,
89 kind,
90 memo,
91 );
92 let key = self.receipt_key(&receipt)?;
93 self.balances[key].write(amount)?;
94
95 self.emit_event(receipt.blocked_event(receiver, amount))?;
96 Ok((blocked_nonce, blocked_at))
97 }
98
99 pub fn claim(&mut self, msg_sender: Address, to: Address, receipt: Bytes) -> Result<()> {
101 if to == RECEIVE_POLICY_GUARD_ADDRESS {
102 return Err(ReceivePolicyGuardError::invalid_claim_address().into());
103 }
104
105 let (receipt, receiver, recovery_mode) = resolve_receipt(receipt)?;
106 let recovery_authority = recovery_mode.authority(&receipt);
107 if recovery_authority != msg_sender {
108 return Err(ReceivePolicyGuardError::unauthorized_claimer().into());
109 };
110
111 let key = self.receipt_key(&receipt)?;
112 let amount = self.balances[key].read()?;
113 if amount.is_zero() {
114 return Err(ReceivePolicyGuardError::invalid_receipt().into());
115 }
116
117 self.balances[key].write(U256::ZERO)?;
118
119 TIP20Token::from_address(receipt.token)?.release_blocked_funds(
120 receipt.originator,
121 receiver,
122 to,
123 amount,
124 recovery_mode,
125 recovery_authority,
126 )?;
127
128 self.emit_event(receipt.claimed_event(receiver, msg_sender, to, amount))
129 }
130
131 pub fn burn_blocked_receipt(&mut self, msg_sender: Address, receipt: Bytes) -> Result<()> {
136 let (receipt, receiver, recovery_mode) = resolve_receipt(receipt)?;
137
138 let key = self.receipt_key(&receipt)?;
139 let amount = self.balances[key].read()?;
140 if amount.is_zero() {
141 return Err(ReceivePolicyGuardError::invalid_receipt().into());
142 }
143
144 let owner = recovery_mode.policy_subject(receipt.originator, receiver);
146 TIP20Token::from_address(receipt.token)?.burn_blocked(msg_sender, owner, amount, false)?;
147 self.balances[key].write(U256::ZERO)?;
148
149 self.emit_event(receipt.burned_event(receiver, msg_sender, amount))
150 }
151
152 fn next_receipt_nonce(&mut self) -> Result<u64> {
154 let nonce = self.nonce.read()?.max(1);
155 self.nonce.write(
156 nonce
157 .checked_add(1)
158 .ok_or(TempoPrecompileError::under_overflow())?,
159 )?;
160 Ok(nonce)
161 }
162
163 fn receipt_key(&self, receipt: &IReceivePolicyGuard::ClaimReceiptV1) -> Result<B256> {
165 self.storage.keccak256(receipt.abi_encode().as_ref())
166 }
167}
168
169#[derive(Debug, Clone, Copy, Default, Storable, PartialEq)]
171#[repr(u8)]
172pub(crate) enum RecoveryMode {
173 #[default]
174 Originator,
175 Receiver,
176 ThirdParty,
177}
178
179impl RecoveryMode {
180 pub(crate) fn encode(authority: Address, msg_sender: Address) -> (Self, Address) {
182 if authority == RECOVERY_ORIGINATOR {
183 (Self::Originator, Address::ZERO)
184 } else if authority == msg_sender {
185 (Self::Receiver, Address::ZERO)
186 } else {
187 (Self::ThirdParty, authority)
188 }
189 }
190
191 pub(crate) fn from(receipt: &ClaimReceiptV1, receiver: Address) -> Self {
193 if receipt.recoveryAuthority == RECOVERY_ORIGINATOR {
194 Self::Originator
195 } else if receipt.recoveryAuthority == receiver {
196 Self::Receiver
197 } else {
198 Self::ThirdParty
199 }
200 }
201
202 pub(crate) fn authority(self, receipt: &ClaimReceiptV1) -> Address {
204 match self {
205 Self::Originator => receipt.originator,
206 Self::Receiver | Self::ThirdParty => receipt.recoveryAuthority,
207 }
208 }
209
210 pub(crate) fn policy_subject(self, originator: Address, receiver: Address) -> Address {
212 match self {
213 Self::Originator => originator,
214 Self::Receiver | Self::ThirdParty => receiver,
215 }
216 }
217
218 pub(crate) fn is_reroute(self, to: Address, receiver: Address) -> bool {
222 match self {
223 Self::Originator => true,
224 Self::Receiver | Self::ThirdParty => to != receiver,
225 }
226 }
227
228 pub(crate) fn spending_account(self, recovery_authority: Address) -> Option<Address> {
230 match self {
231 Self::Originator | Self::Receiver => Some(recovery_authority),
232 Self::ThirdParty => None,
233 }
234 }
235}
236
237fn resolve_receipt(bytes: Bytes) -> Result<(ClaimReceiptV1, Address, RecoveryMode)> {
238 let receipt = ClaimReceiptV1::try_from(bytes)?;
239 let receiver = AddressRegistry::new()
240 .resolve_recipient(receipt.recipient)
241 .map_err(|_| ReceivePolicyGuardError::invalid_claim_address())?;
242 let recovery_mode = RecoveryMode::from(&receipt, receiver);
243
244 Ok((receipt, receiver, recovery_mode))
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250 use crate::{
251 address_registry::AddressRegistry,
252 error::TempoPrecompileError,
253 storage::{ContractStorage, StorageCtx, hashmap::HashMapStorageProvider},
254 test_util::{TIP20Setup, VIRTUAL_MASTER, register_virtual_master},
255 tip20::{BURN_BLOCKED_ROLE, ITIP20},
256 tip403_registry::{ALLOW_ALL_POLICY_ID, REJECT_ALL_POLICY_ID, TIP403Registry},
257 };
258 use alloy::sol_types::SolValue;
259 use tempo_chainspec::hardfork::TempoHardfork;
260 use tempo_contracts::precompiles::{
261 ITIP403Registry, ReceivePolicyGuardEvent, TIP20Error, TIP20Event,
262 };
263
264 fn block_all_senders(receiver: Address, recovery_authority: Address) -> Result<()> {
265 TIP403Registry::new().set_receive_policy(
266 receiver,
267 ITIP403Registry::setReceivePolicyCall {
268 senderPolicyId: REJECT_ALL_POLICY_ID,
269 tokenFilterId: ALLOW_ALL_POLICY_ID,
270 recoveryAuthority: recovery_authority,
271 },
272 )
273 }
274
275 fn assert_invalid_receipt(result: Result<()>) {
276 assert_eq!(
277 result.unwrap_err(),
278 ReceivePolicyGuardError::invalid_receipt().into()
279 );
280 }
281
282 fn assert_unauthorized(result: Result<()>) {
283 assert_eq!(
284 result.unwrap_err(),
285 ReceivePolicyGuardError::unauthorized_claimer().into()
286 );
287 }
288
289 #[test]
290 fn test_claim_blocked_inbound() -> eyre::Result<()> {
291 let admin = Address::random();
292 let transfer_originator = Address::random();
293 let receiver = Address::random();
294 let amount = U256::from(100u64);
295 let blocked_at = 1_728_000u64;
296
297 for kind in [InboundKind::TRANSFER, InboundKind::MINT] {
298 let originator = match kind {
299 InboundKind::TRANSFER => transfer_originator,
300 InboundKind::MINT => admin,
301 InboundKind::__Invalid => unreachable!(),
302 };
303
304 let third_party = Address::random();
305 for (configured_authority, claimer, destination, is_third_party) in [
306 (RECOVERY_ORIGINATOR, originator, originator, false),
307 (receiver, receiver, receiver, false),
308 (third_party, third_party, Address::random(), true),
309 ] {
310 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
311 storage.set_timestamp(U256::from(blocked_at));
312
313 StorageCtx::enter(&mut storage, || {
314 let mut setup = TIP20Setup::create("T", "T", admin).with_issuer(admin);
315 if matches!(kind, InboundKind::TRANSFER) {
316 setup = setup.with_mint(originator, amount);
317 }
318 let mut token = setup.apply()?;
319 block_all_senders(receiver, configured_authority)?;
320
321 let unknown = ClaimReceiptV1::new(
322 token.address(),
323 configured_authority,
324 originator,
325 receiver,
326 blocked_at,
327 99,
328 BlockedReason::RECEIVE_POLICY as u8,
329 kind,
330 B256::ZERO,
331 );
332 let mut guard = ReceivePolicyGuard::new();
333 assert_eq!(guard.balance_of(unknown.abi_encode().into())?, U256::ZERO);
334
335 guard.clear_emitted_events();
336 token.clear_emitted_events();
337 match kind {
338 InboundKind::TRANSFER => {
339 token.transfer(
340 originator,
341 ITIP20::transferCall {
342 to: receiver,
343 amount,
344 },
345 )?;
346 token.assert_emitted_events(vec![TIP20Event::transfer(
347 originator,
348 RECEIVE_POLICY_GUARD_ADDRESS,
349 amount,
350 )]);
351 }
352 InboundKind::MINT => {
353 token.mint(
354 admin,
355 ITIP20::mintCall {
356 to: receiver,
357 amount,
358 },
359 )?;
360 token.assert_emitted_events(vec![
361 TIP20Event::transfer(
362 Address::ZERO,
363 RECEIVE_POLICY_GUARD_ADDRESS,
364 amount,
365 ),
366 TIP20Event::mint(RECEIVE_POLICY_GUARD_ADDRESS, amount),
367 ]);
368 }
369 InboundKind::__Invalid => unreachable!(),
370 }
371 let receipt = ClaimReceiptV1::new(
372 token.address(),
373 configured_authority,
374 originator,
375 receiver,
376 blocked_at,
377 1,
378 BlockedReason::RECEIVE_POLICY as u8,
379 kind,
380 B256::ZERO,
381 );
382 guard.assert_emitted_events(vec![ReceivePolicyGuardEvent::TransferBlocked(
383 IReceivePolicyGuard::TransferBlocked {
384 token: token.address(),
385 receiver,
386 blockedNonce: 1,
387 receiptVersion: BLOCKED_RECEIPT_VERSION,
388 amount,
389 receipt: receipt.abi_encode().into(),
390 },
391 )]);
392 assert_eq!(guard.balance_of(receipt.abi_encode().into())?, amount);
393
394 if is_third_party {
395 assert_unauthorized(guard.claim(
396 receiver,
397 receiver,
398 receipt.abi_encode().into(),
399 ));
400 assert_unauthorized(guard.claim(
401 Address::random(),
402 receiver,
403 receipt.abi_encode().into(),
404 ));
405 assert_eq!(guard.balance_of(receipt.abi_encode().into())?, amount);
406 }
407
408 guard.clear_emitted_events();
409 guard.claim(claimer, destination, receipt.abi_encode().into())?;
410 guard.assert_emitted_events(vec![ReceivePolicyGuardEvent::ReceiptClaimed(
411 IReceivePolicyGuard::ReceiptClaimed {
412 token: token.address(),
413 receiver,
414 blockedNonce: 1,
415 blockedAt: blocked_at,
416 receiptVersion: BLOCKED_RECEIPT_VERSION,
417 originator,
418 recipient: receiver,
419 recoveryAuthority: configured_authority,
420 caller: claimer,
421 to: destination,
422 amount,
423 },
424 )]);
425
426 assert_eq!(guard.balance_of(receipt.abi_encode().into())?, U256::ZERO);
427 assert_eq!(
428 token.balance_of(ITIP20::balanceOfCall {
429 account: RECEIVE_POLICY_GUARD_ADDRESS
430 })?,
431 U256::ZERO
432 );
433 assert_eq!(
434 token.balance_of(ITIP20::balanceOfCall {
435 account: destination
436 })?,
437 amount
438 );
439
440 Ok::<(), TempoPrecompileError>(())
441 })?;
442 }
443 }
444
445 Ok(())
446 }
447
448 #[test]
449 fn test_burn_blocked_receipt_emits_receipt_burned() -> eyre::Result<()> {
450 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
451 let blocked_at = 1_728_010u64;
452 storage.set_timestamp(U256::from(blocked_at));
453
454 let admin = Address::random();
455 let burner = Address::random();
456 let originator = Address::random();
457 let receiver = Address::random();
458 let amount = U256::from(100u64);
459
460 StorageCtx::enter(&mut storage, || {
461 let mut token = TIP20Setup::create("T", "T", admin)
462 .with_issuer(admin)
463 .with_role(burner, *BURN_BLOCKED_ROLE)
464 .with_mint(originator, amount)
465 .apply()?;
466 block_all_senders(receiver, receiver)?;
467
468 token.transfer(
469 originator,
470 ITIP20::transferCall {
471 to: receiver,
472 amount,
473 },
474 )?;
475
476 let receipt = ClaimReceiptV1::new(
477 token.address(),
478 receiver,
479 originator,
480 receiver,
481 blocked_at,
482 1,
483 BlockedReason::RECEIVE_POLICY as u8,
484 InboundKind::TRANSFER,
485 B256::ZERO,
486 );
487
488 let mut registry = TIP403Registry::new();
489 let policy_id = registry.create_policy(
490 admin,
491 ITIP403Registry::createPolicyCall {
492 admin,
493 policyType: ITIP403Registry::PolicyType::BLACKLIST,
494 },
495 )?;
496 registry.modify_policy_blacklist(
497 admin,
498 ITIP403Registry::modifyPolicyBlacklistCall {
499 policyId: policy_id,
500 account: receiver,
501 restricted: true,
502 },
503 )?;
504 token.change_transfer_policy_id(
505 admin,
506 ITIP20::changeTransferPolicyIdCall {
507 newPolicyId: policy_id,
508 },
509 )?;
510
511 let mut guard = ReceivePolicyGuard::new();
512 guard.clear_emitted_events();
513
514 guard.burn_blocked_receipt(burner, receipt.abi_encode().into())?;
515
516 guard.assert_emitted_events(vec![ReceivePolicyGuardEvent::ReceiptBurned(
517 IReceivePolicyGuard::ReceiptBurned {
518 token: token.address(),
519 receiver,
520 receiptVersion: BLOCKED_RECEIPT_VERSION,
521 blockedNonce: 1,
522 blockedAt: blocked_at,
523 originator,
524 recipient: receiver,
525 recoveryAuthority: receiver,
526 caller: burner,
527 amount,
528 },
529 )]);
530 assert_eq!(guard.balance_of(receipt.abi_encode().into())?, U256::ZERO);
531
532 Ok(())
533 })
534 }
535
536 #[test]
537 fn test_claim_rejects_when_token_paused() -> eyre::Result<()> {
538 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
539 let blocked_at = 1_728_000u64;
540 storage.set_timestamp(U256::from(blocked_at));
541
542 let admin = Address::random();
543 let originator = Address::random();
544 let receiver = Address::random();
545 let amount = U256::from(100u64);
546
547 StorageCtx::enter(&mut storage, || {
548 let mut token = TIP20Setup::create("T", "T", admin)
549 .with_issuer(admin)
550 .with_role(admin, TIP20Token::pause_role())
551 .with_mint(originator, amount)
552 .apply()?;
553 block_all_senders(receiver, receiver)?;
554
555 token.transfer(
556 originator,
557 ITIP20::transferCall {
558 to: receiver,
559 amount,
560 },
561 )?;
562 token.pause(admin, ITIP20::pauseCall {})?;
563
564 let receipt = ClaimReceiptV1::new(
565 token.address(),
566 receiver,
567 originator,
568 receiver,
569 blocked_at,
570 1,
571 BlockedReason::RECEIVE_POLICY as u8,
572 InboundKind::TRANSFER,
573 B256::ZERO,
574 );
575 let mut guard = ReceivePolicyGuard::new();
576 let result = guard.claim(receiver, receiver, receipt.abi_encode().into());
577 assert_eq!(result.unwrap_err(), TIP20Error::contract_paused().into());
578
579 Ok(())
580 })
581 }
582
583 #[test]
584 fn test_receive_policy_guard_balance_matches_open_receipts() -> eyre::Result<()> {
585 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
586 storage.set_timestamp(U256::from(1_728_001u64));
587
588 let admin = Address::random();
589 let originator = Address::random();
590 let receiver_a = Address::random();
591 let receiver_b = Address::random();
592 let receiver_c = Address::random();
593 let recovery = Address::random();
594 let amount_a = U256::from(30u64);
595 let amount_b = U256::from(45u64);
596 let amount_c = U256::from(70u64);
597
598 StorageCtx::enter(&mut storage, || {
599 let mut token_a = TIP20Setup::create("A", "A", admin)
600 .with_issuer(admin)
601 .with_mint(originator, amount_a + amount_b)
602 .apply()?;
603 let mut token_b = TIP20Setup::create("B", "B", admin)
604 .with_issuer(admin)
605 .with_mint(originator, amount_c)
606 .apply()?;
607
608 block_all_senders(receiver_a, receiver_a)?;
609 block_all_senders(receiver_b, recovery)?;
610 block_all_senders(receiver_c, receiver_c)?;
611
612 token_a.transfer(
613 originator,
614 ITIP20::transferCall {
615 to: receiver_a,
616 amount: amount_a,
617 },
618 )?;
619 token_a.transfer(
620 originator,
621 ITIP20::transferCall {
622 to: receiver_b,
623 amount: amount_b,
624 },
625 )?;
626 token_b.transfer(
627 originator,
628 ITIP20::transferCall {
629 to: receiver_c,
630 amount: amount_c,
631 },
632 )?;
633
634 let receipt_a = ClaimReceiptV1::new(
635 token_a.address(),
636 receiver_a,
637 originator,
638 receiver_a,
639 1_728_001,
640 1,
641 BlockedReason::RECEIVE_POLICY as u8,
642 InboundKind::TRANSFER,
643 B256::ZERO,
644 );
645 let receipt_b = ClaimReceiptV1::new(
646 token_a.address(),
647 recovery,
648 originator,
649 receiver_b,
650 1_728_001,
651 2,
652 BlockedReason::RECEIVE_POLICY as u8,
653 InboundKind::TRANSFER,
654 B256::ZERO,
655 );
656 let receipt_c = ClaimReceiptV1::new(
657 token_b.address(),
658 receiver_c,
659 originator,
660 receiver_c,
661 1_728_001,
662 3,
663 BlockedReason::RECEIVE_POLICY as u8,
664 InboundKind::TRANSFER,
665 B256::ZERO,
666 );
667
668 let mut guard = ReceivePolicyGuard::new();
669 assert_eq!(
670 token_a.balance_of(ITIP20::balanceOfCall {
671 account: RECEIVE_POLICY_GUARD_ADDRESS
672 })?,
673 guard.balance_of(receipt_a.abi_encode().into())?
674 + guard.balance_of(receipt_b.abi_encode().into())?
675 );
676 assert_eq!(
677 token_b.balance_of(ITIP20::balanceOfCall {
678 account: RECEIVE_POLICY_GUARD_ADDRESS
679 })?,
680 guard.balance_of(receipt_c.abi_encode().into())?
681 );
682
683 guard.claim(receiver_a, receiver_a, receipt_a.abi_encode().into())?;
684 assert_eq!(
685 token_a.balance_of(ITIP20::balanceOfCall {
686 account: RECEIVE_POLICY_GUARD_ADDRESS
687 })?,
688 guard.balance_of(receipt_b.abi_encode().into())?
689 );
690 assert_eq!(
691 token_b.balance_of(ITIP20::balanceOfCall {
692 account: RECEIVE_POLICY_GUARD_ADDRESS
693 })?,
694 guard.balance_of(receipt_c.abi_encode().into())?
695 );
696
697 guard.claim(recovery, recovery, receipt_b.abi_encode().into())?;
698 assert_eq!(
699 token_a.balance_of(ITIP20::balanceOfCall {
700 account: RECEIVE_POLICY_GUARD_ADDRESS
701 })?,
702 U256::ZERO
703 );
704 assert_eq!(
705 token_b.balance_of(ITIP20::balanceOfCall {
706 account: RECEIVE_POLICY_GUARD_ADDRESS
707 })?,
708 guard.balance_of(receipt_c.abi_encode().into())?
709 );
710
711 Ok(())
712 })
713 }
714
715 #[test]
716 fn test_receipt_rejects_bad_encoding() -> eyre::Result<()> {
717 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
718
719 StorageCtx::enter(&mut storage, || {
720 let guard = ReceivePolicyGuard::new();
721 let result = guard.balance_of(vec![0xde, 0xad, 0xbe, 0xef].into());
722 assert!(matches!(
723 result,
724 Err(e) if e == ReceivePolicyGuardError::invalid_receipt().into()
725 ));
726
727 Ok(())
728 })
729 }
730
731 #[test]
732 fn test_store_rejects_invalid_metadata() -> eyre::Result<()> {
733 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
734 let admin = Address::random();
735
736 StorageCtx::enter(&mut storage, || {
737 let token = TIP20Setup::create("T", "T", admin).apply()?;
738 let mut guard = ReceivePolicyGuard::new();
739
740 for (blocked_reason, kind) in [
741 (BlockedReason::NONE, InboundKind::TRANSFER),
742 (BlockedReason::__Invalid, InboundKind::TRANSFER),
743 (BlockedReason::RECEIVE_POLICY, InboundKind::__Invalid),
744 ] {
745 let result = guard.store_blocked(
746 token.address(),
747 Address::random(),
748 &Recipient::direct(Address::random()),
749 Address::ZERO,
750 U256::from(1u64),
751 blocked_reason,
752 kind,
753 B256::ZERO,
754 );
755 assert!(matches!(
756 result,
757 Err(e) if e == ReceivePolicyGuardError::invalid_receipt().into()
758 ));
759 }
760
761 Ok(())
762 })
763 }
764
765 #[test]
766 fn test_receipt_key_binds_receipt_fields() -> eyre::Result<()> {
767 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
768 storage.set_timestamp(U256::from(1_728_002u64));
769
770 let admin = Address::random();
771 let originator_a = Address::random();
772 let originator_b = Address::random();
773 let recipient = Address::random();
774 let recovery = Address::random();
775 let memo = B256::repeat_byte(0x11);
776 let amount_a = U256::from(10u64);
777 let amount_b = U256::from(20u64);
778
779 StorageCtx::enter(&mut storage, || {
780 let token_a = TIP20Setup::create("A", "A", admin).apply()?;
781 let token_b = TIP20Setup::create("B", "B", admin).apply()?;
782 let mut guard = ReceivePolicyGuard::new();
783
784 let (nonce_a, blocked_at_a) = guard.store_blocked(
785 token_a.address(),
786 originator_a,
787 &Recipient::direct(recipient),
788 recovery,
789 amount_a,
790 BlockedReason::RECEIVE_POLICY,
791 InboundKind::TRANSFER,
792 memo,
793 )?;
794 let (nonce_b, blocked_at_b) = guard.store_blocked(
795 token_b.address(),
796 originator_b,
797 &Recipient::direct(recipient),
798 recovery,
799 amount_b,
800 BlockedReason::TOKEN_FILTER,
801 InboundKind::MINT,
802 B256::repeat_byte(0x22),
803 )?;
804
805 let receipt_a = ClaimReceiptV1::new(
806 token_a.address(),
807 recovery,
808 originator_a,
809 recipient,
810 blocked_at_a,
811 nonce_a,
812 BlockedReason::RECEIVE_POLICY as u8,
813 InboundKind::TRANSFER,
814 memo,
815 );
816 let receipt_b = ClaimReceiptV1::new(
817 token_b.address(),
818 recovery,
819 originator_b,
820 recipient,
821 blocked_at_b,
822 nonce_b,
823 BlockedReason::TOKEN_FILTER as u8,
824 InboundKind::MINT,
825 B256::repeat_byte(0x22),
826 );
827
828 assert_eq!(guard.balance_of(receipt_a.abi_encode().into())?, amount_a);
829 assert_eq!(guard.balance_of(receipt_b.abi_encode().into())?, amount_b);
830
831 let mutated_receipts = [
832 ClaimReceiptV1::new(
833 token_a.address(),
834 recovery,
835 Address::random(),
836 recipient,
837 blocked_at_a,
838 nonce_a,
839 BlockedReason::RECEIVE_POLICY as u8,
840 InboundKind::TRANSFER,
841 memo,
842 ),
843 ClaimReceiptV1::new(
844 token_a.address(),
845 recovery,
846 originator_a,
847 Address::random(),
848 blocked_at_a,
849 nonce_a,
850 BlockedReason::RECEIVE_POLICY as u8,
851 InboundKind::TRANSFER,
852 memo,
853 ),
854 ClaimReceiptV1::new(
855 token_a.address(),
856 recovery,
857 originator_a,
858 recipient,
859 blocked_at_a + 1,
860 nonce_a,
861 BlockedReason::RECEIVE_POLICY as u8,
862 InboundKind::TRANSFER,
863 memo,
864 ),
865 ClaimReceiptV1::new(
866 token_a.address(),
867 recovery,
868 originator_a,
869 recipient,
870 blocked_at_a,
871 nonce_a + 1,
872 BlockedReason::RECEIVE_POLICY as u8,
873 InboundKind::TRANSFER,
874 memo,
875 ),
876 ClaimReceiptV1::new(
877 token_a.address(),
878 recovery,
879 originator_a,
880 recipient,
881 blocked_at_a,
882 nonce_a,
883 BlockedReason::TOKEN_FILTER as u8,
884 InboundKind::TRANSFER,
885 memo,
886 ),
887 ClaimReceiptV1::new(
888 token_a.address(),
889 recovery,
890 originator_a,
891 recipient,
892 blocked_at_a,
893 nonce_a,
894 BlockedReason::RECEIVE_POLICY as u8,
895 InboundKind::MINT,
896 memo,
897 ),
898 ClaimReceiptV1::new(
899 token_a.address(),
900 recovery,
901 originator_a,
902 recipient,
903 blocked_at_a,
904 nonce_a,
905 BlockedReason::RECEIVE_POLICY as u8,
906 InboundKind::TRANSFER,
907 B256::repeat_byte(0x33),
908 ),
909 ];
910
911 for mutated in mutated_receipts {
912 assert_eq!(guard.balance_of(mutated.abi_encode().into())?, U256::ZERO);
913 }
914 assert_eq!(guard.balance_of(receipt_a.abi_encode().into())?, amount_a);
915 assert_eq!(guard.balance_of(receipt_b.abi_encode().into())?, amount_b);
916
917 Ok(())
918 })
919 }
920
921 #[test]
922 fn test_claim_rejects_missing_receipt() -> eyre::Result<()> {
923 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
924 storage.set_timestamp(U256::from(1_728_003u64));
925
926 let admin = Address::random();
927 let originator = Address::random();
928 let receiver = Address::random();
929 let amount = U256::from(100u64);
930 StorageCtx::enter(&mut storage, || {
931 let mut token = TIP20Setup::create("T", "T", admin)
932 .with_issuer(admin)
933 .with_mint(originator, amount)
934 .apply()?;
935
936 let receipt = ClaimReceiptV1::new(
937 token.address(),
938 receiver,
939 originator,
940 receiver,
941 1_728_003,
942 1,
943 BlockedReason::RECEIVE_POLICY as u8,
944 InboundKind::TRANSFER,
945 B256::ZERO,
946 );
947
948 let mut guard = ReceivePolicyGuard::new();
949 assert_invalid_receipt(guard.claim(receiver, receiver, receipt.abi_encode().into()));
950
951 block_all_senders(receiver, receiver)?;
952 token.transfer(
953 originator,
954 ITIP20::transferCall {
955 to: receiver,
956 amount,
957 },
958 )?;
959 guard.claim(receiver, receiver, receipt.abi_encode().into())?;
960 assert_invalid_receipt(guard.claim(receiver, receiver, receipt.abi_encode().into()));
961
962 Ok(())
963 })
964 }
965
966 #[test]
967 fn test_claim_requires_authorized_caller() -> eyre::Result<()> {
968 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
969 storage.set_timestamp(U256::from(1_728_004u64));
970
971 let admin = Address::random();
972 let originator = Address::random();
973 let receiver = Address::random();
974 let recovery_receiver = Address::random();
975 let recovery = Address::random();
976 let stranger = Address::random();
977 let amount = U256::from(50u64);
978
979 StorageCtx::enter(&mut storage, || {
980 let mut token = TIP20Setup::create("T", "T", admin)
981 .with_issuer(admin)
982 .with_mint(originator, amount * U256::from(2u64))
983 .apply()?;
984 block_all_senders(receiver, receiver)?;
985 block_all_senders(recovery_receiver, recovery)?;
986
987 token.transfer(
988 originator,
989 ITIP20::transferCall {
990 to: receiver,
991 amount,
992 },
993 )?;
994 token.transfer(
995 originator,
996 ITIP20::transferCall {
997 to: recovery_receiver,
998 amount,
999 },
1000 )?;
1001
1002 let self_receipt = ClaimReceiptV1::new(
1003 token.address(),
1004 receiver,
1005 originator,
1006 receiver,
1007 1_728_004,
1008 1,
1009 BlockedReason::RECEIVE_POLICY as u8,
1010 InboundKind::TRANSFER,
1011 B256::ZERO,
1012 );
1013 let recovery_receipt = ClaimReceiptV1::new(
1014 token.address(),
1015 recovery,
1016 originator,
1017 recovery_receiver,
1018 1_728_004,
1019 2,
1020 BlockedReason::RECEIVE_POLICY as u8,
1021 InboundKind::TRANSFER,
1022 B256::ZERO,
1023 );
1024
1025 let mut guard = ReceivePolicyGuard::new();
1026 assert_unauthorized(guard.claim(stranger, receiver, self_receipt.abi_encode().into()));
1027 for caller in [recovery_receiver, stranger] {
1028 assert_unauthorized(guard.claim(
1029 caller,
1030 recovery_receiver,
1031 recovery_receipt.abi_encode().into(),
1032 ));
1033 }
1034 assert_eq!(guard.balance_of(self_receipt.abi_encode().into())?, amount);
1035 assert_eq!(
1036 guard.balance_of(recovery_receipt.abi_encode().into())?,
1037 amount
1038 );
1039
1040 Ok(())
1041 })
1042 }
1043
1044 #[test]
1045 fn test_claim_virtual_recipient() -> eyre::Result<()> {
1046 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
1047 storage.set_timestamp(U256::from(1_728_009u64));
1048
1049 let admin = Address::random();
1050 let originator = Address::random();
1051 let amount = U256::from(123u64);
1052
1053 StorageCtx::enter(&mut storage, || {
1054 let (_, virtual_addr) = register_virtual_master(&mut AddressRegistry::new())?;
1055 let mut token = TIP20Setup::create("T", "T", admin)
1056 .with_issuer(admin)
1057 .with_mint(originator, amount)
1058 .apply()?;
1059 block_all_senders(VIRTUAL_MASTER, VIRTUAL_MASTER)?;
1060 let mut guard = ReceivePolicyGuard::new();
1061 guard.clear_emitted_events();
1062 token.transfer(
1063 originator,
1064 ITIP20::transferCall {
1065 to: virtual_addr,
1066 amount,
1067 },
1068 )?;
1069
1070 let receipt = ClaimReceiptV1::new(
1071 token.address(),
1072 VIRTUAL_MASTER,
1073 originator,
1074 virtual_addr,
1075 1_728_009,
1076 1,
1077 BlockedReason::RECEIVE_POLICY as u8,
1078 InboundKind::TRANSFER,
1079 B256::ZERO,
1080 );
1081 guard.assert_emitted_events(vec![ReceivePolicyGuardEvent::TransferBlocked(
1082 IReceivePolicyGuard::TransferBlocked {
1083 token: token.address(),
1084 receiver: VIRTUAL_MASTER,
1085 blockedNonce: 1,
1086 receiptVersion: BLOCKED_RECEIPT_VERSION,
1087 amount,
1088 receipt: receipt.abi_encode().into(),
1089 },
1090 )]);
1091 guard.clear_emitted_events();
1092 guard.claim(VIRTUAL_MASTER, VIRTUAL_MASTER, receipt.abi_encode().into())?;
1093
1094 guard.assert_emitted_events(vec![ReceivePolicyGuardEvent::ReceiptClaimed(
1095 IReceivePolicyGuard::ReceiptClaimed {
1096 token: token.address(),
1097 receiver: VIRTUAL_MASTER,
1098 blockedNonce: 1,
1099 blockedAt: 1_728_009,
1100 receiptVersion: BLOCKED_RECEIPT_VERSION,
1101 originator,
1102 recipient: virtual_addr,
1103 recoveryAuthority: VIRTUAL_MASTER,
1104 caller: VIRTUAL_MASTER,
1105 to: VIRTUAL_MASTER,
1106 amount,
1107 },
1108 )]);
1109 assert_eq!(
1110 token.balance_of(ITIP20::balanceOfCall {
1111 account: VIRTUAL_MASTER
1112 })?,
1113 amount
1114 );
1115 assert_eq!(
1116 token.balance_of(ITIP20::balanceOfCall {
1117 account: virtual_addr
1118 })?,
1119 U256::ZERO
1120 );
1121
1122 Ok(())
1123 })
1124 }
1125}