#![allow(missing_docs)]
use casper_eip_712::DomainSeparator;
use odra::{
casper_types::{bytesrepr::Bytes, PublicKey, U256},
named_keys::{base64_encoded_key_value_storage, single_value_storage},
prelude::*
};
use crate::{cep18_token::Cep18, eip712};
const PERMIT_TYPEHASH: [u8; 32] = [
0x6e, 0x71, 0xed, 0xae, 0x12, 0xb1, 0xb9, 0x7f, 0x4d, 0x1f, 0x60, 0x37, 0x0f, 0xef, 0x10, 0x10,
0x5f, 0xa2, 0xfa, 0xae, 0x01, 0x26, 0x11, 0x4a, 0x16, 0x9c, 0x64, 0x84, 0x5d, 0x61, 0x26, 0xc9
];
#[odra::odra_error]
pub enum Error {
InvalidSignature = 36_000,
PermitExpired = 36_001,
InvalidPublicKey = 36_002
}
const CHAIN_NAME_KEY: &str = "chain_name";
const PERMIT_NONCES_KEY: &str = "permit_nonces";
const DOMAIN_VERSION: &str = "1";
single_value_storage!(
CEP2612ChainNameStorage,
String,
CHAIN_NAME_KEY,
ExecutionError::KeyNotFound
);
base64_encoded_key_value_storage!(CEP2612PermitNoncesStorage, PERMIT_NONCES_KEY, Address, U256);
#[odra::module]
pub struct CEP2612 {
permit_nonces: SubModule<CEP2612PermitNoncesStorage>,
chain_name: SubModule<CEP2612ChainNameStorage>,
token: SubModule<Cep18>
}
#[odra::module]
impl CEP2612 {
pub fn init(&mut self, chain_name: String) {
self.chain_name.set(chain_name);
self.permit_nonces.init();
}
pub fn permit(
&mut self,
owner: Address,
spender: Address,
value: U256,
deadline: u64,
public_key: PublicKey,
signature: Bytes
) {
if deadline != u64::MAX && self.env().get_block_time_secs() > deadline {
self.revert(Error::PermitExpired);
}
if Address::from(public_key.clone()) != owner {
self.revert(Error::InvalidPublicKey);
}
let nonce = self.permit_nonces.get(&owner).unwrap_or_default();
let message_hash = self.message_hash(owner, spender, value, nonce, deadline);
let message = Bytes::from(message_hash.to_vec());
let is_valid = self
.env()
.verify_signature(&message, &signature, &public_key);
if !is_valid {
self.revert(Error::InvalidSignature);
}
self.permit_nonces.set(&owner, nonce + U256::from(1));
self.token.raw_approve(&owner, &spender, &value);
}
fn domain_separator(&self) -> DomainSeparator {
let self_address = self.env().self_address();
let name = self.token.name();
let chain_name = self.chain_name.get();
crate::eip712::domain_separator(&name, DOMAIN_VERSION, chain_name, self_address)
}
fn message_hash(
&self,
owner: Address,
spender: Address,
value: U256,
nonce: U256,
deadline: u64
) -> [u8; 32] {
let mut encoded_data = Vec::with_capacity(32 * 5);
let mut value_bytes = [0u8; 32];
value.to_big_endian(&mut value_bytes);
let mut nonce_bytes = [0u8; 32];
nonce.to_big_endian(&mut nonce_bytes);
encoded_data.extend(eip712::encode_address(owner));
encoded_data.extend(eip712::encode_address(spender));
encoded_data.extend(casper_eip_712::encode_uint256(value_bytes));
encoded_data.extend(casper_eip_712::encode_uint256(nonce_bytes));
encoded_data.extend(casper_eip_712::encode_uint64(deadline));
let domain = self.domain_separator();
crate::eip712::hash_typed_data(domain, PERMIT_TYPEHASH, encoded_data)
}
}
#[odra::module]
pub struct CEP2612Wrapper {
cep2612: SubModule<CEP2612>,
token: SubModule<Cep18>
}
#[odra::module]
impl CEP2612Wrapper {
pub fn init(
&mut self,
chain_name: String,
symbol: String,
name: String,
decimals: u8,
initial_supply: U256
) {
self.cep2612.init(chain_name);
self.token.init(symbol, name, decimals, initial_supply);
}
delegate! {
to self.cep2612 {
fn permit(
&mut self,
owner: Address,
spender: Address,
value: U256,
deadline: u64,
public_key: PublicKey,
signature: Bytes
);
}
to self.token {
fn allowance(&self, owner: &Address, spender: &Address) -> U256;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cep18::events::SetAllowance;
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-test";
struct Setup {
env: HostEnv,
wrapper: CEP2612WrapperHostRef,
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 = CEP2612Wrapper::deploy(
&env,
CEP2612WrapperInitArgs {
chain_name: CHAIN_NAME.to_string(),
symbol: TOKEN_SYMBOL.to_string(),
name: TOKEN_NAME.to_string(),
decimals: TOKEN_DECIMALS,
initial_supply: INITIAL_SUPPLY.into()
}
);
Setup {
env,
wrapper,
alice,
bob,
charlie,
alice_pubkey
}
}
#[test]
fn permit_happy_path() {
let Setup {
env,
mut wrapper,
alice,
bob,
charlie,
alice_pubkey
} = setup();
assert_eq!(wrapper.allowance(&alice, &bob), U256::zero());
let value: U256 = 500u64.into();
let deadline: u64 = u64::MAX;
let nonce: U256 = U256::zero();
let signature = sign_permit(
&env,
&alice,
wrapper.address(),
alice,
bob,
value,
nonce,
deadline
);
env.set_caller(charlie);
wrapper.permit(alice, bob, value, deadline, alice_pubkey, signature);
assert_eq!(wrapper.allowance(&alice, &bob), value);
assert!(env.emitted_event(
&wrapper,
SetAllowance {
owner: alice,
spender: bob,
allowance: value
}
));
}
#[test]
fn permit_replay_reverts() {
let Setup {
env,
mut wrapper,
alice,
bob,
charlie,
alice_pubkey
} = setup();
let value: U256 = 500u64.into();
let deadline: u64 = u64::MAX;
let nonce: U256 = U256::zero();
let signature = sign_permit(
&env,
&alice,
wrapper.address(),
alice,
bob,
value,
nonce,
deadline
);
env.set_caller(charlie);
wrapper.permit(
alice,
bob,
value,
deadline,
alice_pubkey.clone(),
signature.clone()
);
assert_eq!(
wrapper.try_permit(alice, bob, value, deadline, alice_pubkey, signature),
Err(Error::InvalidSignature.into())
);
}
#[test]
fn permit_subsequent_with_next_nonce_succeeds() {
let Setup {
env,
mut wrapper,
alice,
bob,
charlie,
alice_pubkey
} = setup();
let deadline: u64 = u64::MAX;
let first_value: U256 = 500u64.into();
let second_value: U256 = 1_000u64.into();
let first_signature = sign_permit(
&env,
&alice,
wrapper.address(),
alice,
bob,
first_value,
U256::zero(),
deadline
);
let second_signature = sign_permit(
&env,
&alice,
wrapper.address(),
alice,
bob,
second_value,
U256::one(),
deadline
);
env.set_caller(charlie);
wrapper.permit(
alice,
bob,
first_value,
deadline,
alice_pubkey.clone(),
first_signature
);
assert_eq!(wrapper.allowance(&alice, &bob), first_value);
wrapper.permit(
alice,
bob,
second_value,
deadline,
alice_pubkey,
second_signature
);
assert_eq!(wrapper.allowance(&alice, &bob), second_value);
}
#[test]
fn permit_expired() {
let Setup {
env,
mut wrapper,
alice,
bob,
charlie,
alice_pubkey
} = setup();
let value: U256 = 500u64.into();
let deadline: u64 = 1_700_000_000;
let nonce: U256 = U256::zero();
env.advance_block_time((deadline + 1) * 1_000);
let signature = sign_permit(
&env,
&alice,
wrapper.address(),
alice,
bob,
value,
nonce,
deadline
);
env.set_caller(charlie);
assert_eq!(
wrapper.try_permit(alice, bob, value, deadline, alice_pubkey, signature),
Err(Error::PermitExpired.into())
);
}
#[test]
fn permit_max_deadline_skips_expiry_check() {
let Setup {
env,
mut wrapper,
alice,
bob,
charlie,
alice_pubkey
} = setup();
env.advance_block_time(1_000_000);
let value: U256 = 500u64.into();
let deadline: u64 = u64::MAX;
let nonce: U256 = U256::zero();
let signature = sign_permit(
&env,
&alice,
wrapper.address(),
alice,
bob,
value,
nonce,
deadline
);
env.set_caller(charlie);
wrapper.permit(alice, bob, value, deadline, alice_pubkey, signature);
assert_eq!(wrapper.allowance(&alice, &bob), value);
}
#[test]
fn permit_invalid_signature() {
let Setup {
env,
mut wrapper,
alice,
bob,
charlie,
alice_pubkey
} = setup();
let deadline: u64 = u64::MAX;
let nonce: U256 = U256::zero();
let signature = sign_permit(
&env,
&alice,
wrapper.address(),
alice,
bob,
U256::from(100u64),
nonce,
deadline
);
env.set_caller(charlie);
assert_eq!(
wrapper.try_permit(
alice,
bob,
U256::from(200u64),
deadline,
alice_pubkey,
signature
),
Err(Error::InvalidSignature.into())
);
}
#[test]
fn permit_with_mismatched_public_key_reverts() {
let Setup {
env,
mut wrapper,
alice,
bob,
charlie,
..
} = setup();
let attacker = env.get_account(3);
let attacker_pubkey = env.public_key(&attacker);
let value: U256 = 500u64.into();
let deadline: u64 = u64::MAX;
let nonce: U256 = U256::zero();
let signature = sign_permit(
&env,
&attacker,
wrapper.address(),
alice,
bob,
value,
nonce,
deadline
);
env.set_caller(charlie);
assert_eq!(
wrapper.try_permit(alice, bob, value, deadline, attacker_pubkey, signature),
Err(Error::InvalidPublicKey.into())
);
assert_eq!(wrapper.allowance(&alice, &bob), U256::zero());
}
#[allow(clippy::too_many_arguments)]
fn sign_permit(
env: &HostEnv,
signer: &Address,
contract_address: Address,
owner: Address,
spender: Address,
value: U256,
nonce: U256,
deadline: u64
) -> Bytes {
let mut value_bytes = [0u8; 32];
value.to_big_endian(&mut value_bytes);
let mut nonce_bytes = [0u8; 32];
nonce.to_big_endian(&mut nonce_bytes);
let mut encoded_data = Vec::with_capacity(32 * 5);
encoded_data.extend(crate::eip712::encode_address(owner));
encoded_data.extend(crate::eip712::encode_address(spender));
encoded_data.extend(casper_eip_712::encode_uint256(value_bytes));
encoded_data.extend(casper_eip_712::encode_uint256(nonce_bytes));
encoded_data.extend(casper_eip_712::encode_uint64(deadline));
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, PERMIT_TYPEHASH, encoded_data);
let message = Bytes::from(message_hash.to_vec());
env.sign_message(&message, signer)
}
}