Skip to main content

odra_modules/
cep3009.rs

1#![allow(clippy::too_many_arguments)]
2#![allow(missing_docs)]
3//! CEP-3009 — an adaptation of [ERC-3009] for the Casper Network.
4//!
5//! ERC-3009 ("Transfer With Authorization") lets a token holder authorize a
6//! transfer of their tokens via an off-chain EIP-712 signature. A third party
7//! (a relayer, or the recipient themselves) submits the authorization
8//! on-chain, so the token holder never has to spend gas to move their tokens.
9//! Unlike ERC-2612, no allowance is granted: each authorization is a direct,
10//! single-use mandate to move a specific `amount` to a specific `to` address.
11//!
12//! The module exposes three entry points:
13//!
14//! * [`CEP3009::transfer_with_authorization`] — anyone may relay; the funds
15//!   move from `from` to `to` once the signature checks out.
16//! * [`CEP3009::receive_with_authorization`] — only `to` may submit. On
17//!   Ethereum this exists to prevent front-running of a relayed
18//!   `to == contract` authorization; Casper has no public mempool, so
19//!   front-running isn't a concern. The variant is still useful because it
20//!   guarantees `caller == to`, which lets a receiving contract atomically
21//!   accept the transfer and act on the deposit in the same call.
22//! * [`CEP3009::cancel_authorization`] — the authorizer (or anyone relaying
23//!   the authorizer's signed cancel message) can burn an unused nonce so a
24//!   leaked authorization can never be redeemed.
25//!
26//! # Differences from EVM ERC-3009
27//!
28//! The EIP-712 typed-data digest construction (domain separator + the
29//! `TransferWithAuthorization`, `ReceiveWithAuthorization`, and
30//! `CancelAuthorization` struct hashes) follows the EVM specification exactly,
31//! so signatures produced by standard EIP-712 tooling remain compatible. The
32//! verification path differs because Casper does not provide `ecrecover`:
33//!
34//! * The caller must pass the signer's [`PublicKey`] explicitly alongside the
35//!   signature. The contract checks that `Address::from(public_key) == from`
36//!   (or `authorizer`, for cancellations) before verifying the signature, so
37//!   a valid signature from a different keypair cannot be used to move
38//!   someone else's tokens or burn someone else's nonce.
39//! * Signature verification uses the host's [`verify_signature`] facility,
40//!   which supports Casper's Ed25519 and Secp256k1 account keys.
41//! * The EIP-712 `chainId` field (a `uint256` on Ethereum) is replaced by a
42//!   `chain_name` string supplied at construction time, matching Casper's
43//!   chain identification model.
44//!
45//! Replay protection follows ERC-3009: each authorization carries a 32-byte
46//! `nonce` chosen by the signer (not a monotonic counter as in ERC-2612), and
47//! the contract records `(authorizer, nonce) -> used` so the same nonce can
48//! never be redeemed twice. Cancellation marks a nonce used without moving
49//! funds.
50//!
51//! [ERC-3009]: https://eips.ethereum.org/EIPS/eip-3009
52//! [`verify_signature`]: odra::ContractEnv::verify_signature
53
54use crate::{cep18::events::Transfer, cep18_token::Cep18, eip712};
55use casper_eip_712::DomainSeparator;
56use odra::{
57    casper_types::{bytesrepr::Bytes, PublicKey, U256},
58    named_keys::{compound_key_value_storage, single_value_storage},
59    prelude::*
60};
61
62// keccak256("TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)")
63const TRANSFER_WITH_AUTHORIZATION_TYPEHASH: [u8; 32] = [
64    0x7c, 0x7c, 0x6c, 0xdb, 0x67, 0xa1, 0x87, 0x43, 0xf4, 0x9e, 0xc6, 0xfa, 0x9b, 0x35, 0xf5, 0x0d,
65    0x52, 0xed, 0x05, 0xcb, 0xed, 0x4c, 0xc5, 0x92, 0xe1, 0x3b, 0x44, 0x50, 0x1c, 0x1a, 0x22, 0x67
66];
67
68// keccak256("ReceiveWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)")
69const RECEIVE_WITH_AUTHORIZATION_TYPEHASH: [u8; 32] = [
70    0xd0, 0x99, 0xcc, 0x98, 0xef, 0x71, 0x10, 0x7a, 0x61, 0x6c, 0x4f, 0x0f, 0x94, 0x1f, 0x04, 0xc3,
71    0x22, 0xd8, 0xe2, 0x54, 0xfe, 0x26, 0xb3, 0xc6, 0x66, 0x8d, 0xb8, 0x7a, 0xae, 0x41, 0x3d, 0xe8
72];
73
74// keccak256("CancelAuthorization(address authorizer,bytes32 nonce)")
75const CANCEL_AUTHORIZATION_TYPEHASH: [u8; 32] = [
76    0x15, 0x8b, 0x0a, 0x9e, 0xdf, 0x7a, 0x82, 0x8a, 0xad, 0x02, 0xf6, 0x3c, 0xd5, 0x15, 0xc6, 0x8e,
77    0xf2, 0xf5, 0x0b, 0xa8, 0x07, 0x39, 0x6f, 0x6d, 0x12, 0x84, 0x28, 0x33, 0xa1, 0x59, 0x74, 0x29
78];
79
80/// Emitted when an authorization is consumed by
81/// [`CEP3009::transfer_with_authorization`] or
82/// [`CEP3009::receive_with_authorization`]. The `(authorizer, nonce)` pair
83/// is now marked used and cannot be redeemed again.
84#[odra::event]
85pub struct AuthorizationUsed {
86    authorizer: Address,
87    nonce: Bytes
88}
89
90/// Emitted when an authorization nonce is burned by
91/// [`CEP3009::cancel_authorization`] before being used. The `(authorizer,
92/// nonce)` pair is marked used without any funds moving.
93#[odra::event]
94pub struct AuthorizationCanceled {
95    authorizer: Address,
96    nonce: Bytes
97}
98
99/// Errors raised by CEP-3009 authorization operations.
100#[odra::odra_error]
101pub enum Error {
102    /// The `(from, nonce)` pair has already been consumed by a transfer or
103    /// burned by a cancel. Authorizations are single-use.
104    NonceAlreadyUsed = 37_000,
105    /// The current block time is past `valid_before` — the authorization has
106    /// expired.
107    AuthorizationExpired = 37_001,
108    /// The current block time is at or before `valid_after` — the
109    /// authorization is not yet in its validity window.
110    AuthorizationNotYetValid = 37_002,
111    /// Signature verification against the supplied public key failed, or the
112    /// recomputed digest does not match what the signer signed (e.g. wrong
113    /// `amount`, `to`, or `nonce`).
114    InvalidSignature = 37_003,
115    /// The supplied `public_key` does not hash to the declared `from` /
116    /// `authorizer` address. Guards against using a valid signature from a
117    /// different keypair to move someone else's tokens or burn their nonce.
118    InvalidPublicKey = 37_004,
119    /// `receive_with_authorization` was submitted by an account other than
120    /// `to`. See the module-level docs for why this restriction exists.
121    InvalidCaller = 37_005,
122    /// Attempted to cancel an authorization whose `(authorizer, nonce)` pair
123    /// has already been consumed.
124    AuthorizationUsed = 37_006
125}
126
127/// Storage defined as named keys.
128const CHAIN_NAME_KEY: &str = "chain_name";
129const USED_NONCES_KEY: &str = "used_nonces";
130
131/// Domain separator version.
132const DOMAIN_VERSION: &str = "1";
133
134single_value_storage!(
135    CEP3009ChainNameStorage,
136    String,
137    CHAIN_NAME_KEY,
138    ExecutionError::KeyNotFound
139);
140
141compound_key_value_storage!(
142    CEP3009UsedNoncesStorage,
143    USED_NONCES_KEY,
144    Address,
145    Bytes,
146    bool
147);
148
149/// CEP-3009 authorization module — adds off-chain signed transfers to a
150/// CEP-18 token.
151///
152/// The module is meant to be composed with a [`Cep18`] sub-module (see
153/// [`CEP3009Wrapper`] for a deployable composition used in tests).
154#[odra::module(events = [AuthorizationUsed, AuthorizationCanceled, Transfer], errors = Error)]
155pub struct CEP3009 {
156    /// Per-(authorizer, nonce) flag recording whether that authorization has
157    /// been consumed or cancelled. Provides single-use semantics.
158    used_nonces: SubModule<CEP3009UsedNoncesStorage>,
159    /// CAIP-2 Chain name used as the EIP-712 domain's `chainId` substitute.
160    chain_name: SubModule<CEP3009ChainNameStorage>,
161    /// The CEP-18 token whose balances are mutated by authorized transfers.
162    token: SubModule<Cep18>
163}
164
165#[odra::module]
166impl CEP3009 {
167    /// Initializes the module by storing the EIP-712 chain ID and the
168    /// used-nonces storage.
169    pub fn init(&mut self, chain_name: String) {
170        self.chain_name.set(chain_name);
171        self.used_nonces.init();
172    }
173
174    /// Returns `true` if `(authorizer, nonce)` has been consumed by a
175    /// previous transfer or burned by [`Self::cancel_authorization`]. Useful
176    /// for off-chain clients to check before submitting a relayed transfer.
177    pub fn authorization_state(&self, authorizer: Address, nonce: Bytes) -> bool {
178        self.used_nonces.get_or_default(&authorizer, &nonce)
179    }
180
181    /// Consumes a signed `TransferWithAuthorization` and moves `amount`
182    /// tokens from `from` to `to`. Any account may submit this call — the
183    /// signature, not the caller, is what authorizes the transfer.
184    ///
185    /// Reverts with [`Error::NonceAlreadyUsed`], [`Error::AuthorizationNotYetValid`],
186    /// [`Error::AuthorizationExpired`], [`Error::InvalidPublicKey`], or
187    /// [`Error::InvalidSignature`] if the corresponding precondition fails.
188    /// On success, marks the nonce used, emits [`AuthorizationUsed`], and
189    /// performs the transfer via [`Cep18::raw_transfer`].
190    pub fn transfer_with_authorization(
191        &mut self,
192        from: Address,
193        to: Address,
194        amount: U256,
195        valid_after: u64,
196        valid_before: u64,
197        nonce: Bytes,
198        public_key: PublicKey,
199        signature: Bytes
200    ) {
201        self.raw_transfer_with_authorization(
202            TRANSFER_WITH_AUTHORIZATION_TYPEHASH,
203            from,
204            to,
205            amount,
206            valid_after,
207            valid_before,
208            nonce,
209            public_key,
210            signature
211        );
212    }
213
214    /// Like [`Self::transfer_with_authorization`], but requires
215    /// `caller == to`. This lets a receiving contract atomically accept a
216    /// pre-signed transfer and act on it within the same call (e.g. a
217    /// "deposit then mint LP token" flow), while preventing any other
218    /// account from triggering the transfer at a moment of their choosing.
219    ///
220    /// Reverts with [`Error::InvalidCaller`] if `caller != to`. All other
221    /// checks and effects mirror `transfer_with_authorization`.
222    pub fn receive_with_authorization(
223        &mut self,
224        from: Address,
225        to: Address,
226        amount: U256,
227        valid_after: u64,
228        valid_before: u64,
229        nonce: Bytes,
230        public_key: PublicKey,
231        signature: Bytes
232    ) {
233        let caller = self.env().caller();
234        if caller != to {
235            self.env().revert(Error::InvalidCaller);
236        }
237
238        self.raw_transfer_with_authorization(
239            RECEIVE_WITH_AUTHORIZATION_TYPEHASH,
240            from,
241            to,
242            amount,
243            valid_after,
244            valid_before,
245            nonce,
246            public_key,
247            signature
248        );
249    }
250
251    /// Burns an unused authorization nonce by consuming a signed
252    /// `CancelAuthorization` message. Once a nonce is cancelled, any
253    /// previously distributed signature for the same `(authorizer, nonce)`
254    /// pair becomes unredeemable — the canonical way for an authorizer to
255    /// revoke a leaked or otherwise unwanted authorization.
256    ///
257    /// Reverts with [`Error::AuthorizationUsed`] if the nonce has already
258    /// been consumed (a cancelled nonce cannot be re-cancelled either), with
259    /// [`Error::InvalidPublicKey`] if `public_key` does not hash to
260    /// `authorizer`, or with [`Error::InvalidSignature`] if signature
261    /// verification fails. On success, marks the nonce used and emits
262    /// [`AuthorizationCanceled`].
263    pub fn cancel_authorization(
264        &mut self,
265        authorizer: Address,
266        nonce: Bytes,
267        public_key: PublicKey,
268        signature: Bytes
269    ) {
270        if self.authorization_state(authorizer, nonce.clone()) {
271            self.env().revert(Error::AuthorizationUsed);
272        }
273
274        if Address::from(public_key.clone()) != authorizer {
275            self.env().revert(Error::InvalidPublicKey);
276        }
277
278        let message = self.build_cancel_message(authorizer, &nonce);
279        if !self
280            .env()
281            .verify_signature(&message, &signature, &public_key)
282        {
283            self.env().revert(Error::InvalidSignature);
284        }
285
286        self.used_nonces.set(&authorizer, &nonce, true);
287        self.env()
288            .emit_event(AuthorizationCanceled { authorizer, nonce });
289    }
290}
291
292impl CEP3009 {
293    /// Shared body of [`Self::transfer_with_authorization`] and
294    /// [`Self::receive_with_authorization`]. They differ only in the EIP-712
295    /// typehash used to compute the digest (and in the caller restriction
296    /// applied by the receive variant before calling this).
297    fn raw_transfer_with_authorization(
298        &mut self,
299        typehash: [u8; 32],
300        from: Address,
301        to: Address,
302        amount: U256,
303        valid_after: u64,
304        valid_before: u64,
305        nonce: Bytes,
306        public_key: PublicKey,
307        signature: Bytes
308    ) {
309        // 1. Replay protection
310        if self.used_nonces.get_or_default(&from, &nonce) {
311            self.env().revert(Error::NonceAlreadyUsed);
312        }
313
314        // 2. block_time
315        let now_secs = self.env().get_block_time_secs();
316
317        // 3. Check valid_after
318        if now_secs <= valid_after {
319            self.env().revert(Error::AuthorizationNotYetValid);
320        }
321
322        // 4. Check valid_before
323        if now_secs >= valid_before {
324            self.env().revert(Error::AuthorizationExpired);
325        }
326
327        // 5. Verify that public_key matches the `from` address
328        let derived_address = Address::from(public_key.clone());
329        if derived_address != from {
330            self.env().revert(Error::InvalidPublicKey);
331        }
332
333        // 6. Build message and verify signature
334        let message = self.build_authorization_message(
335            typehash,
336            from,
337            to,
338            &amount,
339            valid_after,
340            valid_before,
341            &nonce
342        );
343
344        if !self
345            .env()
346            .verify_signature(&message, &signature, &public_key)
347        {
348            self.env().revert(Error::InvalidSignature);
349        }
350
351        // 7. Mark nonce as used
352        self.used_nonces.set(&from, &nonce, true);
353
354        // 8. Emit event
355        self.env().emit_event(AuthorizationUsed {
356            authorizer: from,
357            nonce
358        });
359
360        // 9. Execute transfer (raw_transfer takes refs)
361        self.token.raw_transfer(&from, &to, &amount);
362
363        // 10. Emit event.
364        self.env().emit_event(Transfer {
365            sender: from,
366            recipient: to,
367            amount
368        });
369    }
370
371    /// Builds the EIP-712 digest for a transfer authorization. `amount` is
372    /// big-endian encoded to match the EVM `uint256` layout, and `nonce` is
373    /// right-padded to 32 bytes to match the EIP-712 `bytes32` layout.
374    fn build_authorization_message(
375        &self,
376        typehash: [u8; 32],
377        from: Address,
378        to: Address,
379        amount: &U256,
380        valid_after: u64,
381        valid_before: u64,
382        nonce: &[u8]
383    ) -> Bytes {
384        let mut value_bytes = [0u8; 32];
385        amount.to_big_endian(&mut value_bytes);
386
387        let mut nonce_padded = [0u8; 32];
388        let len = nonce.len().min(32);
389        nonce_padded[..len].copy_from_slice(&nonce[..len]);
390
391        let mut encoded_data = Vec::with_capacity(6 * 32);
392        encoded_data.extend(eip712::encode_address(from));
393        encoded_data.extend(eip712::encode_address(to));
394        encoded_data.extend(casper_eip_712::encode_uint256(value_bytes));
395        encoded_data.extend(casper_eip_712::encode_uint64(valid_after));
396        encoded_data.extend(casper_eip_712::encode_uint64(valid_before));
397        encoded_data.extend(casper_eip_712::encode_bytes32(nonce_padded));
398        let domain = self.domain_separator();
399
400        Bytes::from(crate::eip712::hash_typed_data(domain, typehash, encoded_data).to_vec())
401    }
402
403    /// Builds the EIP-712 digest for a `CancelAuthorization` message.
404    fn build_cancel_message(&self, authorizer: Address, nonce: &[u8]) -> Bytes {
405        let mut nonce_padded = [0u8; 32];
406        let len = nonce.len().min(32);
407        nonce_padded[..len].copy_from_slice(&nonce[..len]);
408
409        let mut encoded_data = Vec::with_capacity(64);
410        encoded_data.extend(eip712::encode_address(authorizer));
411        encoded_data.extend(casper_eip_712::encode_bytes32(nonce_padded));
412        let domain = self.domain_separator();
413
414        Bytes::from(
415            eip712::hash_typed_data(domain, CANCEL_AUTHORIZATION_TYPEHASH, encoded_data).to_vec()
416        )
417    }
418
419    /// Builds the EIP-712 domain separator. Bound to the token `name`, the
420    /// fixed `DOMAIN_VERSION`, the configured `chain_name`, and the
421    /// deployed contract's own address (the EIP-712 `verifyingContract`).
422    fn domain_separator(&self) -> DomainSeparator {
423        let self_address = self.env().self_address();
424        let name = self.token.name();
425        let chain_name = self.chain_name.get();
426        eip712::domain_separator(&name, DOMAIN_VERSION, chain_name, self_address)
427    }
428}
429
430/// Wrapper contract that combines ERC-3009 functionality with a CEP-18 token for testing purposes.
431#[odra::module]
432pub struct CEP3009Wrapper {
433    cep3009: SubModule<CEP3009>,
434    token: SubModule<Cep18>
435}
436
437#[odra::module]
438impl CEP3009Wrapper {
439    /// Initializes both the authorization module (storing the EIP-712 chain
440    /// id) and the underlying CEP-18 token (symbol, name, decimals, supply).
441    pub fn init(
442        &mut self,
443        chain_name: String,
444        symbol: String,
445        name: String,
446        decimals: u8,
447        initial_supply: U256
448    ) {
449        self.cep3009.init(chain_name);
450        self.token.init(symbol, name, decimals, initial_supply);
451    }
452
453    delegate! {
454        to self.cep3009 {
455            fn authorization_state(&self, authorizer: Address, nonce: Bytes) -> bool;
456            fn transfer_with_authorization(
457                &mut self,
458                from: Address,
459                to: Address,
460                amount: U256,
461                valid_after: u64,
462                valid_before: u64,
463                nonce: Bytes,
464                public_key: PublicKey,
465                signature: Bytes
466            );
467            fn receive_with_authorization(
468                &mut self,
469                from: Address,
470                to: Address,
471                amount: U256,
472                valid_after: u64,
473                valid_before: u64,
474                nonce: Bytes,
475                public_key: PublicKey,
476                signature: Bytes
477            );
478            fn cancel_authorization(
479                &mut self,
480                authorizer: Address,
481                nonce: Bytes,
482                public_key: PublicKey,
483                signature: Bytes
484            );
485        }
486
487        to self.token {
488            fn balance_of(&self, address: &Address) -> U256;
489        }
490    }
491}
492
493#[cfg(test)]
494mod tests {
495    use super::*;
496    use odra::host::{Deployer, HostEnv};
497
498    const TOKEN_NAME: &str = "Test Token";
499    const TOKEN_SYMBOL: &str = "TEST";
500    const TOKEN_DECIMALS: u8 = 8;
501    const INITIAL_SUPPLY: u64 = 1_000_000;
502    const CHAIN_NAME: &str = "casper:casper";
503
504    struct Setup {
505        env: HostEnv,
506        wrapper: CEP3009WrapperHostRef,
507        alice: Address,
508        bob: Address,
509        charlie: Address,
510        alice_pubkey: PublicKey
511    }
512
513    /// Deploys the wrapper with alice as deployer (so she gets the initial supply)
514    /// and advances block time past `valid_after = 0`.
515    fn setup() -> Setup {
516        let env = odra_test::env();
517        let alice = env.get_account(0);
518        let bob = env.get_account(1);
519        let charlie = env.get_account(2);
520        let alice_pubkey = env.public_key(&alice);
521
522        let wrapper = CEP3009Wrapper::deploy(
523            &env,
524            CEP3009WrapperInitArgs {
525                chain_name: CHAIN_NAME.to_string(),
526                symbol: TOKEN_SYMBOL.to_string(),
527                name: TOKEN_NAME.to_string(),
528                decimals: TOKEN_DECIMALS,
529                initial_supply: INITIAL_SUPPLY.into()
530            }
531        );
532
533        env.advance_block_time(1_000);
534
535        Setup {
536            env,
537            wrapper,
538            alice,
539            bob,
540            charlie,
541            alice_pubkey
542        }
543    }
544
545    fn fresh_nonce(seed: u8) -> Bytes {
546        Bytes::from(vec![seed; 32])
547    }
548
549    #[test]
550    fn transfer_with_authorization_happy_path() {
551        let Setup {
552            env,
553            mut wrapper,
554            alice,
555            bob,
556            charlie,
557            alice_pubkey
558        } = setup();
559
560        assert_eq!(wrapper.balance_of(&alice), INITIAL_SUPPLY.into());
561        assert_eq!(wrapper.balance_of(&bob), U256::zero());
562
563        let amount: U256 = 100u64.into();
564        let valid_after: u64 = 0;
565        let valid_before: u64 = u64::MAX;
566        let nonce = fresh_nonce(1);
567
568        let signature = sign_transfer_authorization(
569            &env,
570            &alice,
571            wrapper.address(),
572            TRANSFER_WITH_AUTHORIZATION_TYPEHASH,
573            alice,
574            bob,
575            amount,
576            valid_after,
577            valid_before,
578            &nonce
579        );
580
581        // Charlie relays the authorization on alice's behalf.
582        env.set_caller(charlie);
583        wrapper.transfer_with_authorization(
584            alice,
585            bob,
586            amount,
587            valid_after,
588            valid_before,
589            nonce.clone(),
590            alice_pubkey,
591            signature
592        );
593
594        assert_eq!(
595            wrapper.balance_of(&alice),
596            U256::from(INITIAL_SUPPLY) - amount
597        );
598        assert_eq!(wrapper.balance_of(&bob), amount);
599        assert!(wrapper.authorization_state(alice, nonce.clone()));
600        assert!(env.emitted_event(
601            &wrapper,
602            AuthorizationUsed {
603                authorizer: alice,
604                nonce
605            }
606        ));
607    }
608
609    #[test]
610    fn transfer_with_authorization_replay_protection() {
611        let Setup {
612            env,
613            mut wrapper,
614            alice,
615            bob,
616            charlie,
617            alice_pubkey
618        } = setup();
619
620        let amount: U256 = 100u64.into();
621        let valid_after: u64 = 0;
622        let valid_before: u64 = u64::MAX;
623        let nonce = fresh_nonce(2);
624
625        let signature = sign_transfer_authorization(
626            &env,
627            &alice,
628            wrapper.address(),
629            TRANSFER_WITH_AUTHORIZATION_TYPEHASH,
630            alice,
631            bob,
632            amount,
633            valid_after,
634            valid_before,
635            &nonce
636        );
637
638        env.set_caller(charlie);
639        wrapper.transfer_with_authorization(
640            alice,
641            bob,
642            amount,
643            valid_after,
644            valid_before,
645            nonce.clone(),
646            alice_pubkey.clone(),
647            signature.clone()
648        );
649
650        // Submitting the same authorization a second time reverts.
651        assert_eq!(
652            wrapper.try_transfer_with_authorization(
653                alice,
654                bob,
655                amount,
656                valid_after,
657                valid_before,
658                nonce,
659                alice_pubkey,
660                signature
661            ),
662            Err(Error::NonceAlreadyUsed.into())
663        );
664    }
665
666    #[test]
667    fn transfer_with_authorization_not_yet_valid() {
668        let Setup {
669            env,
670            mut wrapper,
671            alice,
672            bob,
673            charlie,
674            alice_pubkey
675        } = setup();
676
677        // Block time is at 1 second; require it to be in the future.
678        let valid_after: u64 = 60;
679        let valid_before: u64 = u64::MAX;
680        let amount: U256 = 100u64.into();
681        let nonce = fresh_nonce(3);
682
683        let signature = sign_transfer_authorization(
684            &env,
685            &alice,
686            wrapper.address(),
687            TRANSFER_WITH_AUTHORIZATION_TYPEHASH,
688            alice,
689            bob,
690            amount,
691            valid_after,
692            valid_before,
693            &nonce
694        );
695
696        env.set_caller(charlie);
697        assert_eq!(
698            wrapper.try_transfer_with_authorization(
699                alice,
700                bob,
701                amount,
702                valid_after,
703                valid_before,
704                nonce,
705                alice_pubkey,
706                signature
707            ),
708            Err(Error::AuthorizationNotYetValid.into())
709        );
710    }
711
712    #[test]
713    fn transfer_with_authorization_expired() {
714        let Setup {
715            env,
716            mut wrapper,
717            alice,
718            bob,
719            charlie,
720            alice_pubkey
721        } = setup();
722
723        // Move further into the future so that valid_before is in the past.
724        env.advance_block_time(60_000);
725
726        let valid_after: u64 = 0;
727        let valid_before: u64 = 10;
728        let amount: U256 = 100u64.into();
729        let nonce = fresh_nonce(4);
730
731        let signature = sign_transfer_authorization(
732            &env,
733            &alice,
734            wrapper.address(),
735            TRANSFER_WITH_AUTHORIZATION_TYPEHASH,
736            alice,
737            bob,
738            amount,
739            valid_after,
740            valid_before,
741            &nonce
742        );
743
744        env.set_caller(charlie);
745        assert_eq!(
746            wrapper.try_transfer_with_authorization(
747                alice,
748                bob,
749                amount,
750                valid_after,
751                valid_before,
752                nonce,
753                alice_pubkey,
754                signature
755            ),
756            Err(Error::AuthorizationExpired.into())
757        );
758    }
759
760    #[test]
761    fn transfer_with_authorization_invalid_signature() {
762        let Setup {
763            env,
764            mut wrapper,
765            alice,
766            bob,
767            charlie,
768            alice_pubkey
769        } = setup();
770
771        let valid_after: u64 = 0;
772        let valid_before: u64 = u64::MAX;
773        let nonce = fresh_nonce(5);
774
775        // Alice signs for an amount of 100 ...
776        let signature = sign_transfer_authorization(
777            &env,
778            &alice,
779            wrapper.address(),
780            TRANSFER_WITH_AUTHORIZATION_TYPEHASH,
781            alice,
782            bob,
783            U256::from(100u64),
784            valid_after,
785            valid_before,
786            &nonce
787        );
788
789        // ... but charlie submits with a different amount, so the digest the
790        // contract recomputes won't match the signature.
791        env.set_caller(charlie);
792        assert_eq!(
793            wrapper.try_transfer_with_authorization(
794                alice,
795                bob,
796                U256::from(200u64),
797                valid_after,
798                valid_before,
799                nonce,
800                alice_pubkey,
801                signature
802            ),
803            Err(Error::InvalidSignature.into())
804        );
805    }
806
807    #[test]
808    fn transfer_with_authorization_public_key_mismatch() {
809        let Setup {
810            env,
811            mut wrapper,
812            alice,
813            bob,
814            charlie,
815            ..
816        } = setup();
817
818        let valid_after: u64 = 0;
819        let valid_before: u64 = u64::MAX;
820        let amount: U256 = 100u64.into();
821        let nonce = fresh_nonce(6);
822
823        // Sign with bob's key while declaring alice as `from`.
824        let bob_pubkey = env.public_key(&bob);
825        let signature = sign_transfer_authorization(
826            &env,
827            &bob,
828            wrapper.address(),
829            TRANSFER_WITH_AUTHORIZATION_TYPEHASH,
830            alice,
831            bob,
832            amount,
833            valid_after,
834            valid_before,
835            &nonce
836        );
837
838        env.set_caller(charlie);
839        assert_eq!(
840            wrapper.try_transfer_with_authorization(
841                alice,
842                bob,
843                amount,
844                valid_after,
845                valid_before,
846                nonce,
847                bob_pubkey,
848                signature
849            ),
850            Err(Error::InvalidPublicKey.into())
851        );
852    }
853
854    #[test]
855    fn receive_with_authorization_happy_path() {
856        let Setup {
857            env,
858            mut wrapper,
859            alice,
860            bob,
861            alice_pubkey,
862            ..
863        } = setup();
864
865        let amount: U256 = 250u64.into();
866        let valid_after: u64 = 0;
867        let valid_before: u64 = u64::MAX;
868        let nonce = fresh_nonce(7);
869
870        let signature = sign_transfer_authorization(
871            &env,
872            &alice,
873            wrapper.address(),
874            RECEIVE_WITH_AUTHORIZATION_TYPEHASH,
875            alice,
876            bob,
877            amount,
878            valid_after,
879            valid_before,
880            &nonce
881        );
882
883        // Bob (the recipient) must be the caller for receive_with_authorization.
884        env.set_caller(bob);
885        wrapper.receive_with_authorization(
886            alice,
887            bob,
888            amount,
889            valid_after,
890            valid_before,
891            nonce.clone(),
892            alice_pubkey,
893            signature
894        );
895
896        assert_eq!(
897            wrapper.balance_of(&alice),
898            U256::from(INITIAL_SUPPLY) - amount
899        );
900        assert_eq!(wrapper.balance_of(&bob), amount);
901        assert!(wrapper.authorization_state(alice, nonce));
902    }
903
904    #[test]
905    fn receive_with_authorization_wrong_caller_reverts() {
906        let Setup {
907            env,
908            mut wrapper,
909            alice,
910            bob,
911            charlie,
912            alice_pubkey
913        } = setup();
914
915        let amount: U256 = 250u64.into();
916        let valid_after: u64 = 0;
917        let valid_before: u64 = u64::MAX;
918        let nonce = fresh_nonce(8);
919
920        let signature = sign_transfer_authorization(
921            &env,
922            &alice,
923            wrapper.address(),
924            RECEIVE_WITH_AUTHORIZATION_TYPEHASH,
925            alice,
926            bob,
927            amount,
928            valid_after,
929            valid_before,
930            &nonce
931        );
932
933        // Charlie tries to relay even though `to` is bob.
934        env.set_caller(charlie);
935        assert_eq!(
936            wrapper.try_receive_with_authorization(
937                alice,
938                bob,
939                amount,
940                valid_after,
941                valid_before,
942                nonce,
943                alice_pubkey,
944                signature
945            ),
946            Err(Error::InvalidCaller.into())
947        );
948    }
949
950    #[test]
951    fn cancel_authorization_happy_path() {
952        let Setup {
953            env,
954            mut wrapper,
955            alice,
956            bob,
957            charlie,
958            alice_pubkey
959        } = setup();
960
961        let nonce = fresh_nonce(9);
962        let cancel_signature =
963            sign_cancel_authorization(&env, &alice, wrapper.address(), alice, &nonce);
964
965        env.set_caller(charlie);
966        wrapper.cancel_authorization(alice, nonce.clone(), alice_pubkey.clone(), cancel_signature);
967
968        assert!(wrapper.authorization_state(alice, nonce.clone()));
969        assert!(env.emitted_event(
970            &wrapper,
971            AuthorizationCanceled {
972                authorizer: alice,
973                nonce: nonce.clone()
974            }
975        ));
976
977        // A subsequent transfer reusing the cancelled nonce is rejected.
978        let amount: U256 = 1u64.into();
979        let valid_after: u64 = 0;
980        let valid_before: u64 = u64::MAX;
981        let transfer_signature = sign_transfer_authorization(
982            &env,
983            &alice,
984            wrapper.address(),
985            TRANSFER_WITH_AUTHORIZATION_TYPEHASH,
986            alice,
987            bob,
988            amount,
989            valid_after,
990            valid_before,
991            &nonce
992        );
993        assert_eq!(
994            wrapper.try_transfer_with_authorization(
995                alice,
996                bob,
997                amount,
998                valid_after,
999                valid_before,
1000                nonce,
1001                alice_pubkey,
1002                transfer_signature
1003            ),
1004            Err(Error::NonceAlreadyUsed.into())
1005        );
1006    }
1007
1008    #[test]
1009    fn cancel_authorization_already_used_reverts() {
1010        let Setup {
1011            env,
1012            mut wrapper,
1013            alice,
1014            bob,
1015            charlie,
1016            alice_pubkey
1017        } = setup();
1018
1019        let amount: U256 = 100u64.into();
1020        let valid_after: u64 = 0;
1021        let valid_before: u64 = u64::MAX;
1022        let nonce = fresh_nonce(10);
1023
1024        // Consume the nonce via a successful transfer first.
1025        let transfer_signature = sign_transfer_authorization(
1026            &env,
1027            &alice,
1028            wrapper.address(),
1029            TRANSFER_WITH_AUTHORIZATION_TYPEHASH,
1030            alice,
1031            bob,
1032            amount,
1033            valid_after,
1034            valid_before,
1035            &nonce
1036        );
1037        env.set_caller(charlie);
1038        wrapper.transfer_with_authorization(
1039            alice,
1040            bob,
1041            amount,
1042            valid_after,
1043            valid_before,
1044            nonce.clone(),
1045            alice_pubkey.clone(),
1046            transfer_signature
1047        );
1048
1049        // Cancelling an already-consumed nonce reverts.
1050        let cancel_signature =
1051            sign_cancel_authorization(&env, &alice, wrapper.address(), alice, &nonce);
1052        assert_eq!(
1053            wrapper.try_cancel_authorization(alice, nonce, alice_pubkey, cancel_signature),
1054            Err(Error::AuthorizationUsed.into())
1055        );
1056    }
1057
1058    #[test]
1059    fn cancel_authorization_with_mismatched_public_key_reverts() {
1060        let Setup {
1061            env,
1062            mut wrapper,
1063            alice,
1064            bob,
1065            charlie,
1066            ..
1067        } = setup();
1068
1069        let nonce = fresh_nonce(11);
1070
1071        // Bob signs a cancel message that names alice as the authorizer,
1072        // using his own keypair.
1073        let bob_pubkey = env.public_key(&bob);
1074        let cancel_signature =
1075            sign_cancel_authorization(&env, &bob, wrapper.address(), alice, &nonce);
1076
1077        // Submitting with authorizer=alice but public_key=bob_pubkey must fail,
1078        // otherwise an attacker could burn alice's nonces.
1079        env.set_caller(charlie);
1080        assert_eq!(
1081            wrapper.try_cancel_authorization(alice, nonce.clone(), bob_pubkey, cancel_signature),
1082            Err(Error::InvalidPublicKey.into())
1083        );
1084
1085        // The nonce must remain unused so alice's pending authorization is not bricked.
1086        assert!(!wrapper.authorization_state(alice, nonce));
1087    }
1088
1089    fn sign_transfer_authorization(
1090        env: &HostEnv,
1091        signer: &Address,
1092        contract_address: Address,
1093        typehash: [u8; 32],
1094        from: Address,
1095        to: Address,
1096        amount: U256,
1097        valid_after: u64,
1098        valid_before: u64,
1099        nonce: &Bytes
1100    ) -> Bytes {
1101        let mut value_bytes = [0u8; 32];
1102        amount.to_big_endian(&mut value_bytes);
1103
1104        let mut nonce_padded = [0u8; 32];
1105        let len = nonce.len().min(32);
1106        nonce_padded[..len].copy_from_slice(&nonce[..len]);
1107
1108        let mut encoded_data = Vec::with_capacity(6 * 32);
1109        encoded_data.extend(crate::eip712::encode_address(from));
1110        encoded_data.extend(crate::eip712::encode_address(to));
1111        encoded_data.extend(casper_eip_712::encode_uint256(value_bytes));
1112        encoded_data.extend(casper_eip_712::encode_uint64(valid_after));
1113        encoded_data.extend(casper_eip_712::encode_uint64(valid_before));
1114        encoded_data.extend(casper_eip_712::encode_bytes32(nonce_padded));
1115
1116        let domain = crate::eip712::domain_separator(
1117            TOKEN_NAME,
1118            DOMAIN_VERSION,
1119            CHAIN_NAME.to_string(),
1120            contract_address
1121        );
1122        let message_hash = crate::eip712::hash_typed_data(domain, typehash, encoded_data);
1123        let message = Bytes::from(message_hash.to_vec());
1124
1125        env.sign_message(&message, signer)
1126    }
1127
1128    fn sign_cancel_authorization(
1129        env: &HostEnv,
1130        signer: &Address,
1131        contract_address: Address,
1132        authorizer: Address,
1133        nonce: &Bytes
1134    ) -> Bytes {
1135        let mut nonce_padded = [0u8; 32];
1136        let len = nonce.len().min(32);
1137        nonce_padded[..len].copy_from_slice(&nonce[..len]);
1138
1139        let mut encoded_data = Vec::with_capacity(64);
1140        encoded_data.extend(crate::eip712::encode_address(authorizer));
1141        encoded_data.extend(casper_eip_712::encode_bytes32(nonce_padded));
1142
1143        let domain = crate::eip712::domain_separator(
1144            TOKEN_NAME,
1145            DOMAIN_VERSION,
1146            CHAIN_NAME.to_string(),
1147            contract_address
1148        );
1149        let message_hash =
1150            crate::eip712::hash_typed_data(domain, CANCEL_AUTHORIZATION_TYPEHASH, encoded_data);
1151        let message = Bytes::from(message_hash.to_vec());
1152
1153        env.sign_message(&message, signer)
1154    }
1155}