diff --git a/Cargo.lock b/Cargo.lock index d8294d75c..c439676f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2550,6 +2550,7 @@ version = "0.1.0-alpha.1" dependencies = [ "alloy-primitives 0.3.3", "alloy-sol-types 0.3.1", + "keccak-const", "mini-alloc", "motsu", "rand", diff --git a/Cargo.toml b/Cargo.toml index 7ec8849e5..33a5bf014 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,6 +78,7 @@ alloy-sol-types = { version = "0.3.1", default-features = false } const-hex = { version = "1.11.1", default-features = false } eyre = "0.6.8" +keccak-const = "0.2.0" koba = "0.1.2" once_cell = "1.19.0" rand = "0.8.5" diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index 4171fbb6b..c0ed507e8 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -14,6 +14,7 @@ alloy-sol-types.workspace = true stylus-sdk.workspace = true stylus-proc.workspace = true mini-alloc.workspace = true +keccak-const.workspace = true [dev-dependencies] alloy-primitives = { workspace = true, features = ["arbitrary"] } diff --git a/contracts/src/utils/cryptography/eip712.rs b/contracts/src/utils/cryptography/eip712.rs new file mode 100644 index 000000000..e20b3d7de --- /dev/null +++ b/contracts/src/utils/cryptography/eip712.rs @@ -0,0 +1,152 @@ +//! [EIP-712](https://eips.ethereum.org/EIPS/eip-712) is a standard for hashing +//! and signing typed structured data. +//! +//! The implementation of the domain separator was designed to be as efficient +//! as possible while still properly updating the chain id to protect against +//! replay attacks on an eventual fork of the chain. +//! +//! NOTE: This contract implements the version of the encoding known as "v4", as +//! implemented by the JSON RPC method [`eth_signTypedDataV4`] in `MetaMask`. +//! +//! [`eth_signTypedDataV4`]: https://docs.metamask.io/guide/signing-data.html + +use alloc::{borrow::ToOwned, string::String, vec::Vec}; + +use alloy_primitives::{keccak256, Address, FixedBytes, U256}; +use alloy_sol_types::{sol, SolType}; + +use crate::utils::cryptography::message_hash_utils::to_typed_data_hash; + +/// keccak256("EIP712Domain(string name,string version,uint256 chainId,address +/// verifyingContract)") +pub const TYPE_HASH: [u8; 32] = [ + 0x8b, 0x73, 0xc3, 0xc6, 0x9b, 0xb8, 0xfe, 0x3d, 0x51, 0x2e, 0xcc, 0x4c, + 0xf7, 0x59, 0xcc, 0x79, 0x23, 0x9f, 0x7b, 0x17, 0x9b, 0x0f, 0xfa, 0xca, + 0xa9, 0xa7, 0x5d, 0x52, 0x2b, 0x39, 0x40, 0x0f, +]; + +/// Field for the domain separator. +pub const FIELDS: [u8; 1] = [0x0f]; + +/// Salt for the domain separator. +pub const SALT: [u8; 32] = [0u8; 32]; + +/// Tuple for the domain separator. +pub type DomainSeparatorTuple = sol! { + tuple(bytes32, bytes32, bytes32, uint256, address) +}; + +/// EIP-712 Contract interface. +pub trait IEIP712 { + /// Immutable name of EIP-712 instance. + const NAME: &'static str; + /// Hashed name of EIP-712 instance. + const HASHED_NAME: [u8; 32] = + keccak_const::Keccak256::new().update(Self::NAME.as_bytes()).finalize(); + + /// Immutable version of EIP-712 instance. + const VERSION: &'static str; + /// Hashed version of EIP-712 instance. + const HASHED_VERSION: [u8; 32] = keccak_const::Keccak256::new() + .update(Self::VERSION.as_bytes()) + .finalize(); + + /// Returns chain id. + fn chain_id() -> U256; + /// Returns the contract's address. + fn contract_address() -> Address; + + /// Returns the fields and values that describe the domain separator used by + /// this contract for EIP-712 signature. + /// + /// # Arguments + /// + /// * `&self` - Read access to the contract's state. + fn eip712_domain( + &self, + ) -> ([u8; 1], String, String, U256, Address, [u8; 32], Vec) { + ( + FIELDS, + Self::NAME.to_owned(), + Self::VERSION.to_owned(), + Self::chain_id(), + Self::contract_address(), + SALT, + Vec::new(), + ) + } + + /// Returns the domain separator for the current chain. + /// + /// # Arguments + /// + /// * `&self` - Read access to the contract's state. + fn domain_separator_v4(&self) -> FixedBytes<32> { + let encoded = DomainSeparatorTuple::encode(&( + TYPE_HASH, + Self::HASHED_NAME, + Self::HASHED_VERSION, + Self::chain_id(), + Self::contract_address().into(), + )); + + keccak256(encoded) + } + + /// Given an already [hashed struct], this function returns the hash of the + /// fully encoded EIP-712 message for this domain. + /// + /// [hashed struct]: https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct + /// + /// # Arguments + /// + /// * `&self` - Read access to the contract's state. + fn hash_typed_data_v4( + &self, + struct_hash: FixedBytes<32>, + ) -> FixedBytes<32> { + let domain_separator = self.domain_separator_v4(); + to_typed_data_hash(&domain_separator, &struct_hash) + } +} + +#[cfg(all(test, feature = "std"))] +mod tests { + use alloy_primitives::{address, uint, Address, U256}; + + use super::{FIELDS, IEIP712, SALT}; + + const CHAIN_ID: U256 = uint!(42161_U256); + + const CONTRACT_ADDRESS: Address = + address!("000000000000000000000000000000000000dEaD"); + + #[derive(Default)] + struct TestEIP712 {} + + impl IEIP712 for TestEIP712 { + const NAME: &'static str = "A Name"; + const VERSION: &'static str = "1"; + + fn chain_id() -> U256 { + CHAIN_ID + } + + fn contract_address() -> Address { + CONTRACT_ADDRESS + } + } + + #[test] + fn domain_test() { + let contract = TestEIP712::default(); + let domain = contract.eip712_domain(); + assert_eq!(FIELDS, domain.0); + assert_eq!(TestEIP712::NAME, domain.1); + assert_eq!(TestEIP712::VERSION, domain.2); + assert_eq!(CHAIN_ID, domain.3); + assert_eq!(CONTRACT_ADDRESS, domain.4); + assert_eq!(SALT, domain.5); + assert_eq!(Vec::::new(), domain.6); + } +} diff --git a/contracts/src/utils/cryptography/message_hash_utils.rs b/contracts/src/utils/cryptography/message_hash_utils.rs new file mode 100644 index 000000000..70eba1965 --- /dev/null +++ b/contracts/src/utils/cryptography/message_hash_utils.rs @@ -0,0 +1,60 @@ +//! Signature message hash utilities for producing digests to be consumed by +//! `ECDSA` recovery or signing. +//! +//! The library provides methods for generating a hash of a message that +//! conforms to the [EIP 712] specification. +//! +//! [EIP-712]: https://eips.ethereum.org/EIPS/eip-712 + +use alloy_primitives::{keccak256, FixedBytes}; + +/// Prefix for ERC-191 version with `0x01`. +pub const TYPED_DATA_PREFIX: [u8; 2] = [0x19, 0x01]; + +/// Returns the keccak256 digest of an EIP-712 typed data (ERC-191 version +/// `0x01`). +/// +/// The digest is calculated from a `domain_separator` and a `struct_hash`, by +/// prefixing them with `[TYPED_DATA_PREFIX]` and hashing the result. It +/// corresponds to the hash signed by the [eth_signTypedData] JSON-RPC method as +/// part of EIP-712. +/// +/// [eth_signTypedData]: https://eips.ethereum.org/EIPS/eip-712 +#[must_use] +pub fn to_typed_data_hash( + domain_separator: &[u8; 32], + struct_hash: &[u8; 32], +) -> FixedBytes<32> { + let mut preimage = [0u8; 66]; + preimage[..2].copy_from_slice(&TYPED_DATA_PREFIX); + preimage[2..34].copy_from_slice(domain_separator); + preimage[34..].copy_from_slice(struct_hash); + keccak256(preimage) +} + +#[cfg(all(test, feature = "std"))] +mod tests { + use alloy_primitives::b256; + + use super::to_typed_data_hash; + + #[test] + fn test_to_typed_data_hash() { + // TYPE_HASH + let domain_separator = b256!( + "8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f" + ); + // bytes32("stylus"); + let struct_hash = b256!( + "7379746c75730000000000000000000000000000000000000000000000000000" + ); + let expected = b256!( + "cefc47137f8165d8270433dd62e395f5672966b83a113a7bb7b2805730a2197e" + ); + + assert_eq!( + expected, + to_typed_data_hash(&domain_separator, &struct_hash), + ); + } +} diff --git a/contracts/src/utils/cryptography/mod.rs b/contracts/src/utils/cryptography/mod.rs index a92e1f427..0f26f7dcb 100644 --- a/contracts/src/utils/cryptography/mod.rs +++ b/contracts/src/utils/cryptography/mod.rs @@ -1,2 +1,4 @@ //! Smart Contracts with cryptography. pub mod ecdsa; +pub mod eip712; +pub mod message_hash_utils; diff --git a/lib/motsu/src/shims.rs b/lib/motsu/src/shims.rs index 99302196f..f4460609e 100644 --- a/lib/motsu/src/shims.rs +++ b/lib/motsu/src/shims.rs @@ -157,6 +157,13 @@ pub fn storage_flush_cache(_: bool) { /// Dummy msg sender set for tests. pub const MSG_SENDER: &[u8; 42] = b"0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF"; +/// Dummy contract address set for tests. +pub const CONTRACT_ADDRESS: &[u8; 42] = + b"0xdCE82b5f92C98F27F116F70491a487EFFDb6a2a9"; + +/// Arbitrum's CHAID ID. +pub const CHAIN_ID: u64 = 42161; + /// Externally Owned Account (EOA) code hash. pub const EOA_CODEHASH: &[u8; 66] = b"0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"; @@ -182,6 +189,30 @@ pub unsafe extern "C" fn msg_sender(sender: *mut u8) { std::ptr::copy(addr.as_ptr(), sender, 20); } +/// Gets the address of the current program. The semantics are equivalent to +/// that of the EVM's [`ADDRESS`] opcode. +/// +/// [`ADDRESS`]: https://www.evm.codes/#30 +/// +/// # Panics +/// +/// May panic if fails to parse `CONTRACT_ADDRESS` as an address. +#[no_mangle] +pub unsafe extern "C" fn contract_address(address: *mut u8) { + let addr = + const_hex::const_decode_to_array::<20>(CONTRACT_ADDRESS).unwrap(); + std::ptr::copy(addr.as_ptr(), address, 20); +} + +/// Gets the chain ID of the current chain. The semantics are equivalent to +/// that of the EVM's [`CHAINID`] opcode. +/// +/// [`CHAINID`]: https://www.evm.codes/#46 +#[no_mangle] +pub unsafe extern "C" fn chainid() -> u64 { + CHAIN_ID +} + /// Emits an EVM log with the given number of topics and data, the first bytes /// of which should be the 32-byte-aligned topic data. The semantics are /// equivalent to that of the EVM's [`LOG0`], [`LOG1`], [`LOG2`], [`LOG3`], and