1pub mod dispatch;
9
10use crate::{
11 error::Result,
12 signature_verifier::SignatureVerifier,
13 storage::{Handler, Mapping},
14 tip20::{ITIP20, Recipient, TIP20Token, is_tip20_prefix},
15 tip403_registry::AuthRole,
16};
17use alloy::{
18 primitives::{Address, B256, U256, aliases::U96, keccak256},
19 sol_types::SolValue,
20};
21use std::sync::LazyLock;
22use tempo_chainspec::constants::{mainnet::MAINNET_CHAIN_ID, moderato::MODERATO_CHAIN_ID};
23pub use tempo_contracts::precompiles::{
24 ITIP20ChannelReserve, TIP20_CHANNEL_RESERVE_ADDRESS, TIP20ChannelReserveError,
25 TIP20ChannelReserveEvent,
26};
27use tempo_precompiles_macros::{Storable, contract};
28use tempo_primitives::TempoAddressExt;
29
30pub const CLOSE_GRACE_PERIOD: u64 = 15 * 60;
32
33static VOUCHER_TYPEHASH: LazyLock<B256> =
35 LazyLock::new(|| keccak256(b"Voucher(bytes32 channelId,uint96 cumulativeAmount)"));
36static EIP712_DOMAIN_TYPEHASH: LazyLock<B256> = LazyLock::new(|| {
38 keccak256(b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
39});
40static NAME_HASH: LazyLock<B256> = LazyLock::new(|| keccak256(b"TIP20 Channel Reserve"));
42static VERSION_HASH: LazyLock<B256> = LazyLock::new(|| keccak256(b"1"));
44
45static DOMAIN_SEPARATOR_MAINNET: LazyLock<B256> =
47 LazyLock::new(|| domain_separator_inner(MAINNET_CHAIN_ID));
48static DOMAIN_SEPARATOR_TESTNET: LazyLock<B256> =
50 LazyLock::new(|| domain_separator_inner(MODERATO_CHAIN_ID));
51
52#[derive(Debug, Clone, Copy, Default, Storable)]
58struct PackedChannelState {
59 settled: U96,
60 deposit: U96,
61 close_requested_at: u32,
62}
63
64impl PackedChannelState {
65 fn exists(self) -> bool {
67 !self.deposit.is_zero()
68 }
69
70 fn close_requested_at(self) -> Option<u32> {
72 (self.close_requested_at != 0).then_some(self.close_requested_at)
73 }
74
75 fn to_sol(self) -> ITIP20ChannelReserve::ChannelState {
77 ITIP20ChannelReserve::ChannelState {
78 settled: self.settled,
79 deposit: self.deposit,
80 closeRequestedAt: self.close_requested_at,
81 }
82 }
83}
84
85#[contract(addr = TIP20_CHANNEL_RESERVE_ADDRESS)]
86pub struct TIP20ChannelReserve {
87 channel_states: Mapping<B256, PackedChannelState>,
89
90 opened_this_tx: Mapping<B256, bool>,
94 channel_open_context_hash: B256,
96}
97
98impl TIP20ChannelReserve {
99 pub fn initialize(&mut self) -> Result<()> {
101 self.__initialize()
102 }
103
104 pub fn set_channel_open_context_hash(&mut self, hash: B256) -> Result<()> {
111 self.channel_open_context_hash.t_write(hash)
112 }
113
114 pub fn open(
122 &mut self,
123 msg_sender: Address,
124 call: ITIP20ChannelReserve::openCall,
125 ) -> Result<B256> {
126 if call.payee.is_zero()
127 || is_tip20_prefix(call.payee)
128 || (call.payee.is_virtual() && (call.operator.is_zero() || call.operator.is_virtual()))
129 {
130 return Err(TIP20ChannelReserveError::invalid_payee().into());
131 }
132
133 let mut token = TIP20Token::from_address(call.token)?;
134
135 let deposit = call.deposit;
136 if deposit.is_zero() {
137 return Err(TIP20ChannelReserveError::zero_deposit().into());
138 }
139
140 let expiring_nonce_hash = self.enclosing_channel_open_context_hash()?;
141 let channel_id = self.compute_channel_id_inner(
142 msg_sender,
143 call.payee,
144 call.operator,
145 call.token,
146 call.salt,
147 call.authorizedSigner,
148 expiring_nonce_hash,
149 )?;
150 if self.channel_states[channel_id].read()?.exists()
151 || self.opened_this_tx[channel_id].t_read()?
152 {
153 return Err(TIP20ChannelReserveError::channel_already_exists().into());
154 }
155
156 token.ensure_authorized_as(Recipient::resolve(call.payee)?.target, AuthRole::Recipient)?;
157 token.system_transfer_from(self.address, msg_sender, U256::from(call.deposit))?;
158
159 self.channel_states[channel_id].write(PackedChannelState {
160 settled: U96::ZERO,
161 deposit,
162 close_requested_at: 0,
163 })?;
164 self.opened_this_tx[channel_id].t_write(true)?;
165
166 self.emit_event(TIP20ChannelReserveEvent::ChannelOpened(
167 ITIP20ChannelReserve::ChannelOpened {
168 channelId: channel_id,
169 payer: msg_sender,
170 payee: call.payee,
171 operator: call.operator,
172 token: call.token,
173 authorizedSigner: call.authorizedSigner,
174 salt: call.salt,
175 expiringNonceHash: expiring_nonce_hash,
176 deposit: call.deposit,
177 },
178 ))?;
179
180 Ok(channel_id)
181 }
182
183 pub fn settle(
188 &mut self,
189 msg_sender: Address,
190 call: ITIP20ChannelReserve::settleCall,
191 ) -> Result<()> {
192 let channel_id = self.channel_id(&call.descriptor)?;
193 let mut state = self.load_existing_state(channel_id)?;
194
195 Self::ensure_payee_or_operator(msg_sender, &call.descriptor)?;
196
197 let cumulative = call.cumulativeAmount;
198 if cumulative > state.deposit {
199 return Err(TIP20ChannelReserveError::amount_exceeds_deposit().into());
200 }
201 if cumulative <= state.settled {
202 return Err(TIP20ChannelReserveError::amount_not_increasing().into());
203 }
204
205 self.validate_voucher(
206 &call.descriptor,
207 channel_id,
208 call.cumulativeAmount,
209 &call.signature,
210 )?;
211
212 let delta = cumulative
213 .checked_sub(state.settled)
214 .expect("cumulative amount already checked to be increasing");
215
216 let mut token = TIP20Token::from_address(call.descriptor.token)?;
217 token.ensure_authorized_as(call.descriptor.payer, AuthRole::Sender)?;
218
219 state.settled = cumulative;
220 self.channel_states[channel_id].write(state)?;
221
222 token.transfer(
223 self.address,
224 ITIP20::transferCall {
225 to: call.descriptor.payee,
226 amount: U256::from(delta),
227 },
228 )?;
229
230 self.emit_event(TIP20ChannelReserveEvent::Settled(
231 ITIP20ChannelReserve::Settled {
232 channelId: channel_id,
233 payer: call.descriptor.payer,
234 payee: call.descriptor.payee,
235 cumulativeAmount: call.cumulativeAmount,
236 deltaPaid: delta,
237 newSettled: cumulative,
238 },
239 ))?;
240
241 Ok(())
242 }
243
244 pub fn top_up(
248 &mut self,
249 msg_sender: Address,
250 call: ITIP20ChannelReserve::topUpCall,
251 ) -> Result<()> {
252 let channel_id = self.channel_id(&call.descriptor)?;
253 let mut state = self.load_existing_state(channel_id)?;
254
255 if msg_sender != call.descriptor.payer {
256 return Err(TIP20ChannelReserveError::not_payer().into());
257 }
258
259 let additional = call.additionalDeposit;
260 let had_close_request = state.close_requested_at().is_some();
261
262 if additional.is_zero() && !had_close_request {
263 return Ok(());
264 }
265
266 if !additional.is_zero() {
267 let next_deposit = state
268 .deposit
269 .checked_add(additional)
270 .ok_or_else(TIP20ChannelReserveError::deposit_overflow)?;
271
272 state.deposit = next_deposit;
273 let mut token = TIP20Token::from_address(call.descriptor.token)?;
274 token.ensure_authorized_as(
275 Recipient::resolve(call.descriptor.payee)?.target,
276 AuthRole::Recipient,
277 )?;
278 token.system_transfer_from(
279 self.address,
280 msg_sender,
281 U256::from(call.additionalDeposit),
282 )?;
283 }
284 if had_close_request {
285 state.close_requested_at = 0;
286 }
287
288 self.channel_states[channel_id].write(state)?;
289 if had_close_request {
290 self.emit_event(TIP20ChannelReserveEvent::CloseRequestCancelled(
291 ITIP20ChannelReserve::CloseRequestCancelled {
292 channelId: channel_id,
293 payer: call.descriptor.payer,
294 payee: call.descriptor.payee,
295 },
296 ))?;
297 }
298 self.emit_event(TIP20ChannelReserveEvent::TopUp(
299 ITIP20ChannelReserve::TopUp {
300 channelId: channel_id,
301 payer: call.descriptor.payer,
302 payee: call.descriptor.payee,
303 additionalDeposit: call.additionalDeposit,
304 newDeposit: state.deposit,
305 },
306 ))?;
307
308 Ok(())
309 }
310
311 pub fn request_close(
315 &mut self,
316 msg_sender: Address,
317 call: ITIP20ChannelReserve::requestCloseCall,
318 ) -> Result<()> {
319 let channel_id = self.channel_id(&call.descriptor)?;
320 let mut state = self.load_existing_state(channel_id)?;
321
322 if msg_sender != call.descriptor.payer {
323 return Err(TIP20ChannelReserveError::not_payer().into());
324 }
325 if state.close_requested_at().is_some() {
326 return Ok(());
327 }
328
329 let close_requested_at = self.now_u32();
330 state.close_requested_at = close_requested_at;
331 self.channel_states[channel_id].write(state)?;
332 self.emit_event(TIP20ChannelReserveEvent::CloseRequested(
333 ITIP20ChannelReserve::CloseRequested {
334 channelId: channel_id,
335 payer: call.descriptor.payer,
336 payee: call.descriptor.payee,
337 closeGraceEnd: U256::from(self.now() + CLOSE_GRACE_PERIOD),
338 },
339 ))?;
340
341 Ok(())
342 }
343
344 pub fn close(
352 &mut self,
353 msg_sender: Address,
354 call: ITIP20ChannelReserve::closeCall,
355 ) -> Result<()> {
356 let channel_id = self.channel_id(&call.descriptor)?;
357 let state = self.load_existing_state(channel_id)?;
358
359 Self::ensure_payee_or_operator(msg_sender, &call.descriptor)?;
360
361 let cumulative = call.cumulativeAmount;
362 let capture = call.captureAmount;
363 let previous_settled = state.settled;
364 if capture < previous_settled || capture > cumulative {
365 return Err(TIP20ChannelReserveError::capture_amount_invalid().into());
366 }
367 if capture > state.deposit {
368 return Err(TIP20ChannelReserveError::amount_exceeds_deposit().into());
369 }
370
371 if capture > previous_settled {
372 self.validate_voucher(
373 &call.descriptor,
374 channel_id,
375 call.cumulativeAmount,
376 &call.signature,
377 )?;
378 }
379
380 let delta = capture
381 .checked_sub(previous_settled)
382 .expect("capture amount already checked against previous settled amount");
383 let refund = state
384 .deposit
385 .checked_sub(capture)
386 .expect("capture amount already checked against deposit");
387
388 self.channel_states[channel_id].delete()?;
389
390 let mut token = TIP20Token::from_address(call.descriptor.token)?;
391 if !delta.is_zero() {
392 token.ensure_authorized_as(call.descriptor.payer, AuthRole::Sender)?;
393 token.transfer(
394 self.address,
395 ITIP20::transferCall {
396 to: call.descriptor.payee,
397 amount: U256::from(delta),
398 },
399 )?;
400 }
401 if !refund.is_zero() {
402 token.transfer(
403 self.address,
404 ITIP20::transferCall {
405 to: call.descriptor.payer,
406 amount: U256::from(refund),
407 },
408 )?;
409 }
410
411 self.emit_event(TIP20ChannelReserveEvent::ChannelClosed(
412 ITIP20ChannelReserve::ChannelClosed {
413 channelId: channel_id,
414 payer: call.descriptor.payer,
415 payee: call.descriptor.payee,
416 settledToPayee: capture,
417 refundedToPayer: refund,
418 },
419 ))?;
420
421 Ok(())
422 }
423
424 pub fn withdraw(
426 &mut self,
427 msg_sender: Address,
428 call: ITIP20ChannelReserve::withdrawCall,
429 ) -> Result<()> {
430 let channel_id = self.channel_id(&call.descriptor)?;
431 let state = self.load_existing_state(channel_id)?;
432
433 if msg_sender != call.descriptor.payer {
434 return Err(TIP20ChannelReserveError::not_payer().into());
435 }
436
437 let close_ready = state
438 .close_requested_at()
439 .is_some_and(|requested_at| self.now() >= u64::from(requested_at) + CLOSE_GRACE_PERIOD);
440 if !close_ready {
441 return Err(TIP20ChannelReserveError::close_not_ready().into());
442 }
443
444 let refund = state
445 .deposit
446 .checked_sub(state.settled)
447 .expect("settled is always <= deposit");
448
449 self.channel_states[channel_id].delete()?;
450 if !refund.is_zero() {
451 TIP20Token::from_address(call.descriptor.token)?.transfer(
452 self.address,
453 ITIP20::transferCall {
454 to: call.descriptor.payer,
455 amount: U256::from(refund),
456 },
457 )?;
458 }
459 self.emit_event(TIP20ChannelReserveEvent::ChannelClosed(
460 ITIP20ChannelReserve::ChannelClosed {
461 channelId: channel_id,
462 payer: call.descriptor.payer,
463 payee: call.descriptor.payee,
464 settledToPayee: state.settled,
465 refundedToPayer: refund,
466 },
467 ))?;
468
469 Ok(())
470 }
471
472 pub fn get_channel(
474 &self,
475 call: ITIP20ChannelReserve::getChannelCall,
476 ) -> Result<ITIP20ChannelReserve::Channel> {
477 let channel_id = self.channel_id(&call.descriptor)?;
478 Ok(ITIP20ChannelReserve::Channel {
479 descriptor: call.descriptor,
480 state: self.channel_states[channel_id].read()?.to_sol(),
481 })
482 }
483
484 pub fn get_channel_state(
486 &self,
487 call: ITIP20ChannelReserve::getChannelStateCall,
488 ) -> Result<ITIP20ChannelReserve::ChannelState> {
489 Ok(self.channel_states[call.channelId].read()?.to_sol())
490 }
491
492 pub fn get_channel_states_batch(
494 &self,
495 call: ITIP20ChannelReserve::getChannelStatesBatchCall,
496 ) -> Result<Vec<ITIP20ChannelReserve::ChannelState>> {
497 call.channelIds
498 .into_iter()
499 .map(|channel_id| {
500 self.channel_states[channel_id]
501 .read()
502 .map(PackedChannelState::to_sol)
503 })
504 .collect()
505 }
506
507 pub fn compute_channel_id(
509 &self,
510 call: ITIP20ChannelReserve::computeChannelIdCall,
511 ) -> Result<B256> {
512 self.compute_channel_id_inner(
513 call.payer,
514 call.payee,
515 call.operator,
516 call.token,
517 call.salt,
518 call.authorizedSigner,
519 call.expiringNonceHash,
520 )
521 }
522
523 pub fn get_voucher_digest(
525 &self,
526 call: ITIP20ChannelReserve::getVoucherDigestCall,
527 ) -> Result<B256> {
528 self.get_voucher_digest_inner(call.channelId, call.cumulativeAmount)
529 }
530
531 pub fn domain_separator(&self) -> Result<B256> {
533 let hash = match self.storage.chain_id() {
534 MAINNET_CHAIN_ID => *DOMAIN_SEPARATOR_MAINNET,
535 MODERATO_CHAIN_ID => *DOMAIN_SEPARATOR_TESTNET,
536 chain_id => domain_separator_inner(chain_id),
537 };
538
539 Ok(hash)
540 }
541
542 fn now(&self) -> u64 {
544 self.storage.timestamp().saturating_to::<u64>()
545 }
546
547 fn now_u32(&self) -> u32 {
549 self.storage.timestamp().saturating_to::<u32>()
550 }
551
552 fn channel_id(&self, descriptor: &ITIP20ChannelReserve::ChannelDescriptor) -> Result<B256> {
554 self.compute_channel_id_inner(
555 descriptor.payer,
556 descriptor.payee,
557 descriptor.operator,
558 descriptor.token,
559 descriptor.salt,
560 descriptor.authorizedSigner,
561 descriptor.expiringNonceHash,
562 )
563 }
564
565 fn ensure_payee_or_operator(
567 msg_sender: Address,
568 descriptor: &ITIP20ChannelReserve::ChannelDescriptor,
569 ) -> Result<()> {
570 if msg_sender != descriptor.payee
571 && (descriptor.operator.is_zero() || msg_sender != descriptor.operator)
572 {
573 return Err(TIP20ChannelReserveError::not_payee_or_operator().into());
574 }
575 Ok(())
576 }
577
578 fn enclosing_channel_open_context_hash(&self) -> Result<B256> {
580 let hash = self.channel_open_context_hash.t_read()?;
581 if hash.is_zero() {
582 return Err(TIP20ChannelReserveError::expiring_nonce_hash_not_set().into());
583 }
584 Ok(hash)
585 }
586
587 #[expect(clippy::too_many_arguments)]
589 fn compute_channel_id_inner(
590 &self,
591 payer: Address,
592 payee: Address,
593 operator: Address,
594 token: Address,
595 salt: B256,
596 authorized_signer: Address,
597 expiring_nonce_hash: B256,
598 ) -> Result<B256> {
599 self.storage.keccak256(
600 &(
601 payer,
602 payee,
603 operator,
604 token,
605 salt,
606 authorized_signer,
607 expiring_nonce_hash,
608 self.address,
609 U256::from(self.storage.chain_id()),
610 )
611 .abi_encode(),
612 )
613 }
614
615 fn load_existing_state(&self, channel_id: B256) -> Result<PackedChannelState> {
617 let state = self.channel_states[channel_id].read()?;
618 if !state.exists() {
619 return Err(TIP20ChannelReserveError::channel_not_found().into());
620 }
621 Ok(state)
622 }
623
624 fn expected_signer(&self, descriptor: &ITIP20ChannelReserve::ChannelDescriptor) -> Address {
626 if descriptor.authorizedSigner.is_zero() {
627 descriptor.payer
628 } else {
629 descriptor.authorizedSigner
630 }
631 }
632
633 fn validate_voucher(
635 &self,
636 descriptor: &ITIP20ChannelReserve::ChannelDescriptor,
637 channel_id: B256,
638 cumulative_amount: U96,
639 signature: &alloy::primitives::Bytes,
640 ) -> Result<()> {
641 let digest = self.get_voucher_digest_inner(channel_id, cumulative_amount)?;
642 let signer = SignatureVerifier::new()
643 .recover(digest, signature.clone())
644 .map_err(|_| TIP20ChannelReserveError::invalid_signature())?;
645 if signer != self.expected_signer(descriptor) {
646 return Err(TIP20ChannelReserveError::invalid_signature().into());
647 }
648 Ok(())
649 }
650
651 fn get_voucher_digest_inner(&self, channel_id: B256, cumulative_amount: U96) -> Result<B256> {
653 let struct_hash = self
654 .storage
655 .keccak256(&(*VOUCHER_TYPEHASH, channel_id, cumulative_amount).abi_encode())?;
656 let domain_separator = self.domain_separator()?;
657
658 let mut digest_input = [0u8; 66];
659 digest_input[0] = 0x19;
660 digest_input[1] = 0x01;
661 digest_input[2..34].copy_from_slice(domain_separator.as_slice());
662 digest_input[34..66].copy_from_slice(struct_hash.as_slice());
663 self.storage.keccak256(&digest_input)
664 }
665}
666
667fn domain_separator_inner(chain_id: u64) -> B256 {
671 keccak256(
672 (
673 *EIP712_DOMAIN_TYPEHASH,
674 *NAME_HASH,
675 *VERSION_HASH,
676 U256::from(chain_id),
677 TIP20_CHANNEL_RESERVE_ADDRESS,
678 )
679 .abi_encode(),
680 )
681}
682
683#[cfg(test)]
684mod tests {
685 use super::*;
686 use crate::{
687 Precompile,
688 address_registry::AddressRegistry,
689 storage::{ContractStorage, StorageCtx, hashmap::HashMapStorageProvider},
690 test_util::{
691 TIP20Setup, VIRTUAL_MASTER, assert_full_coverage, check_selector_coverage,
692 register_virtual_master,
693 },
694 tip403_registry::{ITIP403Registry, TIP403Registry},
695 };
696 use alloy::{
697 primitives::{Bytes, Signature},
698 sol_types::SolCall,
699 };
700 use alloy_signer::SignerSync;
701 use alloy_signer_local::PrivateKeySigner;
702 use tempo_chainspec::hardfork::TempoHardfork;
703 use tempo_contracts::precompiles::{
704 ITIP20ChannelReserve::ITIP20ChannelReserveCalls, TIP20Error,
705 };
706
707 fn descriptor(
708 payer: Address,
709 payee: Address,
710 operator: Address,
711 token: Address,
712 salt: B256,
713 authorized_signer: Address,
714 expiring_nonce_hash: B256,
715 ) -> ITIP20ChannelReserve::ChannelDescriptor {
716 ITIP20ChannelReserve::ChannelDescriptor {
717 payer,
718 payee,
719 operator,
720 token,
721 salt,
722 authorizedSigner: authorized_signer,
723 expiringNonceHash: expiring_nonce_hash,
724 }
725 }
726
727 fn open_call(
728 payee: Address,
729 operator: Address,
730 token: Address,
731 deposit: u128,
732 salt: B256,
733 authorized_signer: Address,
734 ) -> ITIP20ChannelReserve::openCall {
735 ITIP20ChannelReserve::openCall {
736 payee,
737 operator,
738 token,
739 deposit: U96::from(deposit),
740 salt,
741 authorizedSigner: authorized_signer,
742 }
743 }
744
745 fn seed_expiring_nonce_hash(reserve: &mut TIP20ChannelReserve) -> Result<B256> {
746 let hash = B256::random();
747 reserve.set_channel_open_context_hash(hash)?;
748 Ok(hash)
749 }
750
751 fn install_blacklist_policy(
752 token: &mut TIP20Token,
753 admin: Address,
754 ) -> Result<(TIP403Registry, u64, u64)> {
755 let mut registry = TIP403Registry::new();
756 registry.initialize()?;
757 let blacklist = |registry: &mut TIP403Registry| {
758 registry.create_policy(
759 admin,
760 ITIP403Registry::createPolicyCall {
761 admin,
762 policyType: ITIP403Registry::PolicyType::BLACKLIST,
763 },
764 )
765 };
766 let sender_policy = blacklist(&mut registry)?;
767 let recipient_policy = blacklist(&mut registry)?;
768 let compound_policy = registry.create_compound_policy(
769 admin,
770 ITIP403Registry::createCompoundPolicyCall {
771 senderPolicyId: sender_policy,
772 recipientPolicyId: recipient_policy,
773 mintRecipientPolicyId: 1,
774 },
775 )?;
776 token.change_transfer_policy_id(
777 admin,
778 ITIP20::changeTransferPolicyIdCall {
779 newPolicyId: compound_policy,
780 },
781 )?;
782 Ok((registry, sender_policy, recipient_policy))
783 }
784
785 fn set_blacklisted(
786 registry: &mut TIP403Registry,
787 admin: Address,
788 policy_id: u64,
789 account: Address,
790 restricted: bool,
791 ) -> Result<()> {
792 registry.modify_policy_blacklist(
793 admin,
794 ITIP403Registry::modifyPolicyBlacklistCall {
795 policyId: policy_id,
796 account,
797 restricted,
798 },
799 )
800 }
801
802 #[test]
803 fn test_selector_coverage() -> eyre::Result<()> {
804 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
805 StorageCtx::enter(&mut storage, || {
806 let mut reserve = TIP20ChannelReserve::new();
807 let unsupported = check_selector_coverage(
808 &mut reserve,
809 ITIP20ChannelReserveCalls::SELECTORS,
810 "ITIP20ChannelReserve",
811 ITIP20ChannelReserveCalls::name_by_selector,
812 );
813 assert_full_coverage([unsupported]);
814 Ok(())
815 })
816 }
817
818 #[test]
819 fn test_open_requires_expiring_nonce_hash() -> eyre::Result<()> {
820 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
821 let payer = Address::random();
822 let payee = Address::random();
823
824 StorageCtx::enter(&mut storage, || {
825 let token = TIP20Setup::path_usd(payer)
826 .with_issuer(payer)
827 .with_mint(payer, U256::from(100u128))
828 .apply()?;
829 let mut reserve = TIP20ChannelReserve::new();
830 reserve.initialize()?;
831
832 let result = reserve.open(
833 payer,
834 open_call(
835 payee,
836 Address::ZERO,
837 token.address(),
838 1,
839 B256::random(),
840 Address::ZERO,
841 ),
842 );
843 assert_eq!(
844 result.unwrap_err(),
845 TIP20ChannelReserveError::expiring_nonce_hash_not_set().into()
846 );
847 Ok(())
848 })
849 }
850
851 #[test]
852 fn test_open_rejects_invalid_payees() -> eyre::Result<()> {
853 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
854 let payer = Address::random();
855
856 StorageCtx::enter(&mut storage, || {
857 let token = TIP20Setup::path_usd(payer)
858 .with_issuer(payer)
859 .with_mint(payer, U256::from(100u128))
860 .apply()?;
861 let (_, virtual_payee) = register_virtual_master(&mut AddressRegistry::new())?;
862 let mut reserve = TIP20ChannelReserve::new();
863 reserve.initialize()?;
864 seed_expiring_nonce_hash(&mut reserve)?;
865
866 for invalid_payee in &[token.address(), virtual_payee] {
867 for invalid_operator_for_virtual_payee in &[Address::ZERO, virtual_payee] {
868 let result = reserve.open(
869 payer,
870 open_call(
871 *invalid_payee,
872 *invalid_operator_for_virtual_payee,
873 token.address(),
874 1,
875 B256::random(),
876 Address::ZERO,
877 ),
878 );
879 assert_eq!(
880 result.unwrap_err(),
881 TIP20ChannelReserveError::invalid_payee().into()
882 );
883 }
884 }
885
886 reserve.open(
888 payer,
889 open_call(
890 virtual_payee,
891 Address::random(),
892 token.address(),
893 1,
894 B256::random(),
895 Address::ZERO,
896 ),
897 )?;
898 Ok(())
899 })
900 }
901
902 #[test]
903 fn test_virtual_payee_admission_checks_resolved_master() -> eyre::Result<()> {
904 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
905 let payer = Address::random();
906 let admin = payer;
907 let operator = Address::random();
908
909 StorageCtx::enter(&mut storage, || {
910 let mut token = TIP20Setup::path_usd(admin)
911 .with_issuer(admin)
912 .with_mint(payer, U256::from(1_000u128))
913 .apply()?;
914 let (mut registry, _sender_policy, recipient_policy) =
915 install_blacklist_policy(&mut token, admin)?;
916 let (_, virtual_payee) = register_virtual_master(&mut AddressRegistry::new())?;
917 let mut reserve = TIP20ChannelReserve::new();
918 reserve.initialize()?;
919
920 set_blacklisted(&mut registry, admin, recipient_policy, VIRTUAL_MASTER, true)?;
922 seed_expiring_nonce_hash(&mut reserve)?;
923 let res = reserve.open(
924 payer,
925 open_call(
926 virtual_payee,
927 operator,
928 token.address(),
929 100,
930 B256::random(),
931 Address::ZERO,
932 ),
933 );
934 assert_eq!(res.unwrap_err(), TIP20Error::policy_forbids().into());
935
936 set_blacklisted(
938 &mut registry,
939 admin,
940 recipient_policy,
941 VIRTUAL_MASTER,
942 false,
943 )?;
944 let salt = B256::random();
945 let expiring_nonce_hash = seed_expiring_nonce_hash(&mut reserve)?;
946 reserve.open(
947 payer,
948 open_call(
949 virtual_payee,
950 operator,
951 token.address(),
952 100,
953 salt,
954 Address::ZERO,
955 ),
956 )?;
957 let descriptor = descriptor(
958 payer,
959 virtual_payee,
960 operator,
961 token.address(),
962 salt,
963 Address::ZERO,
964 expiring_nonce_hash,
965 );
966
967 set_blacklisted(&mut registry, admin, recipient_policy, VIRTUAL_MASTER, true)?;
968 let res = reserve.top_up(
969 payer,
970 ITIP20ChannelReserve::topUpCall {
971 descriptor,
972 additionalDeposit: U96::from(1),
973 },
974 );
975 assert_eq!(res.unwrap_err(), TIP20Error::policy_forbids().into());
976 Ok(())
977 })
978 }
979
980 #[test]
981 fn test_tip403_logical_payer_payee_policy_checks() -> eyre::Result<()> {
982 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
983 let payer_signer = PrivateKeySigner::random();
984 let payer = payer_signer.address();
985 let payee = Address::random();
986 let admin = payer;
987
988 StorageCtx::enter(&mut storage, || {
989 let mut token = TIP20Setup::path_usd(admin)
990 .with_issuer(admin)
991 .with_mint(payer, U256::from(1_000u128))
992 .apply()?;
993 let (mut registry, sender_policy, recipient_policy) =
994 install_blacklist_policy(&mut token, admin)?;
995 let mut reserve = TIP20ChannelReserve::new();
996 reserve.initialize()?;
997
998 set_blacklisted(&mut registry, admin, recipient_policy, payee, true)?;
1000 seed_expiring_nonce_hash(&mut reserve)?;
1001 let res = reserve.open(
1002 payer,
1003 open_call(
1004 payee,
1005 Address::ZERO,
1006 token.address(),
1007 100,
1008 B256::random(),
1009 Address::ZERO,
1010 ),
1011 );
1012 assert_eq!(res.unwrap_err(), TIP20Error::policy_forbids().into());
1013
1014 set_blacklisted(&mut registry, admin, recipient_policy, payee, false)?;
1016 let salt = B256::random();
1017 let expiring_nonce_hash = seed_expiring_nonce_hash(&mut reserve)?;
1018 let channel_id = reserve.open(
1019 payer,
1020 open_call(
1021 payee,
1022 Address::ZERO,
1023 token.address(),
1024 100,
1025 salt,
1026 Address::ZERO,
1027 ),
1028 )?;
1029 let descriptor = descriptor(
1030 payer,
1031 payee,
1032 Address::ZERO,
1033 token.address(),
1034 salt,
1035 Address::ZERO,
1036 expiring_nonce_hash,
1037 );
1038
1039 set_blacklisted(&mut registry, admin, recipient_policy, payee, true)?;
1041 let res = reserve.top_up(
1042 payer,
1043 ITIP20ChannelReserve::topUpCall {
1044 descriptor: descriptor.clone(),
1045 additionalDeposit: U96::from(1),
1046 },
1047 );
1048 assert_eq!(res.unwrap_err(), TIP20Error::policy_forbids().into());
1049
1050 set_blacklisted(&mut registry, admin, recipient_policy, payee, false)?;
1052 set_blacklisted(&mut registry, admin, sender_policy, payer, true)?;
1053 let digest =
1054 reserve.get_voucher_digest(ITIP20ChannelReserve::getVoucherDigestCall {
1055 channelId: channel_id,
1056 cumulativeAmount: U96::from(10),
1057 })?;
1058 let signature =
1059 Bytes::copy_from_slice(&payer_signer.sign_hash_sync(&digest)?.as_bytes());
1060
1061 let res = reserve.settle(
1063 payee,
1064 ITIP20ChannelReserve::settleCall {
1065 descriptor: descriptor.clone(),
1066 cumulativeAmount: U96::from(10),
1067 signature: signature.clone(),
1068 },
1069 );
1070 assert_eq!(res.unwrap_err(), TIP20Error::policy_forbids().into());
1071
1072 let res = reserve.close(
1074 payee,
1075 ITIP20ChannelReserve::closeCall {
1076 descriptor,
1077 cumulativeAmount: U96::from(10),
1078 captureAmount: U96::from(10),
1079 signature,
1080 },
1081 );
1082 assert_eq!(res.unwrap_err(), TIP20Error::policy_forbids().into());
1083
1084 Ok(())
1085 })
1086 }
1087
1088 #[test]
1089 fn test_open_settle_close_flow_deletes_state_and_same_tx_reopen_guard() -> eyre::Result<()> {
1090 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
1091 let payer_signer = PrivateKeySigner::random();
1092 let payer = payer_signer.address();
1093 let payee = Address::random();
1094 let salt = B256::random();
1095
1096 StorageCtx::enter(&mut storage, || {
1097 let token = TIP20Setup::path_usd(payer)
1098 .with_issuer(payer)
1099 .with_mint(payer, U256::from(1_000u128))
1100 .apply()?;
1101
1102 let mut reserve = TIP20ChannelReserve::new();
1103 reserve.initialize()?;
1104 let expiring_nonce_hash = seed_expiring_nonce_hash(&mut reserve)?;
1105
1106 let channel_id = reserve.open(
1107 payer,
1108 open_call(
1109 payee,
1110 Address::ZERO,
1111 token.address(),
1112 300,
1113 salt,
1114 Address::ZERO,
1115 ),
1116 )?;
1117
1118 let digest =
1119 reserve.get_voucher_digest(ITIP20ChannelReserve::getVoucherDigestCall {
1120 channelId: channel_id,
1121 cumulativeAmount: U96::from(120),
1122 })?;
1123 let signature =
1124 Bytes::copy_from_slice(&payer_signer.sign_hash_sync(&digest)?.as_bytes());
1125
1126 let channel_descriptor = descriptor(
1127 payer,
1128 payee,
1129 Address::ZERO,
1130 token.address(),
1131 salt,
1132 Address::ZERO,
1133 expiring_nonce_hash,
1134 );
1135 reserve.settle(
1136 payee,
1137 ITIP20ChannelReserve::settleCall {
1138 descriptor: channel_descriptor.clone(),
1139 cumulativeAmount: U96::from(120),
1140 signature,
1141 },
1142 )?;
1143
1144 let close_digest =
1145 reserve.get_voucher_digest(ITIP20ChannelReserve::getVoucherDigestCall {
1146 channelId: channel_id,
1147 cumulativeAmount: U96::from(500),
1148 })?;
1149 let close_signature =
1150 Bytes::copy_from_slice(&payer_signer.sign_hash_sync(&close_digest)?.as_bytes());
1151 reserve.close(
1152 payee,
1153 ITIP20ChannelReserve::closeCall {
1154 descriptor: channel_descriptor,
1155 cumulativeAmount: U96::from(500),
1156 captureAmount: U96::from(200),
1157 signature: close_signature,
1158 },
1159 )?;
1160
1161 let state = reserve.get_channel_state(ITIP20ChannelReserve::getChannelStateCall {
1162 channelId: channel_id,
1163 })?;
1164 assert!(state.deposit.is_zero());
1165 assert!(state.settled.is_zero());
1166 assert_eq!(state.closeRequestedAt, 0);
1167
1168 let reopen_result = reserve.open(
1169 payer,
1170 open_call(
1171 payee,
1172 Address::ZERO,
1173 token.address(),
1174 1,
1175 salt,
1176 Address::ZERO,
1177 ),
1178 );
1179 assert_eq!(
1180 reopen_result.unwrap_err(),
1181 TIP20ChannelReserveError::channel_already_exists().into()
1182 );
1183
1184 let new_expiring_nonce_hash = seed_expiring_nonce_hash(&mut reserve)?;
1185 let reopened_channel_id = reserve.open(
1186 payer,
1187 open_call(
1188 payee,
1189 Address::ZERO,
1190 token.address(),
1191 1,
1192 salt,
1193 Address::ZERO,
1194 ),
1195 )?;
1196 assert_ne!(channel_id, reopened_channel_id);
1197 assert_ne!(expiring_nonce_hash, new_expiring_nonce_hash);
1198
1199 let reopened_state =
1200 reserve.get_channel_state(ITIP20ChannelReserve::getChannelStateCall {
1201 channelId: reopened_channel_id,
1202 })?;
1203 assert_eq!(reopened_state.deposit, U96::from(1));
1204
1205 Ok(())
1206 })
1207 }
1208
1209 #[test]
1210 fn test_expiring_nonce_hash_and_operator_participate_in_channel_id() -> eyre::Result<()> {
1211 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
1212 let payer = Address::random();
1213 let payee = Address::random();
1214 let operator = Address::random();
1215 let salt = B256::random();
1216
1217 StorageCtx::enter(&mut storage, || {
1218 let token = TIP20Setup::path_usd(payer)
1219 .with_issuer(payer)
1220 .with_mint(payer, U256::from(100u128))
1221 .apply()?;
1222 let reserve = TIP20ChannelReserve::new();
1223
1224 let hash_a = B256::random();
1225 let hash_b = B256::random();
1226 let without_operator =
1227 reserve.compute_channel_id(ITIP20ChannelReserve::computeChannelIdCall {
1228 payer,
1229 payee,
1230 operator: Address::ZERO,
1231 token: token.address(),
1232 salt,
1233 authorizedSigner: Address::ZERO,
1234 expiringNonceHash: hash_a,
1235 })?;
1236 let with_operator =
1237 reserve.compute_channel_id(ITIP20ChannelReserve::computeChannelIdCall {
1238 payer,
1239 payee,
1240 operator,
1241 token: token.address(),
1242 salt,
1243 authorizedSigner: Address::ZERO,
1244 expiringNonceHash: hash_a,
1245 })?;
1246 let with_other_hash =
1247 reserve.compute_channel_id(ITIP20ChannelReserve::computeChannelIdCall {
1248 payer,
1249 payee,
1250 operator: Address::ZERO,
1251 token: token.address(),
1252 salt,
1253 authorizedSigner: Address::ZERO,
1254 expiringNonceHash: hash_b,
1255 })?;
1256
1257 assert_ne!(without_operator, with_operator);
1258 assert_ne!(without_operator, with_other_hash);
1259 Ok(())
1260 })
1261 }
1262
1263 #[test]
1264 fn test_multiple_opens_same_transaction() -> eyre::Result<()> {
1265 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
1266 let payer = Address::random();
1267 let payee = Address::random();
1268 let salt = B256::random();
1269
1270 StorageCtx::enter(&mut storage, || {
1271 let token = TIP20Setup::path_usd(payer)
1272 .with_issuer(payer)
1273 .with_mint(payer, U256::from(100u128))
1274 .apply()?;
1275 let mut reserve = TIP20ChannelReserve::new();
1276 reserve.initialize()?;
1277
1278 let hash = seed_expiring_nonce_hash(&mut reserve)?;
1279 let first = reserve.open(
1280 payer,
1281 open_call(
1282 payee,
1283 Address::ZERO,
1284 token.address(),
1285 10,
1286 salt,
1287 Address::ZERO,
1288 ),
1289 )?;
1290 let second = reserve.open(
1291 payer,
1292 open_call(
1293 payee,
1294 Address::ZERO,
1295 token.address(),
1296 10,
1297 B256::random(),
1298 Address::ZERO,
1299 ),
1300 )?;
1301 assert_ne!(first, second);
1302
1303 let other_hash = seed_expiring_nonce_hash(&mut reserve)?;
1304 let same_descriptor_other_tx_hash = reserve.open(
1305 payer,
1306 open_call(
1307 payee,
1308 Address::ZERO,
1309 token.address(),
1310 10,
1311 salt,
1312 Address::ZERO,
1313 ),
1314 )?;
1315 assert_ne!(first, same_descriptor_other_tx_hash);
1316 assert_ne!(hash, other_hash);
1317
1318 Ok(())
1319 })
1320 }
1321
1322 #[test]
1323 fn test_settle_allows_operator_and_rejects_unrelated_sender() -> eyre::Result<()> {
1324 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
1325 let payer_signer = PrivateKeySigner::random();
1326 let payer = payer_signer.address();
1327 let payee = Address::random();
1328 let operator = Address::random();
1329
1330 StorageCtx::enter(&mut storage, || {
1331 let token = TIP20Setup::path_usd(payer)
1332 .with_issuer(payer)
1333 .with_mint(payer, U256::from(200u128))
1334 .apply()?;
1335 let mut reserve = TIP20ChannelReserve::new();
1336 reserve.initialize()?;
1337
1338 let salt = B256::random();
1339 let expiring_nonce_hash = seed_expiring_nonce_hash(&mut reserve)?;
1340 let channel_id = reserve.open(
1341 payer,
1342 open_call(payee, operator, token.address(), 100, salt, Address::ZERO),
1343 )?;
1344 let channel_descriptor = descriptor(
1345 payer,
1346 payee,
1347 operator,
1348 token.address(),
1349 salt,
1350 Address::ZERO,
1351 expiring_nonce_hash,
1352 );
1353 let digest =
1354 reserve.get_voucher_digest(ITIP20ChannelReserve::getVoucherDigestCall {
1355 channelId: channel_id,
1356 cumulativeAmount: U96::from(40),
1357 })?;
1358 let signature =
1359 Bytes::copy_from_slice(&payer_signer.sign_hash_sync(&digest)?.as_bytes());
1360
1361 reserve.settle(
1362 operator,
1363 ITIP20ChannelReserve::settleCall {
1364 descriptor: channel_descriptor,
1365 cumulativeAmount: U96::from(40),
1366 signature,
1367 },
1368 )?;
1369 let state = reserve.get_channel_state(ITIP20ChannelReserve::getChannelStateCall {
1370 channelId: channel_id,
1371 })?;
1372 assert_eq!(state.settled, U96::from(40));
1373
1374 let salt = B256::random();
1375 let expiring_nonce_hash = seed_expiring_nonce_hash(&mut reserve)?;
1376 reserve.open(
1377 payer,
1378 open_call(
1379 payee,
1380 Address::ZERO,
1381 token.address(),
1382 10,
1383 salt,
1384 Address::ZERO,
1385 ),
1386 )?;
1387 let descriptor_without_operator = descriptor(
1388 payer,
1389 payee,
1390 Address::ZERO,
1391 token.address(),
1392 salt,
1393 Address::ZERO,
1394 expiring_nonce_hash,
1395 );
1396 let result = reserve.settle(
1397 Address::random(),
1398 ITIP20ChannelReserve::settleCall {
1399 descriptor: descriptor_without_operator,
1400 cumulativeAmount: U96::from(1),
1401 signature: Bytes::copy_from_slice(&Signature::test_signature().as_bytes()),
1402 },
1403 );
1404 assert_eq!(
1405 result.unwrap_err(),
1406 TIP20ChannelReserveError::not_payee_or_operator().into()
1407 );
1408
1409 Ok(())
1410 })
1411 }
1412
1413 #[test]
1414 fn test_close_allows_operator_and_rejects_unrelated_sender() -> eyre::Result<()> {
1415 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
1416 let payer_signer = PrivateKeySigner::random();
1417 let payer = payer_signer.address();
1418 let payee = Address::random();
1419 let operator = Address::random();
1420
1421 StorageCtx::enter(&mut storage, || {
1422 let token = TIP20Setup::path_usd(payer)
1423 .with_issuer(payer)
1424 .with_mint(payer, U256::from(300u128))
1425 .apply()?;
1426 let mut reserve = TIP20ChannelReserve::new();
1427 reserve.initialize()?;
1428
1429 let salt = B256::random();
1430 let expiring_nonce_hash = seed_expiring_nonce_hash(&mut reserve)?;
1431 let channel_id = reserve.open(
1432 payer,
1433 open_call(payee, operator, token.address(), 100, salt, Address::ZERO),
1434 )?;
1435 let channel_descriptor = descriptor(
1436 payer,
1437 payee,
1438 operator,
1439 token.address(),
1440 salt,
1441 Address::ZERO,
1442 expiring_nonce_hash,
1443 );
1444 let digest =
1445 reserve.get_voucher_digest(ITIP20ChannelReserve::getVoucherDigestCall {
1446 channelId: channel_id,
1447 cumulativeAmount: U96::from(80),
1448 })?;
1449 let signature =
1450 Bytes::copy_from_slice(&payer_signer.sign_hash_sync(&digest)?.as_bytes());
1451
1452 reserve.close(
1453 operator,
1454 ITIP20ChannelReserve::closeCall {
1455 descriptor: channel_descriptor,
1456 cumulativeAmount: U96::from(80),
1457 captureAmount: U96::from(40),
1458 signature,
1459 },
1460 )?;
1461 let state = reserve.get_channel_state(ITIP20ChannelReserve::getChannelStateCall {
1462 channelId: channel_id,
1463 })?;
1464 assert!(state.deposit.is_zero());
1465 assert!(state.settled.is_zero());
1466 assert_eq!(state.closeRequestedAt, 0);
1467
1468 let salt = B256::random();
1469 let expiring_nonce_hash = seed_expiring_nonce_hash(&mut reserve)?;
1470 reserve.open(
1471 payer,
1472 open_call(
1473 payee,
1474 Address::ZERO,
1475 token.address(),
1476 10,
1477 salt,
1478 Address::ZERO,
1479 ),
1480 )?;
1481 let descriptor_without_operator = descriptor(
1482 payer,
1483 payee,
1484 Address::ZERO,
1485 token.address(),
1486 salt,
1487 Address::ZERO,
1488 expiring_nonce_hash,
1489 );
1490 let result = reserve.close(
1491 Address::random(),
1492 ITIP20ChannelReserve::closeCall {
1493 descriptor: descriptor_without_operator,
1494 cumulativeAmount: U96::from(1),
1495 captureAmount: U96::from(1),
1496 signature: Bytes::copy_from_slice(&Signature::test_signature().as_bytes()),
1497 },
1498 );
1499 assert_eq!(
1500 result.unwrap_err(),
1501 TIP20ChannelReserveError::not_payee_or_operator().into()
1502 );
1503
1504 Ok(())
1505 })
1506 }
1507
1508 #[test]
1509 fn test_zero_top_up_without_close_request_is_noop() -> eyre::Result<()> {
1510 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
1511 let payer = Address::random();
1512 let payee = Address::random();
1513 let salt = B256::random();
1514
1515 StorageCtx::enter(&mut storage, || {
1516 let token = TIP20Setup::path_usd(payer)
1517 .with_issuer(payer)
1518 .with_mint(payer, U256::from(1_000u128))
1519 .apply()?;
1520 let mut reserve = TIP20ChannelReserve::new();
1521 reserve.initialize()?;
1522
1523 let expiring_nonce_hash = seed_expiring_nonce_hash(&mut reserve)?;
1524 let descriptor = descriptor(
1525 payer,
1526 payee,
1527 Address::ZERO,
1528 token.address(),
1529 salt,
1530 Address::ZERO,
1531 expiring_nonce_hash,
1532 );
1533 reserve.open(
1534 payer,
1535 open_call(
1536 payee,
1537 Address::ZERO,
1538 token.address(),
1539 100,
1540 salt,
1541 Address::ZERO,
1542 ),
1543 )?;
1544 reserve.clear_emitted_events();
1545
1546 reserve.top_up(
1547 payer,
1548 ITIP20ChannelReserve::topUpCall {
1549 descriptor: descriptor.clone(),
1550 additionalDeposit: U96::ZERO,
1551 },
1552 )?;
1553
1554 let channel =
1555 reserve.get_channel(ITIP20ChannelReserve::getChannelCall { descriptor })?;
1556 assert_eq!(channel.state.closeRequestedAt, 0);
1557 assert_eq!(channel.state.deposit, 100);
1558 assert!(
1559 StorageCtx
1560 .get_events(TIP20_CHANNEL_RESERVE_ADDRESS)
1561 .is_empty()
1562 );
1563
1564 Ok(())
1565 })
1566 }
1567
1568 #[test]
1569 fn test_top_up_cancels_close_request() -> eyre::Result<()> {
1570 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
1571 let payer = Address::random();
1572 let payee = Address::random();
1573 let salt = B256::random();
1574
1575 StorageCtx::enter(&mut storage, || {
1576 let token = TIP20Setup::path_usd(payer)
1577 .with_issuer(payer)
1578 .with_mint(payer, U256::from(1_000u128))
1579 .apply()?;
1580 let mut reserve = TIP20ChannelReserve::new();
1581 reserve.initialize()?;
1582
1583 let expiring_nonce_hash = seed_expiring_nonce_hash(&mut reserve)?;
1584 let descriptor = descriptor(
1585 payer,
1586 payee,
1587 Address::ZERO,
1588 token.address(),
1589 salt,
1590 Address::ZERO,
1591 expiring_nonce_hash,
1592 );
1593 reserve.open(
1594 payer,
1595 open_call(
1596 payee,
1597 Address::ZERO,
1598 token.address(),
1599 100,
1600 salt,
1601 Address::ZERO,
1602 ),
1603 )?;
1604
1605 reserve.storage.set_timestamp(U256::from(1_000u64));
1606 reserve.request_close(
1607 payer,
1608 ITIP20ChannelReserve::requestCloseCall {
1609 descriptor: descriptor.clone(),
1610 },
1611 )?;
1612 let requested = reserve.get_channel(ITIP20ChannelReserve::getChannelCall {
1613 descriptor: descriptor.clone(),
1614 })?;
1615 assert_eq!(requested.state.closeRequestedAt, 1_000);
1616
1617 reserve.top_up(
1618 payer,
1619 ITIP20ChannelReserve::topUpCall {
1620 descriptor: descriptor.clone(),
1621 additionalDeposit: U96::from(25),
1622 },
1623 )?;
1624
1625 let channel =
1626 reserve.get_channel(ITIP20ChannelReserve::getChannelCall { descriptor })?;
1627 assert_eq!(channel.state.closeRequestedAt, 0);
1628 assert_eq!(channel.state.deposit, 125);
1629
1630 Ok(())
1631 })
1632 }
1633
1634 #[test]
1635 fn test_dispatch_rejects_static_mutation() -> eyre::Result<()> {
1636 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
1637 StorageCtx::enter(&mut storage, || {
1638 let mut reserve = TIP20ChannelReserve::new();
1639 let result = reserve.call(
1640 &ITIP20ChannelReserve::openCall {
1641 payee: Address::random(),
1642 operator: Address::ZERO,
1643 token: TIP20_CHANNEL_RESERVE_ADDRESS,
1644 deposit: U96::from(1),
1645 salt: B256::ZERO,
1646 authorizedSigner: Address::ZERO,
1647 }
1648 .abi_encode(),
1649 Address::ZERO,
1650 );
1651 assert!(result.is_ok());
1652 Ok(())
1653 })
1654 }
1655
1656 #[test]
1657 fn test_settle_rejects_invalid_signature() -> eyre::Result<()> {
1658 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
1659 let payer = Address::random();
1660 let payee = Address::random();
1661 let salt = B256::random();
1662
1663 StorageCtx::enter(&mut storage, || {
1664 let token = TIP20Setup::path_usd(payer)
1665 .with_issuer(payer)
1666 .with_mint(payer, U256::from(100u128))
1667 .apply()?;
1668 let mut reserve = TIP20ChannelReserve::new();
1669 reserve.initialize()?;
1670 let expiring_nonce_hash = seed_expiring_nonce_hash(&mut reserve)?;
1671 reserve.open(
1672 payer,
1673 open_call(
1674 payee,
1675 Address::ZERO,
1676 token.address(),
1677 100,
1678 salt,
1679 Address::ZERO,
1680 ),
1681 )?;
1682
1683 let result = reserve.settle(
1684 payee,
1685 ITIP20ChannelReserve::settleCall {
1686 descriptor: descriptor(
1687 payer,
1688 payee,
1689 Address::ZERO,
1690 token.address(),
1691 salt,
1692 Address::ZERO,
1693 expiring_nonce_hash,
1694 ),
1695 cumulativeAmount: U96::from(10),
1696 signature: Bytes::copy_from_slice(
1697 &Signature::test_signature().as_bytes()[..64],
1698 ),
1699 },
1700 );
1701 assert_eq!(
1702 result.unwrap_err(),
1703 TIP20ChannelReserveError::invalid_signature().into()
1704 );
1705 Ok(())
1706 })
1707 }
1708
1709 #[test]
1710 fn test_settle_rejects_keychain_signature_wrapper() -> eyre::Result<()> {
1711 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
1712 let payer = Address::random();
1713 let payee = Address::random();
1714 let salt = B256::random();
1715
1716 StorageCtx::enter(&mut storage, || {
1717 let token = TIP20Setup::path_usd(payer)
1718 .with_issuer(payer)
1719 .with_mint(payer, U256::from(100u128))
1720 .apply()?;
1721 let mut reserve = TIP20ChannelReserve::new();
1722 reserve.initialize()?;
1723 let expiring_nonce_hash = seed_expiring_nonce_hash(&mut reserve)?;
1724 reserve.open(
1725 payer,
1726 open_call(
1727 payee,
1728 Address::ZERO,
1729 token.address(),
1730 100,
1731 salt,
1732 Address::ZERO,
1733 ),
1734 )?;
1735
1736 let mut keychain_signature = Vec::new();
1737 keychain_signature.push(0x03);
1738 keychain_signature.extend_from_slice(Address::random().as_slice());
1739 keychain_signature.extend_from_slice(Signature::test_signature().as_bytes().as_slice());
1740
1741 let result = reserve.settle(
1742 payee,
1743 ITIP20ChannelReserve::settleCall {
1744 descriptor: descriptor(
1745 payer,
1746 payee,
1747 Address::ZERO,
1748 token.address(),
1749 salt,
1750 Address::ZERO,
1751 expiring_nonce_hash,
1752 ),
1753 cumulativeAmount: U96::from(10),
1754 signature: keychain_signature.into(),
1755 },
1756 );
1757 assert_eq!(
1758 result.unwrap_err(),
1759 TIP20ChannelReserveError::invalid_signature().into()
1760 );
1761 Ok(())
1762 })
1763 }
1764
1765 #[test]
1766 fn test_withdraw_after_grace_deletes_state() -> eyre::Result<()> {
1767 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
1768 let payer = Address::random();
1769 let payee = Address::random();
1770 let salt = B256::random();
1771
1772 StorageCtx::enter(&mut storage, || {
1773 let token = TIP20Setup::path_usd(payer)
1774 .with_issuer(payer)
1775 .with_mint(payer, U256::from(100u128))
1776 .apply()?;
1777 let mut reserve = TIP20ChannelReserve::new();
1778 reserve.initialize()?;
1779 let expiring_nonce_hash = seed_expiring_nonce_hash(&mut reserve)?;
1780 let channel_id = reserve.open(
1781 payer,
1782 open_call(
1783 payee,
1784 Address::ZERO,
1785 token.address(),
1786 100,
1787 salt,
1788 Address::ZERO,
1789 ),
1790 )?;
1791 let descriptor = descriptor(
1792 payer,
1793 payee,
1794 Address::ZERO,
1795 token.address(),
1796 salt,
1797 Address::ZERO,
1798 expiring_nonce_hash,
1799 );
1800
1801 reserve.storage.set_timestamp(U256::from(1_000u64));
1802 reserve.request_close(
1803 payer,
1804 ITIP20ChannelReserve::requestCloseCall {
1805 descriptor: descriptor.clone(),
1806 },
1807 )?;
1808 reserve
1809 .storage
1810 .set_timestamp(U256::from(1_000u64 + CLOSE_GRACE_PERIOD));
1811 reserve.withdraw(payer, ITIP20ChannelReserve::withdrawCall { descriptor })?;
1812
1813 let state = reserve.get_channel_state(ITIP20ChannelReserve::getChannelStateCall {
1814 channelId: channel_id,
1815 })?;
1816 assert!(state.deposit.is_zero());
1817 assert!(state.settled.is_zero());
1818 assert_eq!(state.closeRequestedAt, 0);
1819
1820 Ok(())
1821 })
1822 }
1823
1824 #[test]
1825 fn test_withdraw_requires_close_request() -> eyre::Result<()> {
1826 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
1827 let payer = Address::random();
1828 let payee = Address::random();
1829 let salt = B256::random();
1830
1831 StorageCtx::enter(&mut storage, || {
1832 let token = TIP20Setup::path_usd(payer)
1833 .with_issuer(payer)
1834 .with_mint(payer, U256::from(100u128))
1835 .apply()?;
1836 let mut reserve = TIP20ChannelReserve::new();
1837 reserve.initialize()?;
1838 let expiring_nonce_hash = seed_expiring_nonce_hash(&mut reserve)?;
1839 let descriptor = descriptor(
1840 payer,
1841 payee,
1842 Address::ZERO,
1843 token.address(),
1844 salt,
1845 Address::ZERO,
1846 expiring_nonce_hash,
1847 );
1848 reserve.open(
1849 payer,
1850 open_call(
1851 payee,
1852 Address::ZERO,
1853 token.address(),
1854 100,
1855 salt,
1856 Address::ZERO,
1857 ),
1858 )?;
1859
1860 let result = reserve.withdraw(payer, ITIP20ChannelReserve::withdrawCall { descriptor });
1861 assert_eq!(
1862 result.unwrap_err(),
1863 TIP20ChannelReserveError::close_not_ready().into()
1864 );
1865 Ok(())
1866 })
1867 }
1868}