Skip to main content

tempo_precompiles/tip20_channel_reserve/
mod.rs

1//! TIP-1034 TIP-20 channel reserve precompile.
2//!
3//! Channels lock TIP-20 deposits from a payer and let the payee claim signed
4//! cumulative vouchers. A channel is identified by its descriptor, the current
5//! chain, this precompile address, and a transaction-derived nonce hash that
6//! prevents accidental replay of `open` calls across transactions.
7
8pub 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
30/// 15 minute grace period between `requestClose` and `withdraw`.
31pub const CLOSE_GRACE_PERIOD: u64 = 15 * 60;
32
33/// EIP-712 type hash for signed cumulative payment vouchers.
34static VOUCHER_TYPEHASH: LazyLock<B256> =
35    LazyLock::new(|| keccak256(b"Voucher(bytes32 channelId,uint96 cumulativeAmount)"));
36/// EIP-712 domain type hash used by [`TIP20ChannelReserve::domain_separator`].
37static EIP712_DOMAIN_TYPEHASH: LazyLock<B256> = LazyLock::new(|| {
38    keccak256(b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
39});
40/// EIP-712 domain name hash for the reserve voucher domain.
41static NAME_HASH: LazyLock<B256> = LazyLock::new(|| keccak256(b"TIP20 Channel Reserve"));
42/// EIP-712 domain version hash for the reserve voucher domain.
43static VERSION_HASH: LazyLock<B256> = LazyLock::new(|| keccak256(b"1"));
44
45/// EIP-712 domain separator for the reserve voucher domain on mainnet.
46static DOMAIN_SEPARATOR_MAINNET: LazyLock<B256> =
47    LazyLock::new(|| domain_separator_inner(MAINNET_CHAIN_ID));
48/// EIP-712 domain separator for the reserve voucher domain on testnet.
49static DOMAIN_SEPARATOR_TESTNET: LazyLock<B256> =
50    LazyLock::new(|| domain_separator_inner(MODERATO_CHAIN_ID));
51
52/// Packed persistent state for one channel.
53///
54/// `deposit` being non-zero is the existence marker. `settled` is the cumulative amount
55/// already transferred to the payee. `close_requested_at` is zero until the payer starts
56/// the unilateral close timer.
57#[derive(Debug, Clone, Copy, Default, Storable)]
58struct PackedChannelState {
59    settled: U96,
60    deposit: U96,
61    close_requested_at: u32,
62}
63
64impl PackedChannelState {
65    /// Returns whether this storage slot contains an active channel.
66    fn exists(self) -> bool {
67        !self.deposit.is_zero()
68    }
69
70    /// Returns the payer's close request timestamp, if the close timer is active.
71    fn close_requested_at(self) -> Option<u32> {
72        (self.close_requested_at != 0).then_some(self.close_requested_at)
73    }
74
75    /// Converts packed native storage to the public Solidity ABI shape.
76    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    /// Persistent channel state keyed by `compute_channel_id_inner`.
88    channel_states: Mapping<B256, PackedChannelState>,
89
90    // WARNING: transient storage slots must remain after persistent storage fields until the
91    // `contract` macro supports independent persistent/transient layouts.
92    /// Transient same-transaction guard that prevents close-and-reopen with the same id.
93    opened_this_tx: Mapping<B256, bool>,
94    /// Transient per-transaction entropy seeded by the EVM handler before calls can open channels.
95    channel_open_context_hash: B256,
96}
97
98impl TIP20ChannelReserve {
99    /// Initializes the precompile storage layout.
100    pub fn initialize(&mut self) -> Result<()> {
101        self.__initialize()
102    }
103
104    /// Seeds the enclosing transaction's replay-protected context hash for `open` calls.
105    ///
106    /// The handler seeds `keccak256(encode_for_signing || sender)` for every real transaction
107    /// type. The value is stored in transient storage so batched `open` calls share the same
108    /// transaction-derived hash and the context is automatically cleared before the next
109    /// transaction. If this is not called, `open` reads zero from transient storage and reverts.
110    pub fn set_channel_open_context_hash(&mut self, hash: B256) -> Result<()> {
111        self.channel_open_context_hash.t_write(hash)
112    }
113
114    /// Opens a channel and pulls the initial deposit from the payer into reserve.
115    ///
116    /// Payees and integrators must independently decide whether any nonzero operator is acceptable
117    /// before relying on the channel.
118    ///
119    /// Payees cannot be zero or TIP-20 addresses. Virtual payees require a non-virtual operator.
120    /// This prevents channels whose payee cannot receive direct payouts or submit vouchers itself.
121    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    /// Settles an increasing cumulative voucher, paying only the unsettled delta to the payee.
184    ///
185    /// The payee can call directly. If an operator was set when the channel was opened, that
186    /// operator can submit the payee's voucher and route the payment to the descriptor payee.
187    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    /// Adds deposit to an existing channel and cancels a pending close request.
245    ///
246    /// A zero top-up is allowed and only cancels a pending close request.
247    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    /// Starts the payer's unilateral close timer.
312    ///
313    /// Repeated calls are idempotent while the timer is active.
314    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    /// Closes a channel from the payee side and refunds any uncaptured deposit to the payer.
345    ///
346    /// The payee can call directly. If an operator was set when the channel was opened, that
347    /// operator can close the channel and route any capture to the descriptor payee.
348    ///
349    /// `captureAmount` can be below `cumulativeAmount` but cannot be below what has already
350    /// settled. A new voucher is only required when the close captures more than `settled`.
351    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    /// Withdraws the payer's remaining deposit after the close grace period has elapsed.
425    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    /// Returns a descriptor with its current on-chain state.
473    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    /// Returns the current state for a channel id, or the zero state for an empty slot.
485    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    /// Returns current states for multiple channel ids.
493    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    /// Computes the deterministic channel id for a full channel descriptor.
508    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    /// Returns the EIP-712 digest that the payer or authorized signer must sign.
524    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    /// Returns the EIP-712 domain separator for this chain and precompile address.
532    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    /// Returns the current block timestamp as `u64`.
543    fn now(&self) -> u64 {
544        self.storage.timestamp().saturating_to::<u64>()
545    }
546
547    /// Returns the current block timestamp as the packed close-request representation.
548    fn now_u32(&self) -> u32 {
549        self.storage.timestamp().saturating_to::<u32>()
550    }
551
552    /// Computes the channel id from a descriptor.
553    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    /// Ensures the caller is the payee or the descriptor's nonzero operator.
566    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    /// Loads the transaction-scoped nonce hash seeded by the handler.
579    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    /// Computes the channel id including chain and precompile domain separation.
588    #[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    /// Loads an active channel or returns `ChannelNotFound`.
616    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    /// Returns the address authorized to sign vouchers for this descriptor.
625    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    /// Validates a voucher signature against the descriptor's expected signer.
634    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    /// Computes the EIP-712 voucher digest.
652    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
667/// Computes the EIP-712 domain separator.
668///
669/// NOTE: This keccak is unmetered because it is not computed at tx runtime.
670fn 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            // Virtual payees are valid when a non-virtual operator is set to submit vouchers on their behalf.
887            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            // Admission must check the effective recipient, not just the virtual alias.
921            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            // Top-ups must enforce the same resolved-recipient admission check.
937            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            // A blocked recipient cannot be used as the payee for a new channel.
999            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            // Unblock the payee so we can fund a channel for later sender-side checks.
1015            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            // Top-ups also reject channels whose payee can no longer receive.
1040            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            // Once funded, vouchers cannot transmit new value if the payer is blocked.
1051            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            // Settle enforces the logical payer-as-sender check, not just reserve -> payee.
1062            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            // Close enforces the same check when it would pay additional value to the payee.
1073            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}