#![allow(clippy::too_many_arguments)]
#![allow(missing_docs)]
use crate::{cep18::events::Transfer, cep18_token::Cep18, eip712};
use casper_eip_712::DomainSeparator;
use odra::{
casper_types::{bytesrepr::Bytes, PublicKey, U256},
named_keys::{compound_key_value_storage, single_value_storage},
prelude::*
};
const TRANSFER_WITH_AUTHORIZATION_TYPEHASH: [u8; 32] = [
0x7c, 0x7c, 0x6c, 0xdb, 0x67, 0xa1, 0x87, 0x43, 0xf4, 0x9e, 0xc6, 0xfa, 0x9b, 0x35, 0xf5, 0x0d,
0x52, 0xed, 0x05, 0xcb, 0xed, 0x4c, 0xc5, 0x92, 0xe1, 0x3b, 0x44, 0x50, 0x1c, 0x1a, 0x22, 0x67
];
const RECEIVE_WITH_AUTHORIZATION_TYPEHASH: [u8; 32] = [
0xd0, 0x99, 0xcc, 0x98, 0xef, 0x71, 0x10, 0x7a, 0x61, 0x6c, 0x4f, 0x0f, 0x94, 0x1f, 0x04, 0xc3,
0x22, 0xd8, 0xe2, 0x54, 0xfe, 0x26, 0xb3, 0xc6, 0x66, 0x8d, 0xb8, 0x7a, 0xae, 0x41, 0x3d, 0xe8
];
const CANCEL_AUTHORIZATION_TYPEHASH: [u8; 32] = [
0x15, 0x8b, 0x0a, 0x9e, 0xdf, 0x7a, 0x82, 0x8a, 0xad, 0x02, 0xf6, 0x3c, 0xd5, 0x15, 0xc6, 0x8e,
0xf2, 0xf5, 0x0b, 0xa8, 0x07, 0x39, 0x6f, 0x6d, 0x12, 0x84, 0x28, 0x33, 0xa1, 0x59, 0x74, 0x29
];
#[odra::event]
pub struct AuthorizationUsed {
authorizer: Address,
nonce: Bytes
}
#[odra::event]
pub struct AuthorizationCanceled {
authorizer: Address,
nonce: Bytes
}
#[odra::odra_error]
pub enum Error {
NonceAlreadyUsed = 37_000,
AuthorizationExpired = 37_001,
AuthorizationNotYetValid = 37_002,
InvalidSignature = 37_003,
InvalidPublicKey = 37_004,
InvalidCaller = 37_005,
AuthorizationUsed = 37_006
}
const CHAIN_NAME_KEY: &str = "chain_name";
const USED_NONCES_KEY: &str = "used_nonces";
const DOMAIN_VERSION: &str = "1";
single_value_storage!(
CEP3009ChainNameStorage,
String,
CHAIN_NAME_KEY,
ExecutionError::KeyNotFound
);
compound_key_value_storage!(
CEP3009UsedNoncesStorage,
USED_NONCES_KEY,
Address,
Bytes,
bool
);
#[odra::module(events = [AuthorizationUsed, AuthorizationCanceled, Transfer], errors = Error)]
pub struct CEP3009 {
used_nonces: SubModule<CEP3009UsedNoncesStorage>,
chain_name: SubModule<CEP3009ChainNameStorage>,
token: SubModule<Cep18>
}
#[odra::module]
impl CEP3009 {
pub fn init(&mut self, chain_name: String) {
self.chain_name.set(chain_name);
self.used_nonces.init();
}
pub fn authorization_state(&self, authorizer: Address, nonce: Bytes) -> bool {
self.used_nonces.get_or_default(&authorizer, &nonce)
}
pub fn transfer_with_authorization(
&mut self,
from: Address,
to: Address,
amount: U256,
valid_after: u64,
valid_before: u64,
nonce: Bytes,
public_key: PublicKey,
signature: Bytes
) {
self.raw_transfer_with_authorization(
TRANSFER_WITH_AUTHORIZATION_TYPEHASH,
from,
to,
amount,
valid_after,
valid_before,
nonce,
public_key,
signature
);
}
pub fn receive_with_authorization(
&mut self,
from: Address,
to: Address,
amount: U256,
valid_after: u64,
valid_before: u64,
nonce: Bytes,
public_key: PublicKey,
signature: Bytes
) {
let caller = self.env().caller();
if caller != to {
self.env().revert(Error::InvalidCaller);
}
self.raw_transfer_with_authorization(
RECEIVE_WITH_AUTHORIZATION_TYPEHASH,
from,
to,
amount,
valid_after,
valid_before,
nonce,
public_key,
signature
);
}
pub fn cancel_authorization(
&mut self,
authorizer: Address,
nonce: Bytes,
public_key: PublicKey,
signature: Bytes
) {
if self.authorization_state(authorizer, nonce.clone()) {
self.env().revert(Error::AuthorizationUsed);
}
if Address::from(public_key.clone()) != authorizer {
self.env().revert(Error::InvalidPublicKey);
}
let message = self.build_cancel_message(authorizer, &nonce);
if !self
.env()
.verify_signature(&message, &signature, &public_key)
{
self.env().revert(Error::InvalidSignature);
}
self.used_nonces.set(&authorizer, &nonce, true);
self.env()
.emit_event(AuthorizationCanceled { authorizer, nonce });
}
}
impl CEP3009 {
fn raw_transfer_with_authorization(
&mut self,
typehash: [u8; 32],
from: Address,
to: Address,
amount: U256,
valid_after: u64,
valid_before: u64,
nonce: Bytes,
public_key: PublicKey,
signature: Bytes
) {
if self.used_nonces.get_or_default(&from, &nonce) {
self.env().revert(Error::NonceAlreadyUsed);
}
let now_secs = self.env().get_block_time_secs();
if now_secs <= valid_after {
self.env().revert(Error::AuthorizationNotYetValid);
}
if now_secs >= valid_before {
self.env().revert(Error::AuthorizationExpired);
}
let derived_address = Address::from(public_key.clone());
if derived_address != from {
self.env().revert(Error::InvalidPublicKey);
}
let message = self.build_authorization_message(
typehash,
from,
to,
&amount,
valid_after,
valid_before,
&nonce
);
if !self
.env()
.verify_signature(&message, &signature, &public_key)
{
self.env().revert(Error::InvalidSignature);
}
self.used_nonces.set(&from, &nonce, true);
self.env().emit_event(AuthorizationUsed {
authorizer: from,
nonce
});
self.token.raw_transfer(&from, &to, &amount);
self.env().emit_event(Transfer {
sender: from,
recipient: to,
amount
});
}
fn build_authorization_message(
&self,
typehash: [u8; 32],
from: Address,
to: Address,
amount: &U256,
valid_after: u64,
valid_before: u64,
nonce: &[u8]
) -> Bytes {
let mut value_bytes = [0u8; 32];
amount.to_big_endian(&mut value_bytes);
let mut nonce_padded = [0u8; 32];
let len = nonce.len().min(32);
nonce_padded[..len].copy_from_slice(&nonce[..len]);
let mut encoded_data = Vec::with_capacity(6 * 32);
encoded_data.extend(eip712::encode_address(from));
encoded_data.extend(eip712::encode_address(to));
encoded_data.extend(casper_eip_712::encode_uint256(value_bytes));
encoded_data.extend(casper_eip_712::encode_uint64(valid_after));
encoded_data.extend(casper_eip_712::encode_uint64(valid_before));
encoded_data.extend(casper_eip_712::encode_bytes32(nonce_padded));
let domain = self.domain_separator();
Bytes::from(crate::eip712::hash_typed_data(domain, typehash, encoded_data).to_vec())
}
fn build_cancel_message(&self, authorizer: Address, nonce: &[u8]) -> Bytes {
let mut nonce_padded = [0u8; 32];
let len = nonce.len().min(32);
nonce_padded[..len].copy_from_slice(&nonce[..len]);
let mut encoded_data = Vec::with_capacity(64);
encoded_data.extend(eip712::encode_address(authorizer));
encoded_data.extend(casper_eip_712::encode_bytes32(nonce_padded));
let domain = self.domain_separator();
Bytes::from(
eip712::hash_typed_data(domain, CANCEL_AUTHORIZATION_TYPEHASH, encoded_data).to_vec()
)
}
fn domain_separator(&self) -> DomainSeparator {
let self_address = self.env().self_address();
let name = self.token.name();
let chain_name = self.chain_name.get();
eip712::domain_separator(&name, DOMAIN_VERSION, chain_name, self_address)
}
}
#[odra::module]
pub struct CEP3009Wrapper {
cep3009: SubModule<CEP3009>,
token: SubModule<Cep18>
}
#[odra::module]
impl CEP3009Wrapper {
pub fn init(
&mut self,
chain_name: String,
symbol: String,
name: String,
decimals: u8,
initial_supply: U256
) {
self.cep3009.init(chain_name);
self.token.init(symbol, name, decimals, initial_supply);
}
delegate! {
to self.cep3009 {
fn authorization_state(&self, authorizer: Address, nonce: Bytes) -> bool;
fn transfer_with_authorization(
&mut self,
from: Address,
to: Address,
amount: U256,
valid_after: u64,
valid_before: u64,
nonce: Bytes,
public_key: PublicKey,
signature: Bytes
);
fn receive_with_authorization(
&mut self,
from: Address,
to: Address,
amount: U256,
valid_after: u64,
valid_before: u64,
nonce: Bytes,
public_key: PublicKey,
signature: Bytes
);
fn cancel_authorization(
&mut self,
authorizer: Address,
nonce: Bytes,
public_key: PublicKey,
signature: Bytes
);
}
to self.token {
fn balance_of(&self, address: &Address) -> U256;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use odra::host::{Deployer, HostEnv};
const TOKEN_NAME: &str = "Test Token";
const TOKEN_SYMBOL: &str = "TEST";
const TOKEN_DECIMALS: u8 = 8;
const INITIAL_SUPPLY: u64 = 1_000_000;
const CHAIN_NAME: &str = "casper:casper";
struct Setup {
env: HostEnv,
wrapper: CEP3009WrapperHostRef,
alice: Address,
bob: Address,
charlie: Address,
alice_pubkey: PublicKey
}
fn setup() -> Setup {
let env = odra_test::env();
let alice = env.get_account(0);
let bob = env.get_account(1);
let charlie = env.get_account(2);
let alice_pubkey = env.public_key(&alice);
let wrapper = CEP3009Wrapper::deploy(
&env,
CEP3009WrapperInitArgs {
chain_name: CHAIN_NAME.to_string(),
symbol: TOKEN_SYMBOL.to_string(),
name: TOKEN_NAME.to_string(),
decimals: TOKEN_DECIMALS,
initial_supply: INITIAL_SUPPLY.into()
}
);
env.advance_block_time(1_000);
Setup {
env,
wrapper,
alice,
bob,
charlie,
alice_pubkey
}
}
fn fresh_nonce(seed: u8) -> Bytes {
Bytes::from(vec![seed; 32])
}
#[test]
fn transfer_with_authorization_happy_path() {
let Setup {
env,
mut wrapper,
alice,
bob,
charlie,
alice_pubkey
} = setup();
assert_eq!(wrapper.balance_of(&alice), INITIAL_SUPPLY.into());
assert_eq!(wrapper.balance_of(&bob), U256::zero());
let amount: U256 = 100u64.into();
let valid_after: u64 = 0;
let valid_before: u64 = u64::MAX;
let nonce = fresh_nonce(1);
let signature = sign_transfer_authorization(
&env,
&alice,
wrapper.address(),
TRANSFER_WITH_AUTHORIZATION_TYPEHASH,
alice,
bob,
amount,
valid_after,
valid_before,
&nonce
);
env.set_caller(charlie);
wrapper.transfer_with_authorization(
alice,
bob,
amount,
valid_after,
valid_before,
nonce.clone(),
alice_pubkey,
signature
);
assert_eq!(
wrapper.balance_of(&alice),
U256::from(INITIAL_SUPPLY) - amount
);
assert_eq!(wrapper.balance_of(&bob), amount);
assert!(wrapper.authorization_state(alice, nonce.clone()));
assert!(env.emitted_event(
&wrapper,
AuthorizationUsed {
authorizer: alice,
nonce
}
));
}
#[test]
fn transfer_with_authorization_replay_protection() {
let Setup {
env,
mut wrapper,
alice,
bob,
charlie,
alice_pubkey
} = setup();
let amount: U256 = 100u64.into();
let valid_after: u64 = 0;
let valid_before: u64 = u64::MAX;
let nonce = fresh_nonce(2);
let signature = sign_transfer_authorization(
&env,
&alice,
wrapper.address(),
TRANSFER_WITH_AUTHORIZATION_TYPEHASH,
alice,
bob,
amount,
valid_after,
valid_before,
&nonce
);
env.set_caller(charlie);
wrapper.transfer_with_authorization(
alice,
bob,
amount,
valid_after,
valid_before,
nonce.clone(),
alice_pubkey.clone(),
signature.clone()
);
assert_eq!(
wrapper.try_transfer_with_authorization(
alice,
bob,
amount,
valid_after,
valid_before,
nonce,
alice_pubkey,
signature
),
Err(Error::NonceAlreadyUsed.into())
);
}
#[test]
fn transfer_with_authorization_not_yet_valid() {
let Setup {
env,
mut wrapper,
alice,
bob,
charlie,
alice_pubkey
} = setup();
let valid_after: u64 = 60;
let valid_before: u64 = u64::MAX;
let amount: U256 = 100u64.into();
let nonce = fresh_nonce(3);
let signature = sign_transfer_authorization(
&env,
&alice,
wrapper.address(),
TRANSFER_WITH_AUTHORIZATION_TYPEHASH,
alice,
bob,
amount,
valid_after,
valid_before,
&nonce
);
env.set_caller(charlie);
assert_eq!(
wrapper.try_transfer_with_authorization(
alice,
bob,
amount,
valid_after,
valid_before,
nonce,
alice_pubkey,
signature
),
Err(Error::AuthorizationNotYetValid.into())
);
}
#[test]
fn transfer_with_authorization_expired() {
let Setup {
env,
mut wrapper,
alice,
bob,
charlie,
alice_pubkey
} = setup();
env.advance_block_time(60_000);
let valid_after: u64 = 0;
let valid_before: u64 = 10;
let amount: U256 = 100u64.into();
let nonce = fresh_nonce(4);
let signature = sign_transfer_authorization(
&env,
&alice,
wrapper.address(),
TRANSFER_WITH_AUTHORIZATION_TYPEHASH,
alice,
bob,
amount,
valid_after,
valid_before,
&nonce
);
env.set_caller(charlie);
assert_eq!(
wrapper.try_transfer_with_authorization(
alice,
bob,
amount,
valid_after,
valid_before,
nonce,
alice_pubkey,
signature
),
Err(Error::AuthorizationExpired.into())
);
}
#[test]
fn transfer_with_authorization_invalid_signature() {
let Setup {
env,
mut wrapper,
alice,
bob,
charlie,
alice_pubkey
} = setup();
let valid_after: u64 = 0;
let valid_before: u64 = u64::MAX;
let nonce = fresh_nonce(5);
let signature = sign_transfer_authorization(
&env,
&alice,
wrapper.address(),
TRANSFER_WITH_AUTHORIZATION_TYPEHASH,
alice,
bob,
U256::from(100u64),
valid_after,
valid_before,
&nonce
);
env.set_caller(charlie);
assert_eq!(
wrapper.try_transfer_with_authorization(
alice,
bob,
U256::from(200u64),
valid_after,
valid_before,
nonce,
alice_pubkey,
signature
),
Err(Error::InvalidSignature.into())
);
}
#[test]
fn transfer_with_authorization_public_key_mismatch() {
let Setup {
env,
mut wrapper,
alice,
bob,
charlie,
..
} = setup();
let valid_after: u64 = 0;
let valid_before: u64 = u64::MAX;
let amount: U256 = 100u64.into();
let nonce = fresh_nonce(6);
let bob_pubkey = env.public_key(&bob);
let signature = sign_transfer_authorization(
&env,
&bob,
wrapper.address(),
TRANSFER_WITH_AUTHORIZATION_TYPEHASH,
alice,
bob,
amount,
valid_after,
valid_before,
&nonce
);
env.set_caller(charlie);
assert_eq!(
wrapper.try_transfer_with_authorization(
alice,
bob,
amount,
valid_after,
valid_before,
nonce,
bob_pubkey,
signature
),
Err(Error::InvalidPublicKey.into())
);
}
#[test]
fn receive_with_authorization_happy_path() {
let Setup {
env,
mut wrapper,
alice,
bob,
alice_pubkey,
..
} = setup();
let amount: U256 = 250u64.into();
let valid_after: u64 = 0;
let valid_before: u64 = u64::MAX;
let nonce = fresh_nonce(7);
let signature = sign_transfer_authorization(
&env,
&alice,
wrapper.address(),
RECEIVE_WITH_AUTHORIZATION_TYPEHASH,
alice,
bob,
amount,
valid_after,
valid_before,
&nonce
);
env.set_caller(bob);
wrapper.receive_with_authorization(
alice,
bob,
amount,
valid_after,
valid_before,
nonce.clone(),
alice_pubkey,
signature
);
assert_eq!(
wrapper.balance_of(&alice),
U256::from(INITIAL_SUPPLY) - amount
);
assert_eq!(wrapper.balance_of(&bob), amount);
assert!(wrapper.authorization_state(alice, nonce));
}
#[test]
fn receive_with_authorization_wrong_caller_reverts() {
let Setup {
env,
mut wrapper,
alice,
bob,
charlie,
alice_pubkey
} = setup();
let amount: U256 = 250u64.into();
let valid_after: u64 = 0;
let valid_before: u64 = u64::MAX;
let nonce = fresh_nonce(8);
let signature = sign_transfer_authorization(
&env,
&alice,
wrapper.address(),
RECEIVE_WITH_AUTHORIZATION_TYPEHASH,
alice,
bob,
amount,
valid_after,
valid_before,
&nonce
);
env.set_caller(charlie);
assert_eq!(
wrapper.try_receive_with_authorization(
alice,
bob,
amount,
valid_after,
valid_before,
nonce,
alice_pubkey,
signature
),
Err(Error::InvalidCaller.into())
);
}
#[test]
fn cancel_authorization_happy_path() {
let Setup {
env,
mut wrapper,
alice,
bob,
charlie,
alice_pubkey
} = setup();
let nonce = fresh_nonce(9);
let cancel_signature =
sign_cancel_authorization(&env, &alice, wrapper.address(), alice, &nonce);
env.set_caller(charlie);
wrapper.cancel_authorization(alice, nonce.clone(), alice_pubkey.clone(), cancel_signature);
assert!(wrapper.authorization_state(alice, nonce.clone()));
assert!(env.emitted_event(
&wrapper,
AuthorizationCanceled {
authorizer: alice,
nonce: nonce.clone()
}
));
let amount: U256 = 1u64.into();
let valid_after: u64 = 0;
let valid_before: u64 = u64::MAX;
let transfer_signature = sign_transfer_authorization(
&env,
&alice,
wrapper.address(),
TRANSFER_WITH_AUTHORIZATION_TYPEHASH,
alice,
bob,
amount,
valid_after,
valid_before,
&nonce
);
assert_eq!(
wrapper.try_transfer_with_authorization(
alice,
bob,
amount,
valid_after,
valid_before,
nonce,
alice_pubkey,
transfer_signature
),
Err(Error::NonceAlreadyUsed.into())
);
}
#[test]
fn cancel_authorization_already_used_reverts() {
let Setup {
env,
mut wrapper,
alice,
bob,
charlie,
alice_pubkey
} = setup();
let amount: U256 = 100u64.into();
let valid_after: u64 = 0;
let valid_before: u64 = u64::MAX;
let nonce = fresh_nonce(10);
let transfer_signature = sign_transfer_authorization(
&env,
&alice,
wrapper.address(),
TRANSFER_WITH_AUTHORIZATION_TYPEHASH,
alice,
bob,
amount,
valid_after,
valid_before,
&nonce
);
env.set_caller(charlie);
wrapper.transfer_with_authorization(
alice,
bob,
amount,
valid_after,
valid_before,
nonce.clone(),
alice_pubkey.clone(),
transfer_signature
);
let cancel_signature =
sign_cancel_authorization(&env, &alice, wrapper.address(), alice, &nonce);
assert_eq!(
wrapper.try_cancel_authorization(alice, nonce, alice_pubkey, cancel_signature),
Err(Error::AuthorizationUsed.into())
);
}
#[test]
fn cancel_authorization_with_mismatched_public_key_reverts() {
let Setup {
env,
mut wrapper,
alice,
bob,
charlie,
..
} = setup();
let nonce = fresh_nonce(11);
let bob_pubkey = env.public_key(&bob);
let cancel_signature =
sign_cancel_authorization(&env, &bob, wrapper.address(), alice, &nonce);
env.set_caller(charlie);
assert_eq!(
wrapper.try_cancel_authorization(alice, nonce.clone(), bob_pubkey, cancel_signature),
Err(Error::InvalidPublicKey.into())
);
assert!(!wrapper.authorization_state(alice, nonce));
}
fn sign_transfer_authorization(
env: &HostEnv,
signer: &Address,
contract_address: Address,
typehash: [u8; 32],
from: Address,
to: Address,
amount: U256,
valid_after: u64,
valid_before: u64,
nonce: &Bytes
) -> Bytes {
let mut value_bytes = [0u8; 32];
amount.to_big_endian(&mut value_bytes);
let mut nonce_padded = [0u8; 32];
let len = nonce.len().min(32);
nonce_padded[..len].copy_from_slice(&nonce[..len]);
let mut encoded_data = Vec::with_capacity(6 * 32);
encoded_data.extend(crate::eip712::encode_address(from));
encoded_data.extend(crate::eip712::encode_address(to));
encoded_data.extend(casper_eip_712::encode_uint256(value_bytes));
encoded_data.extend(casper_eip_712::encode_uint64(valid_after));
encoded_data.extend(casper_eip_712::encode_uint64(valid_before));
encoded_data.extend(casper_eip_712::encode_bytes32(nonce_padded));
let domain = crate::eip712::domain_separator(
TOKEN_NAME,
DOMAIN_VERSION,
CHAIN_NAME.to_string(),
contract_address
);
let message_hash = crate::eip712::hash_typed_data(domain, typehash, encoded_data);
let message = Bytes::from(message_hash.to_vec());
env.sign_message(&message, signer)
}
fn sign_cancel_authorization(
env: &HostEnv,
signer: &Address,
contract_address: Address,
authorizer: Address,
nonce: &Bytes
) -> Bytes {
let mut nonce_padded = [0u8; 32];
let len = nonce.len().min(32);
nonce_padded[..len].copy_from_slice(&nonce[..len]);
let mut encoded_data = Vec::with_capacity(64);
encoded_data.extend(crate::eip712::encode_address(authorizer));
encoded_data.extend(casper_eip_712::encode_bytes32(nonce_padded));
let domain = crate::eip712::domain_separator(
TOKEN_NAME,
DOMAIN_VERSION,
CHAIN_NAME.to_string(),
contract_address
);
let message_hash =
crate::eip712::hash_typed_data(domain, CANCEL_AUTHORIZATION_TYPEHASH, encoded_data);
let message = Bytes::from(message_hash.to_vec());
env.sign_message(&message, signer)
}
}