diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index da7375eb..91d2efb2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @bidzyyys @qalisander +* @bidzyyys @qalisander @0xNeshi diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index b2ce1d6d..0420c5ec 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -127,4 +127,4 @@ jobs: uses: actions/checkout@v4 - name: Check spelling of files in the workspace - uses: crate-ci/typos@v1.27.0 + uses: crate-ci/typos@v1.27.2 diff --git a/CHANGELOG.md b/CHANGELOG.md index a3bfdb06..fd754d3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- +- `SafeErc20` Utility. #289 +- Finite Fields arithmetics. #376 ### Changed diff --git a/Cargo.lock b/Cargo.lock index e9cc2220..96f5a1ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1443,6 +1443,18 @@ dependencies = [ "spki", ] +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.68", +] + [[package]] name = "either" version = "1.13.0" @@ -1488,6 +1500,26 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "enum-ordinalize" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea0dcfa4e54eeb516fe454635a95753ddd39acda650ce703031c6973e315dd5" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.68", +] + [[package]] name = "enumset" version = "1.1.3" @@ -2410,7 +2442,7 @@ dependencies = [ [[package]] name = "motsu-proc" -version = "0.1.0" +version = "0.2.0" dependencies = [ "alloy-primitives", "alloy-sol-types", @@ -2560,9 +2592,14 @@ dependencies = [ name = "openzeppelin-crypto" version = "0.1.1" dependencies = [ + "crypto-bigint", + "educe", "hex-literal", + "num-traits", + "proptest", "rand", "tiny-keccak", + "zeroize", ] [[package]] @@ -3218,6 +3255,19 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "safe-erc20-example" +version = "0.1.1" +dependencies = [ + "alloy", + "alloy-primitives", + "e2e", + "eyre", + "openzeppelin-stylus", + "stylus-sdk", + "tokio", +] + [[package]] name = "salsa20" version = "0.10.2" diff --git a/Cargo.toml b/Cargo.toml index bef968da..dc99af34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,8 +18,9 @@ members = [ "examples/basic/token", "examples/basic/script", "examples/ecdsa", - "benches", "examples/ownable-two-step", + "examples/safe-erc20", + "benches", ] default-members = [ "contracts", @@ -33,6 +34,7 @@ default-members = [ "examples/erc721", "examples/erc721-consecutive", "examples/erc721-metadata", + "examples/safe-erc20", "examples/merkle-proofs", "examples/ownable", "examples/ownable-two-step", @@ -96,6 +98,12 @@ tiny-keccak = { version = "2.0.2", features = ["keccak"] } tokio = { version = "1.12.0", features = ["full"] } futures = "0.3.30" dashmap = "6.1.0" +crypto-bigint = { version = "0.5.5", default-features = false, features = ["zeroize"] } +num-traits = "0.2.14" +zeroize = { version = "1.8.1", features = ["derive"] } +proptest = "1" +educe = "0.6.0" +hex-literal = "0.4.1" # procedural macros syn = { version = "2.0.58", features = ["full"] } @@ -106,10 +114,10 @@ quote = "1.0.35" openzeppelin-stylus = { path = "contracts" } openzeppelin-stylus-proc = { path = "contracts-proc", version = "0.1.0" } openzeppelin-crypto = { path = "lib/crypto" } -motsu = { path = "lib/motsu"} -motsu-proc = { path = "lib/motsu-proc", version = "0.1.0" } +motsu = { path = "lib/motsu" } +motsu-proc = { path = "lib/motsu-proc", version = ">=0.1, <0.3" } e2e = { path = "lib/e2e" } -e2e-proc = {path = "lib/e2e-proc"} +e2e-proc = { path = "lib/e2e-proc" } [profile.release] codegen-units = 1 diff --git a/contracts/src/token/erc20/mod.rs b/contracts/src/token/erc20/mod.rs index 19fba1ea..91399f42 100644 --- a/contracts/src/token/erc20/mod.rs +++ b/contracts/src/token/erc20/mod.rs @@ -16,6 +16,7 @@ use stylus_sdk::{ use crate::utils::introspection::erc165::{Erc165, IErc165}; pub mod extensions; +pub mod utils; sol! { /// Emitted when `value` tokens are moved from one account (`from`) to diff --git a/contracts/src/token/erc20/utils/mod.rs b/contracts/src/token/erc20/utils/mod.rs new file mode 100644 index 00000000..955a3f63 --- /dev/null +++ b/contracts/src/token/erc20/utils/mod.rs @@ -0,0 +1,4 @@ +//! Utilities for the ERC-20 standard. +pub mod safe_erc20; + +pub use safe_erc20::SafeErc20; diff --git a/contracts/src/token/erc20/utils/safe_erc20.rs b/contracts/src/token/erc20/utils/safe_erc20.rs new file mode 100644 index 00000000..1669a69c --- /dev/null +++ b/contracts/src/token/erc20/utils/safe_erc20.rs @@ -0,0 +1,433 @@ +//! Wrappers around ERC-20 operations that throw on failure (when the token +//! contract returns false). Tokens that return no value (and instead revert or +//! throw on failure) are also supported, non-reverting calls are assumed to be +//! successful. +//! +//! To use this library, you can add a `#[inherit(SafeErc20)]` attribute to +//! your contract, which allows you to call the safe operations as +//! `contract.safe_transfer(token_addr, ...)`, etc. + +use alloy_primitives::{Address, U256}; +use alloy_sol_types::{sol, SolCall}; +use stylus_sdk::{ + call::RawCall, + contract::address, + evm::gas_left, + function_selector, + storage::TopLevelStorage, + stylus_proc::{public, sol_storage, SolidityError}, + types::AddressVM, +}; + +use crate::token::erc20; + +sol! { + /// An operation with an ERC-20 token failed. + /// + /// * `token` - Address of the ERC-20 token. + #[derive(Debug)] + #[allow(missing_docs)] + error SafeErc20FailedOperation(address token); + + /// Indicates a failed [`ISafeErc20::safe_decrease_allowance`] request. + /// + /// * `spender` - Address of future tokens' spender. + /// * `current_allowance` - Current allowance of the `spender`. + /// * `requested_decrease` - Requested decrease in allowance for `spender`. + #[derive(Debug)] + #[allow(missing_docs)] + error SafeErc20FailedDecreaseAllowance( + address spender, + uint256 current_allowance, + uint256 requested_decrease + ); +} + +/// A [`SafeErc20`] error. +#[derive(SolidityError, Debug)] +pub enum Error { + /// Error type from [`erc20::Erc20`] contract [`erc20::Error`]. + Erc20(erc20::Error), + /// An operation with an ERC-20 token failed. + SafeErc20FailedOperation(SafeErc20FailedOperation), + /// Indicates a failed [`ISafeErc20::safe_decrease_allowance`] request. + SafeErc20FailedDecreaseAllowance(SafeErc20FailedDecreaseAllowance), +} + +pub use token::*; +#[allow(missing_docs)] +mod token { + alloy_sol_types::sol! { + /// Interface of the ERC-20 token. + interface IErc20 { + function allowance(address owner, address spender) external view returns (uint256); + function approve(address spender, uint256 value) external returns (bool); + function transfer(address to, uint256 value) external returns (bool); + function transferFrom(address from, address to, uint256 value) external returns (bool); + } + } +} +sol_storage! { + /// State of the [`SafeErc20`] Contract. + pub struct SafeErc20 {} +} + +/// NOTE: Implementation of [`TopLevelStorage`] to be able use `&mut self` when +/// calling other contracts and not `&mut (impl TopLevelStorage + +/// BorrowMut)`. Should be fixed in the future by the Stylus team. +unsafe impl TopLevelStorage for SafeErc20 {} + +/// Required interface of an [`SafeErc20`] utility contract. +pub trait ISafeErc20 { + /// The error type associated to this trait implementation. + type Error: Into>; + + /// Transfer `value` amount of `token` from the calling contract to `to`. If + /// `token` returns no value, non-reverting calls are assumed to be + /// successful. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `token` - Address of the ERC-20 token contract. + /// * `to` - Account to transfer tokens to. + /// * `value` - Number of tokens to transfer. + /// + /// # Errors + /// + /// If the `token` address is not a contract, then the error + /// [`Error::SafeErc20FailedOperation`] is returned. + /// If the contract fails to execute the call, then the error + /// [`Error::SafeErc20FailedOperation`] is returned. + /// If the call returns value that is not `true`, then the error + /// [`Error::SafeErc20FailedOperation`] is returned. + fn safe_transfer( + &mut self, + token: Address, + to: Address, + value: U256, + ) -> Result<(), Self::Error>; + + /// Transfer `value` amount of `token` from `from` to `to`, spending the + /// approval given by `from` to the calling contract. If `token` returns + /// no value, non-reverting calls are assumed to be successful. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `token` - Address of the ERC-20 token contract. + /// * `from` - Account to transfer tokens from. + /// * `to` - Account to transfer tokens to. + /// * `value` - Number of tokens to transfer. + /// + /// # Errors + /// + /// If the `token` address is not a contract, then the error + /// [`Error::SafeErc20FailedOperation`] is returned. + /// If the contract fails to execute the call, then the error + /// [`Error::SafeErc20FailedOperation`] is returned. + /// If the call returns value that is not `true`, then the error + /// [`Error::SafeErc20FailedOperation`] is returned. + fn safe_transfer_from( + &mut self, + token: Address, + from: Address, + to: Address, + value: U256, + ) -> Result<(), Self::Error>; + + /// Increase the calling contract's allowance toward `spender` by `value`. + /// If `token` returns no value, non-reverting calls are assumed to be + /// successful. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `token` - Address of the ERC-20 token contract. + /// * `spender` - Account that will spend the tokens. + /// * `value` - Value to increase current allowance for `spender`. + /// + /// # Errors + /// + /// If the `token` address is not a contract, then the error + /// [`Error::SafeErc20FailedOperation`] is returned. + /// If the contract fails to execute the call, then the error + /// [`Error::SafeErc20FailedOperation`] is returned. + /// If the call returns value that is not `true`, then the error + /// [`Error::SafeErc20FailedOperation`] is returned. + /// + /// # Panics + /// + /// If increased allowance exceeds `U256::MAX`. + fn safe_increase_allowance( + &mut self, + token: Address, + spender: Address, + value: U256, + ) -> Result<(), Self::Error>; + + /// Decrease the calling contract's allowance toward `spender` by + /// `requested_decrease`. If `token` returns no value, non-reverting + /// calls are assumed to be successful. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `token` - Address of the ERC-20 token contract. + /// * `spender` - Account that will spend the tokens. + /// * `requested_decrease` - Value allowed to be spent by `spender`. + /// + /// # Errors + /// + /// If the `token` address is not a contract, then the error + /// [`Error::SafeErc20FailedOperation`] is returned. + /// If the current allowance is less than `requested_decrease`, then the + /// error [`Error::SafeErc20FailedDecreaseAllowance`] is returned. + /// If the contract fails to execute the call, then the error + /// [`Error::SafeErc20FailedOperation`] is returned. + /// If the call returns value that is not `true`, then the error + /// [`Error::SafeErc20FailedOperation`] is returned. + fn safe_decrease_allowance( + &mut self, + token: Address, + spender: Address, + requested_decrease: U256, + ) -> Result<(), Self::Error>; + + /// Set the calling contract's allowance toward `spender` to `value`. If + /// `token` returns no value, non-reverting calls are assumed to be + /// successful. Meant to be used with tokens that require the approval + /// to be set to zero before setting it to a non-zero value, such as USDT. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `token` - Address of the ERC-20 token contract. + /// * `spender` - Account that will spend the tokens. + /// * `value` - Value allowed to be spent by `spender`. + /// + /// # Errors + /// + /// If the `token` address is not a contract, then the error + /// [`Error::SafeErc20FailedOperation`] is returned. + /// If the contract fails to execute the call, then the error + /// [`Error::SafeErc20FailedOperation`] is returned. + /// If the call returns value that is not `true`, then the error + /// [`Error::SafeErc20FailedOperation`] is returned. + fn force_approve( + &mut self, + token: Address, + spender: Address, + value: U256, + ) -> Result<(), Self::Error>; +} + +#[public] +impl ISafeErc20 for SafeErc20 { + type Error = Error; + + fn safe_transfer( + &mut self, + token: Address, + to: Address, + value: U256, + ) -> Result<(), Self::Error> { + let call = IErc20::transferCall { to, value }; + + Self::call_optional_return(token, &call) + } + + fn safe_transfer_from( + &mut self, + token: Address, + from: Address, + to: Address, + value: U256, + ) -> Result<(), Self::Error> { + let call = IErc20::transferFromCall { from, to, value }; + + Self::call_optional_return(token, &call) + } + + fn safe_increase_allowance( + &mut self, + token: Address, + spender: Address, + value: U256, + ) -> Result<(), Self::Error> { + let current_allowance = Self::allowance(token, spender)?; + let new_allowance = current_allowance + .checked_add(value) + .expect("should not exceed `U256::MAX` for allowance"); + self.force_approve(token, spender, new_allowance) + } + + fn safe_decrease_allowance( + &mut self, + token: Address, + spender: Address, + requested_decrease: U256, + ) -> Result<(), Self::Error> { + let current_allowance = Self::allowance(token, spender)?; + + if current_allowance < requested_decrease { + return Err(SafeErc20FailedDecreaseAllowance { + spender, + current_allowance, + requested_decrease, + } + .into()); + } + + self.force_approve( + token, + spender, + current_allowance - requested_decrease, + ) + } + + fn force_approve( + &mut self, + token: Address, + spender: Address, + value: U256, + ) -> Result<(), Self::Error> { + let approve_call = IErc20::approveCall { spender, value }; + + // Try performing the approval with the desired value. + if Self::call_optional_return(token, &approve_call).is_ok() { + return Ok(()); + } + + // If that fails, reset the allowance to zero, then retry the desired + // approval. + let reset_approval_call = + IErc20::approveCall { spender, value: U256::ZERO }; + Self::call_optional_return(token, &reset_approval_call)?; + Self::call_optional_return(token, &approve_call) + } +} + +impl SafeErc20 { + /// Imitates a Stylus high-level call, relaxing the requirement on the + /// return value: if data is returned, it must not be `false`, otherwise + /// calls are assumed to be successful. + /// + /// # Arguments + /// + /// * `token` - Address of the ERC-20 token contract. + /// * `call` - [`IErc20`] call that implements [`SolCall`] trait. + /// + /// # Errors + /// + /// If the `token` address is not a contract, then the error + /// [`Error::SafeErc20FailedOperation`] is returned. + /// If the contract fails to execute the call, then the error + /// [`Error::SafeErc20FailedOperation`] is returned. + /// If the call returns value that is not `true`, then the error + /// [`Error::SafeErc20FailedOperation`] is returned. + fn call_optional_return( + token: Address, + call: &impl SolCall, + ) -> Result<(), Error> { + if !Address::has_code(&token) { + return Err(SafeErc20FailedOperation { token }.into()); + } + + match RawCall::new() + .gas(gas_left()) + .limit_return_data(0, 32) + .call(token, &call.abi_encode()) + { + Ok(data) if data.is_empty() || Self::encodes_true(&data) => Ok(()), + _ => Err(SafeErc20FailedOperation { token }.into()), + } + } + + /// Returns the remaining number of ERC-20 tokens that `spender` + /// will be allowed to spend on behalf of an owner. + /// + /// # Arguments + /// + /// * `token` - Address of the ERC-20 token contract. + /// * `spender` - Account that will spend the tokens. + /// + /// # Errors + /// + /// If the `token` address is not a contract, then the error + /// [`Error::SafeErc20FailedOperation`] is returned. + /// If the contract fails to read `spender`'s allowance, then the error + /// [`Error::SafeErc20FailedOperation`] is returned. + fn allowance(token: Address, spender: Address) -> Result { + if !Address::has_code(&token) { + return Err(SafeErc20FailedOperation { token }.into()); + } + + let call = IErc20::allowanceCall { owner: address(), spender }; + let allowance = RawCall::new() + .gas(gas_left()) + .limit_return_data(0, 32) + .call(token, &call.abi_encode()) + .map_err(|_| { + Error::SafeErc20FailedOperation(SafeErc20FailedOperation { + token, + }) + })?; + + Ok(U256::from_be_slice(&allowance)) + } + + /// Returns true if a slice of bytes is an ABI encoded `true` value. + /// + /// # Arguments + /// + /// * `data` - Slice of bytes. + fn encodes_true(data: &[u8]) -> bool { + data.split_last().map_or(false, |(last, rest)| { + *last == 1 && rest.iter().all(|&byte| byte == 0) + }) + } +} + +#[cfg(all(test, feature = "std"))] +mod tests { + use super::SafeErc20; + #[test] + fn encodes_true_empty_slice() { + assert_eq!(false, SafeErc20::encodes_true(&vec![])); + } + + #[test] + fn encodes_false_single_byte() { + assert_eq!(false, SafeErc20::encodes_true(&vec![0])); + } + + #[test] + fn encodes_true_single_byte() { + assert_eq!(true, SafeErc20::encodes_true(&vec![1])); + } + + #[test] + fn encodes_false_many_bytes() { + assert_eq!( + false, + SafeErc20::encodes_true(&vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + ); + } + + #[test] + fn encodes_true_many_bytes() { + assert_eq!( + true, + SafeErc20::encodes_true(&vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]) + ); + } + + #[test] + fn encodes_true_wrong_bytes() { + assert_eq!( + false, + SafeErc20::encodes_true(&vec![0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1]) + ); + } +} diff --git a/examples/safe-erc20/Cargo.toml b/examples/safe-erc20/Cargo.toml new file mode 100644 index 00000000..15408bd5 --- /dev/null +++ b/examples/safe-erc20/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "safe-erc20-example" +edition.workspace = true +license.workspace = true +repository.workspace = true +publish = false +version.workspace = true + +[dependencies] +openzeppelin-stylus.workspace = true +alloy-primitives.workspace = true +stylus-sdk.workspace = true + +[dev-dependencies] +alloy.workspace = true +eyre.workspace = true +tokio.workspace = true +e2e.workspace = true + +[features] +e2e = [] + +[lib] +crate-type = ["lib", "cdylib"] diff --git a/examples/safe-erc20/src/lib.rs b/examples/safe-erc20/src/lib.rs new file mode 100644 index 00000000..fcc64a9c --- /dev/null +++ b/examples/safe-erc20/src/lib.rs @@ -0,0 +1,17 @@ +#![cfg_attr(not(test), no_main)] +extern crate alloc; + +use openzeppelin_stylus::token::erc20::utils::safe_erc20::SafeErc20; +use stylus_sdk::prelude::{entrypoint, public, sol_storage}; + +sol_storage! { + #[entrypoint] + struct SafeErc20Example { + #[borrow] + SafeErc20 safe_erc20; + } +} + +#[public] +#[inherit(SafeErc20)] +impl SafeErc20Example {} diff --git a/examples/safe-erc20/tests/abi/mod.rs b/examples/safe-erc20/tests/abi/mod.rs new file mode 100644 index 00000000..8a441879 --- /dev/null +++ b/examples/safe-erc20/tests/abi/mod.rs @@ -0,0 +1,25 @@ +#![allow(dead_code)] +use alloy::sol; + +sol!( + #[sol(rpc)] + contract SafeErc20 { + function safeTransfer(address token, address to, uint256 value) external; + function safeTransferFrom(address token, address from, address to, uint256 value) external; + function safeIncreaseAllowance(address token, address spender, uint256 value) external; + function safeDecreaseAllowance(address token, address spender, uint256 requestedDecrease) external; + function forceApprove(address token, address spender, uint256 value) external; + + error SafeErc20FailedOperation(address token); + error SafeErc20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease); + } + + contract Erc20 { + error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed); + + #[derive(Debug, PartialEq)] + event Transfer(address indexed from, address indexed to, uint256 value); + #[derive(Debug, PartialEq)] + event Approval(address indexed owner, address indexed spender, uint256 value); + } +); diff --git a/examples/safe-erc20/tests/address_with_no_code.rs b/examples/safe-erc20/tests/address_with_no_code.rs new file mode 100644 index 00000000..221be971 --- /dev/null +++ b/examples/safe-erc20/tests/address_with_no_code.rs @@ -0,0 +1,135 @@ +#![cfg(feature = "e2e")] + +use abi::SafeErc20; +use alloy::primitives::{uint, U256}; +use e2e::{send, Account, ReceiptExt, Revert}; + +mod abi; +mod mock; + +#[e2e::test] +async fn reverts_on_transfer( + alice: Account, + bob: Account, + has_no_code: Account, +) -> eyre::Result<()> { + let safe_erc20_addr = alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = SafeErc20::new(safe_erc20_addr, &alice.wallet); + let bob_addr = bob.address(); + let has_no_code_addr = has_no_code.address(); + + let value = uint!(1_U256); + + let err = + send!(safe_erc20_alice.safeTransfer(has_no_code_addr, bob_addr, value)) + .expect_err("should not be able to invoke 'transfer' on EOA"); + assert!(err.reverted_with(SafeErc20::SafeErc20FailedOperation { + token: has_no_code_addr + })); + + Ok(()) +} + +#[e2e::test] +async fn reverts_on_transfer_from( + alice: Account, + bob: Account, + has_no_code: Account, +) -> eyre::Result<()> { + let safe_erc20_addr = alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = SafeErc20::new(safe_erc20_addr, &alice.wallet); + let alice_addr = alice.address(); + let bob_addr = bob.address(); + let has_no_code_addr = has_no_code.address(); + + let value = uint!(1_U256); + + let err = send!(safe_erc20_alice.safeTransferFrom( + has_no_code_addr, + alice_addr, + bob_addr, + value + )) + .expect_err("should not be able to invoke 'transferFrom' on EOA"); + assert!(err.reverted_with(SafeErc20::SafeErc20FailedOperation { + token: has_no_code_addr + })); + + Ok(()) +} + +#[e2e::test] +async fn reverts_on_increase_allowance( + alice: Account, + bob: Account, + has_no_code: Account, +) -> eyre::Result<()> { + let safe_erc20_addr = alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = SafeErc20::new(safe_erc20_addr, &alice.wallet); + let bob_addr = bob.address(); + let has_no_code_addr = has_no_code.address(); + + let value = uint!(1_U256); + + let err = send!(safe_erc20_alice.safeIncreaseAllowance( + has_no_code_addr, + bob_addr, + value + )) + .expect_err("should not be able to invoke 'increaseAllowance' on EOA"); + assert!(err.reverted_with(SafeErc20::SafeErc20FailedOperation { + token: has_no_code_addr + })); + + Ok(()) +} + +#[e2e::test] +async fn reverts_on_decrease_allowance( + alice: Account, + bob: Account, + has_no_code: Account, +) -> eyre::Result<()> { + let safe_erc20_addr = alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = SafeErc20::new(safe_erc20_addr, &alice.wallet); + let bob_addr = bob.address(); + let has_no_code_addr = has_no_code.address(); + + let requested_decrease = uint!(1_U256); + + let err = send!(safe_erc20_alice.safeDecreaseAllowance( + has_no_code_addr, + bob_addr, + requested_decrease + )) + .expect_err("should not be able to invoke 'decreaseAllowance' on EOA"); + assert!(err.reverted_with(SafeErc20::SafeErc20FailedOperation { + token: has_no_code_addr + })); + + Ok(()) +} + +#[e2e::test] +async fn reverts_on_force_approve( + alice: Account, + bob: Account, + has_no_code: Account, +) -> eyre::Result<()> { + let safe_erc20_addr = alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = SafeErc20::new(safe_erc20_addr, &alice.wallet); + let bob_addr = bob.address(); + let has_no_code_addr = has_no_code.address(); + + let err = send!(safe_erc20_alice.forceApprove( + has_no_code_addr, + bob_addr, + U256::ZERO + )) + .expect_err("should not be able to invoke 'forceApprove' on EOA"); + assert!(err.reverted_with(SafeErc20::SafeErc20FailedOperation { + token: has_no_code_addr + })); + + Ok(()) +} diff --git a/examples/safe-erc20/tests/erc20.rs b/examples/safe-erc20/tests/erc20.rs new file mode 100644 index 00000000..ae534333 --- /dev/null +++ b/examples/safe-erc20/tests/erc20.rs @@ -0,0 +1,620 @@ +#![cfg(feature = "e2e")] + +use abi::{Erc20, SafeErc20}; +use alloy::primitives::uint; +use alloy_primitives::U256; +use e2e::{ + receipt, send, watch, Account, EventExt, Panic, PanicCode, ReceiptExt, + Revert, +}; +use mock::{erc20, erc20::ERC20Mock}; + +mod abi; +mod mock; + +mod transfers { + use super::*; + + #[e2e::test] + async fn does_not_revert_on_transfer( + alice: Account, + bob: Account, + ) -> eyre::Result<()> { + let safe_erc20_addr = alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = SafeErc20::new(safe_erc20_addr, &alice.wallet); + let bob_addr = bob.address(); + + let balance = uint!(10_U256); + let value = uint!(1_U256); + + let erc20_address = erc20::deploy(&alice.wallet).await?; + let erc20_alice = ERC20Mock::new(erc20_address, &alice.wallet); + + let _ = watch!(erc20_alice.mint(safe_erc20_addr, balance)); + + let initial_safe_erc20_balance = + erc20_alice.balanceOf(safe_erc20_addr).call().await?._0; + let initial_bob_balance = + erc20_alice.balanceOf(bob_addr).call().await?._0; + assert_eq!(initial_safe_erc20_balance, balance); + assert_eq!(initial_bob_balance, U256::ZERO); + + let receipt = receipt!(safe_erc20_alice.safeTransfer( + erc20_address, + bob_addr, + value + ))?; + + assert!(receipt.emits(Erc20::Transfer { + from: safe_erc20_addr, + to: bob_addr, + value + })); + + let safe_erc20_balance = + erc20_alice.balanceOf(safe_erc20_addr).call().await?._0; + let bob_balance = erc20_alice.balanceOf(bob_addr).call().await?._0; + + assert_eq!(initial_safe_erc20_balance - value, safe_erc20_balance); + assert_eq!(initial_bob_balance + value, bob_balance); + + Ok(()) + } + + #[e2e::test] + async fn reverts_on_transfer_with_internal_error( + alice: Account, + bob: Account, + ) -> eyre::Result<()> { + let safe_erc20_addr = alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = SafeErc20::new(safe_erc20_addr, &alice.wallet); + let bob_addr = bob.address(); + + let value = uint!(1_U256); + + let erc20_address = erc20::deploy(&alice.wallet).await?; + let erc20_alice = ERC20Mock::new(erc20_address, &alice.wallet); + + let initial_safe_erc20_balance = + erc20_alice.balanceOf(safe_erc20_addr).call().await?._0; + let initial_bob_balance = + erc20_alice.balanceOf(bob_addr).call().await?._0; + + let err = send!(safe_erc20_alice.safeTransfer( + erc20_address, + bob_addr, + value + )) + .expect_err("should not transfer when insufficient balance"); + + assert!(err.reverted_with(SafeErc20::SafeErc20FailedOperation { + token: erc20_address + })); + + let safe_erc20_balance = + erc20_alice.balanceOf(safe_erc20_addr).call().await?._0; + let bob_balance = erc20_alice.balanceOf(bob_addr).call().await?._0; + + assert_eq!(initial_safe_erc20_balance, safe_erc20_balance); + assert_eq!(initial_bob_balance, bob_balance); + + Ok(()) + } + + #[e2e::test] + async fn does_not_revert_on_transfer_from( + alice: Account, + bob: Account, + ) -> eyre::Result<()> { + let safe_erc20_addr = alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = SafeErc20::new(safe_erc20_addr, &alice.wallet); + let alice_addr = alice.address(); + let bob_addr = bob.address(); + + let balance = uint!(10_U256); + let value = uint!(1_U256); + + let erc20_address = erc20::deploy(&alice.wallet).await?; + let erc20_alice = ERC20Mock::new(erc20_address, &alice.wallet); + + let _ = watch!(erc20_alice.mint(alice_addr, balance)); + let _ = watch!(erc20_alice.approve(safe_erc20_addr, value)); + + let initial_alice_balance = + erc20_alice.balanceOf(alice_addr).call().await?._0; + let initial_bob_balance = + erc20_alice.balanceOf(bob_addr).call().await?._0; + assert_eq!(initial_alice_balance, balance); + assert_eq!(initial_bob_balance, U256::ZERO); + + let receipt = receipt!(safe_erc20_alice.safeTransferFrom( + erc20_address, + alice_addr, + bob_addr, + value + ))?; + + assert!(receipt.emits(Erc20::Transfer { + from: alice_addr, + to: bob_addr, + value + })); + + let alice_balance = erc20_alice.balanceOf(alice_addr).call().await?._0; + let bob_balance = erc20_alice.balanceOf(bob_addr).call().await?._0; + + assert_eq!(initial_alice_balance - value, alice_balance); + assert_eq!(initial_bob_balance + value, bob_balance); + + Ok(()) + } + + #[e2e::test] + async fn reverts_on_transfer_from_internal_error( + alice: Account, + bob: Account, + ) -> eyre::Result<()> { + let safe_erc20_addr = alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = SafeErc20::new(safe_erc20_addr, &alice.wallet); + let alice_addr = alice.address(); + let bob_addr = bob.address(); + + let value = uint!(1_U256); + + let erc20_address = erc20::deploy(&alice.wallet).await?; + let erc20_alice = ERC20Mock::new(erc20_address, &alice.wallet); + + let _ = watch!(erc20_alice.approve(safe_erc20_addr, value)); + + let initial_alice_balance = + erc20_alice.balanceOf(alice_addr).call().await?._0; + let initial_bob_balance = + erc20_alice.balanceOf(bob_addr).call().await?._0; + + let err = send!(safe_erc20_alice.safeTransferFrom( + erc20_address, + alice_addr, + bob_addr, + value + )) + .expect_err("should not transfer when insufficient balance"); + + assert!(err.reverted_with(SafeErc20::SafeErc20FailedOperation { + token: erc20_address + })); + + let alice_balance = erc20_alice.balanceOf(alice_addr).call().await?._0; + let bob_balance = erc20_alice.balanceOf(bob_addr).call().await?._0; + + assert_eq!(initial_alice_balance, alice_balance); + assert_eq!(initial_bob_balance, bob_balance); + + Ok(()) + } +} + +mod approvals { + mod with_zero_allowance { + use super::super::*; + + #[e2e::test] + async fn does_not_revert_when_force_approving_a_non_zero_allowance( + alice: Account, + ) -> eyre::Result<()> { + let safe_erc20_addr = + alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = + SafeErc20::new(safe_erc20_addr, &alice.wallet); + let spender_addr = alice.address(); + + let erc20_address = erc20::deploy(&alice.wallet).await?; + let erc20_alice = ERC20Mock::new(erc20_address, &alice.wallet); + + let _ = watch!(erc20_alice.regular_approve( + safe_erc20_addr, + spender_addr, + U256::ZERO + )); + + let value = uint!(100_U256); + + let receipt = receipt!(safe_erc20_alice.forceApprove( + erc20_address, + spender_addr, + value + ))?; + + assert!(receipt.emits(Erc20::Approval { + owner: safe_erc20_addr, + spender: spender_addr, + value, + })); + + let spender_allowance = erc20_alice + .allowance(safe_erc20_addr, spender_addr) + .call() + .await? + ._0; + assert_eq!(spender_allowance, value); + + Ok(()) + } + + #[e2e::test] + async fn does_not_revert_when_force_approving_a_zero_allowance( + alice: Account, + ) -> eyre::Result<()> { + let safe_erc20_addr = + alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = + SafeErc20::new(safe_erc20_addr, &alice.wallet); + let spender_addr = alice.address(); + + let erc20_address = erc20::deploy(&alice.wallet).await?; + let erc20_alice = ERC20Mock::new(erc20_address, &alice.wallet); + + let _ = watch!(erc20_alice.regular_approve( + safe_erc20_addr, + spender_addr, + U256::ZERO + )); + + let receipt = receipt!(safe_erc20_alice.forceApprove( + erc20_address, + spender_addr, + U256::ZERO + ))?; + + assert!(receipt.emits(Erc20::Approval { + owner: safe_erc20_addr, + spender: spender_addr, + value: U256::ZERO, + })); + + let spender_allowance = erc20_alice + .allowance(safe_erc20_addr, spender_addr) + .call() + .await? + ._0; + assert_eq!(spender_allowance, U256::ZERO); + + Ok(()) + } + + #[e2e::test] + async fn does_not_revert_when_increasing_the_allowance( + alice: Account, + ) -> eyre::Result<()> { + let safe_erc20_addr = + alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = + SafeErc20::new(safe_erc20_addr, &alice.wallet); + let spender_addr = alice.address(); + + let erc20_address = erc20::deploy(&alice.wallet).await?; + let erc20_alice = ERC20Mock::new(erc20_address, &alice.wallet); + + let _ = watch!(erc20_alice.regular_approve( + safe_erc20_addr, + spender_addr, + U256::ZERO + )); + + let value = uint!(10_U256); + + let receipt = receipt!(safe_erc20_alice.safeIncreaseAllowance( + erc20_address, + spender_addr, + value + ))?; + + assert!(receipt.emits(Erc20::Approval { + owner: safe_erc20_addr, + spender: spender_addr, + value, + })); + + let spender_allowance = erc20_alice + .allowance(safe_erc20_addr, spender_addr) + .call() + .await? + ._0; + assert_eq!(spender_allowance, value); + + Ok(()) + } + + #[e2e::test] + async fn panics_when_increasing_the_allowance_overflow( + alice: Account, + ) -> eyre::Result<()> { + let safe_erc20_addr = + alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = + SafeErc20::new(safe_erc20_addr, &alice.wallet); + let spender_addr = alice.address(); + + let erc20_address = erc20::deploy(&alice.wallet).await?; + let erc20_alice = ERC20Mock::new(erc20_address, &alice.wallet); + + let _ = watch!(erc20_alice.regular_approve( + safe_erc20_addr, + spender_addr, + U256::MAX + )); + + let value = uint!(1_U256); + + let err = send!(safe_erc20_alice.safeIncreaseAllowance( + erc20_address, + spender_addr, + value + )) + .expect_err("should not exceed U256::MAX"); + + assert!(err.panicked_with(PanicCode::ArithmeticOverflow)); + + Ok(()) + } + + #[e2e::test] + async fn reverts_when_decreasing_the_allowance( + alice: Account, + ) -> eyre::Result<()> { + let safe_erc20_addr = + alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = + SafeErc20::new(safe_erc20_addr, &alice.wallet); + let spender_addr = alice.address(); + + let erc20_address = erc20::deploy(&alice.wallet).await?; + let erc20_alice = ERC20Mock::new(erc20_address, &alice.wallet); + + let _ = watch!(erc20_alice.regular_approve( + safe_erc20_addr, + spender_addr, + U256::ZERO + )); + + let value = uint!(10_U256); + + let err = send!(safe_erc20_alice.safeDecreaseAllowance( + erc20_address, + spender_addr, + value + )) + .expect_err("should not be able to succeed on 'decreaseAllowance'"); + assert!(err.reverted_with( + SafeErc20::SafeErc20FailedDecreaseAllowance { + spender: spender_addr, + currentAllowance: U256::ZERO, + requestedDecrease: value + } + )); + + Ok(()) + } + } + + mod with_non_zero_allowance { + use super::super::*; + + #[e2e::test] + async fn does_not_revert_when_force_approving_a_non_zero_allowance( + alice: Account, + ) -> eyre::Result<()> { + let safe_erc20_addr = + alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = + SafeErc20::new(safe_erc20_addr, &alice.wallet); + let spender_addr = alice.address(); + + let erc20_address = erc20::deploy(&alice.wallet).await?; + let erc20_alice = ERC20Mock::new(erc20_address, &alice.wallet); + + let allowance = uint!(100_U256); + + let _ = watch!(erc20_alice.regular_approve( + safe_erc20_addr, + spender_addr, + allowance + )); + + let value = uint!(20_U256); + + let receipt = receipt!(safe_erc20_alice.forceApprove( + erc20_address, + spender_addr, + value + ))?; + + assert!(receipt.emits(Erc20::Approval { + owner: safe_erc20_addr, + spender: spender_addr, + value, + })); + + let spender_allowance = erc20_alice + .allowance(safe_erc20_addr, spender_addr) + .call() + .await? + ._0; + assert_eq!(spender_allowance, value); + + Ok(()) + } + + #[e2e::test] + async fn does_not_revert_when_force_approving_a_zero_allowance( + alice: Account, + ) -> eyre::Result<()> { + let safe_erc20_addr = + alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = + SafeErc20::new(safe_erc20_addr, &alice.wallet); + let spender_addr = alice.address(); + + let erc20_address = erc20::deploy(&alice.wallet).await?; + let erc20_alice = ERC20Mock::new(erc20_address, &alice.wallet); + + let allowance = uint!(100_U256); + + let _ = watch!(erc20_alice.regular_approve( + safe_erc20_addr, + spender_addr, + allowance + )); + + let receipt = receipt!(safe_erc20_alice.forceApprove( + erc20_address, + spender_addr, + U256::ZERO + ))?; + + assert!(receipt.emits(Erc20::Approval { + owner: safe_erc20_addr, + spender: spender_addr, + value: U256::ZERO, + })); + + let spender_allowance = erc20_alice + .allowance(safe_erc20_addr, spender_addr) + .call() + .await? + ._0; + assert_eq!(spender_allowance, U256::ZERO); + + Ok(()) + } + + #[e2e::test] + async fn does_not_revert_when_increasing_the_allowance( + alice: Account, + ) -> eyre::Result<()> { + let safe_erc20_addr = + alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = + SafeErc20::new(safe_erc20_addr, &alice.wallet); + let spender_addr = alice.address(); + + let erc20_address = erc20::deploy(&alice.wallet).await?; + let erc20_alice = ERC20Mock::new(erc20_address, &alice.wallet); + + let allowance = uint!(100_U256); + + let _ = watch!(erc20_alice.regular_approve( + safe_erc20_addr, + spender_addr, + allowance + )); + + let value = uint!(10_U256); + + let receipt = receipt!(safe_erc20_alice.safeIncreaseAllowance( + erc20_address, + spender_addr, + value + ))?; + + assert!(receipt.emits(Erc20::Approval { + owner: safe_erc20_addr, + spender: spender_addr, + value: allowance + value, + })); + + let spender_allowance = erc20_alice + .allowance(safe_erc20_addr, spender_addr) + .call() + .await? + ._0; + assert_eq!(spender_allowance, allowance + value); + + Ok(()) + } + + #[e2e::test] + async fn does_not_revert_when_decreasing_the_allowance_to_a_positive_value( + alice: Account, + ) -> eyre::Result<()> { + let safe_erc20_addr = + alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = + SafeErc20::new(safe_erc20_addr, &alice.wallet); + let spender_addr = alice.address(); + + let erc20_address = erc20::deploy(&alice.wallet).await?; + let erc20_alice = ERC20Mock::new(erc20_address, &alice.wallet); + + let allowance = uint!(100_U256); + + let _ = watch!(erc20_alice.regular_approve( + safe_erc20_addr, + spender_addr, + allowance + )); + + let value = uint!(50_U256); + + let receipt = receipt!(safe_erc20_alice.safeDecreaseAllowance( + erc20_address, + spender_addr, + value + ))?; + + assert!(receipt.emits(Erc20::Approval { + owner: safe_erc20_addr, + spender: spender_addr, + value: allowance - value, + })); + + let spender_allowance = erc20_alice + .allowance(safe_erc20_addr, spender_addr) + .call() + .await? + ._0; + assert_eq!(spender_allowance, allowance - value); + + Ok(()) + } + + #[e2e::test] + async fn reverts_when_decreasing_the_allowance_to_a_negative_value( + alice: Account, + ) -> eyre::Result<()> { + let safe_erc20_addr = + alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = + SafeErc20::new(safe_erc20_addr, &alice.wallet); + let spender_addr = alice.address(); + + let erc20_address = erc20::deploy(&alice.wallet).await?; + let erc20_alice = ERC20Mock::new(erc20_address, &alice.wallet); + + let allowance = uint!(100_U256); + + let _ = watch!(erc20_alice.regular_approve( + safe_erc20_addr, + spender_addr, + allowance + )); + + let value = uint!(200_U256); + + let err = send!(safe_erc20_alice.safeDecreaseAllowance( + erc20_address, + spender_addr, + value + )) + .expect_err("should not be able to succeed on 'decreaseAllowance'"); + assert!(err.reverted_with( + SafeErc20::SafeErc20FailedDecreaseAllowance { + spender: spender_addr, + currentAllowance: allowance, + requestedDecrease: value + } + )); + + Ok(()) + } + } +} diff --git a/examples/safe-erc20/tests/erc20_that_always_returns_false.rs b/examples/safe-erc20/tests/erc20_that_always_returns_false.rs new file mode 100644 index 00000000..a3fa7851 --- /dev/null +++ b/examples/safe-erc20/tests/erc20_that_always_returns_false.rs @@ -0,0 +1,128 @@ +#![cfg(feature = "e2e")] + +use abi::SafeErc20; +use alloy::primitives::U256; +use e2e::{send, Account, ReceiptExt, Revert}; +use mock::erc20_return_false; + +mod abi; +mod mock; + +#[e2e::test] +async fn reverts_on_transfer(alice: Account, bob: Account) -> eyre::Result<()> { + let safe_erc20_addr = alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = SafeErc20::new(safe_erc20_addr, &alice.wallet); + let bob_addr = bob.address(); + + let erc20_address = erc20_return_false::deploy(&alice.wallet).await?; + + let err = send!(safe_erc20_alice.safeTransfer( + erc20_address, + bob_addr, + U256::ZERO + )) + .expect_err("should not be able to succeed on 'transfer'"); + assert!(err.reverted_with(SafeErc20::SafeErc20FailedOperation { + token: erc20_address + })); + + Ok(()) +} + +#[e2e::test] +async fn reverts_on_transfer_from( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let safe_erc20_addr = alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = SafeErc20::new(safe_erc20_addr, &alice.wallet); + let alice_addr = alice.address(); + let bob_addr = bob.address(); + + let erc20_address = erc20_return_false::deploy(&alice.wallet).await?; + + let err = send!(safe_erc20_alice.safeTransferFrom( + erc20_address, + alice_addr, + bob_addr, + U256::ZERO + )) + .expect_err("should not be able to succeed on 'transferFrom'"); + assert!(err.reverted_with(SafeErc20::SafeErc20FailedOperation { + token: erc20_address + })); + + Ok(()) +} + +#[e2e::test] +async fn reverts_on_increase_allowance( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let safe_erc20_addr = alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = SafeErc20::new(safe_erc20_addr, &alice.wallet); + let bob_addr = bob.address(); + + let erc20_address = erc20_return_false::deploy(&alice.wallet).await?; + + let err = send!(safe_erc20_alice.safeIncreaseAllowance( + erc20_address, + bob_addr, + U256::ZERO + )) + .expect_err("should not be able to succeed on 'increaseAllowance'"); + assert!(err.reverted_with(SafeErc20::SafeErc20FailedOperation { + token: erc20_address + })); + + Ok(()) +} + +#[e2e::test] +async fn reverts_on_decrease_allowance( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let safe_erc20_addr = alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = SafeErc20::new(safe_erc20_addr, &alice.wallet); + let bob_addr = bob.address(); + + let erc20_address = erc20_return_false::deploy(&alice.wallet).await?; + + let err = send!(safe_erc20_alice.safeDecreaseAllowance( + erc20_address, + bob_addr, + U256::ZERO + )) + .expect_err("should not be able to succeed on 'decreaseAllowance'"); + assert!(err.reverted_with(SafeErc20::SafeErc20FailedOperation { + token: erc20_address + })); + + Ok(()) +} + +#[e2e::test] +async fn reverts_on_force_approve( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let safe_erc20_addr = alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = SafeErc20::new(safe_erc20_addr, &alice.wallet); + let bob_addr = bob.address(); + + let erc20_address = erc20_return_false::deploy(&alice.wallet).await?; + + let err = send!(safe_erc20_alice.forceApprove( + erc20_address, + bob_addr, + U256::ZERO + )) + .expect_err("should not be able to succeed on 'forceApprove'"); + assert!(err.reverted_with(SafeErc20::SafeErc20FailedOperation { + token: erc20_address + })); + + Ok(()) +} diff --git a/examples/safe-erc20/tests/erc20_that_does_not_return.rs b/examples/safe-erc20/tests/erc20_that_does_not_return.rs new file mode 100644 index 00000000..07709621 --- /dev/null +++ b/examples/safe-erc20/tests/erc20_that_does_not_return.rs @@ -0,0 +1,628 @@ +#![cfg(feature = "e2e")] + +use abi::{Erc20, SafeErc20}; +use alloy::primitives::uint; +use alloy_primitives::U256; +use e2e::{ + receipt, send, watch, Account, EventExt, Panic, PanicCode, ReceiptExt, + Revert, +}; +use mock::{erc20_no_return, erc20_no_return::ERC20NoReturnMock}; + +mod abi; +mod mock; + +mod transfers { + use super::*; + + #[e2e::test] + async fn does_not_revert_on_transfer( + alice: Account, + bob: Account, + ) -> eyre::Result<()> { + let safe_erc20_addr = alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = SafeErc20::new(safe_erc20_addr, &alice.wallet); + let bob_addr = bob.address(); + + let balance = uint!(10_U256); + let value = uint!(1_U256); + + let erc20_address = erc20_no_return::deploy(&alice.wallet).await?; + let erc20_alice = ERC20NoReturnMock::new(erc20_address, &alice.wallet); + + let _ = watch!(erc20_alice.mint(safe_erc20_addr, balance)); + + let initial_safe_erc20_balance = + erc20_alice.balanceOf(safe_erc20_addr).call().await?._0; + let initial_bob_balance = + erc20_alice.balanceOf(bob_addr).call().await?._0; + assert_eq!(initial_safe_erc20_balance, balance); + assert_eq!(initial_bob_balance, U256::ZERO); + + let receipt = receipt!(safe_erc20_alice.safeTransfer( + erc20_address, + bob_addr, + value + ))?; + + assert!(receipt.emits(Erc20::Transfer { + from: safe_erc20_addr, + to: bob_addr, + value, + })); + + let safe_erc20_balance = + erc20_alice.balanceOf(safe_erc20_addr).call().await?._0; + let bob_balance = erc20_alice.balanceOf(bob_addr).call().await?._0; + + assert_eq!(initial_safe_erc20_balance - value, safe_erc20_balance); + assert_eq!(initial_bob_balance + value, bob_balance); + + Ok(()) + } + + #[e2e::test] + async fn reverts_on_transfer_with_internal_error( + alice: Account, + bob: Account, + ) -> eyre::Result<()> { + let safe_erc20_addr = alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = SafeErc20::new(safe_erc20_addr, &alice.wallet); + let bob_addr = bob.address(); + + let value = uint!(1_U256); + + let erc20_address = erc20_no_return::deploy(&alice.wallet).await?; + let erc20_alice = ERC20NoReturnMock::new(erc20_address, &alice.wallet); + + let initial_safe_erc20_balance = + erc20_alice.balanceOf(safe_erc20_addr).call().await?._0; + let initial_bob_balance = + erc20_alice.balanceOf(bob_addr).call().await?._0; + + let err = send!(safe_erc20_alice.safeTransfer( + erc20_address, + bob_addr, + value + )) + .expect_err("should not transfer when insufficient balance"); + assert!(err.reverted_with(SafeErc20::SafeErc20FailedOperation { + token: erc20_address + })); + + let safe_erc20_balance = + erc20_alice.balanceOf(safe_erc20_addr).call().await?._0; + let bob_balance = erc20_alice.balanceOf(bob_addr).call().await?._0; + + assert_eq!(initial_safe_erc20_balance, safe_erc20_balance); + assert_eq!(initial_bob_balance, bob_balance); + + Ok(()) + } + + #[e2e::test] + async fn does_not_revert_on_transfer_from( + alice: Account, + bob: Account, + ) -> eyre::Result<()> { + let safe_erc20_addr = alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = SafeErc20::new(safe_erc20_addr, &alice.wallet); + let alice_addr = alice.address(); + let bob_addr = bob.address(); + + let balance = uint!(10_U256); + let value = uint!(1_U256); + + let erc20_address = erc20_no_return::deploy(&alice.wallet).await?; + let erc20_alice = ERC20NoReturnMock::new(erc20_address, &alice.wallet); + + let _ = watch!(erc20_alice.mint(alice_addr, balance)); + let _ = watch!(erc20_alice.approve(safe_erc20_addr, value)); + + let initial_alice_balance = + erc20_alice.balanceOf(alice_addr).call().await?._0; + let initial_bob_balance = + erc20_alice.balanceOf(bob_addr).call().await?._0; + assert_eq!(initial_alice_balance, balance); + assert_eq!(initial_bob_balance, U256::ZERO); + + let receipt = receipt!(safe_erc20_alice.safeTransferFrom( + erc20_address, + alice_addr, + bob_addr, + value + ))?; + + assert!(receipt.emits(Erc20::Transfer { + from: alice_addr, + to: bob_addr, + value, + })); + + let alice_balance = erc20_alice.balanceOf(alice_addr).call().await?._0; + let bob_balance = erc20_alice.balanceOf(bob_addr).call().await?._0; + + assert_eq!(initial_alice_balance - value, alice_balance); + assert_eq!(initial_bob_balance + value, bob_balance); + + Ok(()) + } + + #[e2e::test] + async fn reverts_on_transfer_from_internal_error( + alice: Account, + bob: Account, + ) -> eyre::Result<()> { + let safe_erc20_addr = alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = SafeErc20::new(safe_erc20_addr, &alice.wallet); + let alice_addr = alice.address(); + let bob_addr = bob.address(); + + let value = uint!(1_U256); + + let erc20_address = erc20_no_return::deploy(&alice.wallet).await?; + let erc20_alice = ERC20NoReturnMock::new(erc20_address, &alice.wallet); + + let _ = watch!(erc20_alice.approve(safe_erc20_addr, value)); + + let initial_alice_balance = + erc20_alice.balanceOf(alice_addr).call().await?._0; + let initial_bob_balance = + erc20_alice.balanceOf(bob_addr).call().await?._0; + + let err = send!(safe_erc20_alice.safeTransferFrom( + erc20_address, + alice_addr, + bob_addr, + value + )) + .expect_err("should not transfer when insufficient balance"); + assert!(err.reverted_with(SafeErc20::SafeErc20FailedOperation { + token: erc20_address + })); + + let alice_balance = erc20_alice.balanceOf(alice_addr).call().await?._0; + let bob_balance = erc20_alice.balanceOf(bob_addr).call().await?._0; + + assert_eq!(initial_alice_balance, alice_balance); + assert_eq!(initial_bob_balance, bob_balance); + + Ok(()) + } +} + +mod approvals { + mod with_zero_allowance { + use super::super::*; + + #[e2e::test] + async fn does_not_revert_when_force_approving_a_non_zero_allowance( + alice: Account, + ) -> eyre::Result<()> { + let safe_erc20_addr = + alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = + SafeErc20::new(safe_erc20_addr, &alice.wallet); + let spender_addr = alice.address(); + + let erc20_address = erc20_no_return::deploy(&alice.wallet).await?; + let erc20_alice = + ERC20NoReturnMock::new(erc20_address, &alice.wallet); + + let _ = watch!(erc20_alice.regular_approve( + safe_erc20_addr, + spender_addr, + U256::ZERO + )); + + let value = uint!(100_U256); + + let receipt = receipt!(safe_erc20_alice.forceApprove( + erc20_address, + spender_addr, + value + ))?; + + assert!(receipt.emits(Erc20::Approval { + owner: safe_erc20_addr, + spender: spender_addr, + value, + })); + + let spender_allowance = erc20_alice + .allowance(safe_erc20_addr, spender_addr) + .call() + .await? + ._0; + assert_eq!(spender_allowance, value); + + Ok(()) + } + + #[e2e::test] + async fn does_not_revert_when_force_approving_a_zero_allowance( + alice: Account, + ) -> eyre::Result<()> { + let safe_erc20_addr = + alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = + SafeErc20::new(safe_erc20_addr, &alice.wallet); + let spender_addr = alice.address(); + + let erc20_address = erc20_no_return::deploy(&alice.wallet).await?; + let erc20_alice = + ERC20NoReturnMock::new(erc20_address, &alice.wallet); + + let _ = watch!(erc20_alice.regular_approve( + safe_erc20_addr, + spender_addr, + U256::ZERO + )); + + let receipt = receipt!(safe_erc20_alice.forceApprove( + erc20_address, + spender_addr, + U256::ZERO + ))?; + + assert!(receipt.emits(Erc20::Approval { + owner: safe_erc20_addr, + spender: spender_addr, + value: U256::ZERO, + })); + + let spender_allowance = erc20_alice + .allowance(safe_erc20_addr, spender_addr) + .call() + .await? + ._0; + assert_eq!(spender_allowance, U256::ZERO); + + Ok(()) + } + + #[e2e::test] + async fn does_not_revert_when_increasing_the_allowance( + alice: Account, + ) -> eyre::Result<()> { + let safe_erc20_addr = + alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = + SafeErc20::new(safe_erc20_addr, &alice.wallet); + let spender_addr = alice.address(); + + let erc20_address = erc20_no_return::deploy(&alice.wallet).await?; + let erc20_alice = + ERC20NoReturnMock::new(erc20_address, &alice.wallet); + + let _ = watch!(erc20_alice.regular_approve( + safe_erc20_addr, + spender_addr, + U256::ZERO + )); + + let value = uint!(10_U256); + + let receipt = receipt!(safe_erc20_alice.safeIncreaseAllowance( + erc20_address, + spender_addr, + value + ))?; + + assert!(receipt.emits(Erc20::Approval { + owner: safe_erc20_addr, + spender: spender_addr, + value, + })); + + let spender_allowance = erc20_alice + .allowance(safe_erc20_addr, spender_addr) + .call() + .await? + ._0; + assert_eq!(spender_allowance, value); + + Ok(()) + } + + #[e2e::test] + async fn panics_when_increasing_the_allowance_overflow( + alice: Account, + ) -> eyre::Result<()> { + let safe_erc20_addr = + alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = + SafeErc20::new(safe_erc20_addr, &alice.wallet); + let spender_addr = alice.address(); + + let erc20_address = erc20_no_return::deploy(&alice.wallet).await?; + let erc20_alice = + ERC20NoReturnMock::new(erc20_address, &alice.wallet); + + let _ = watch!(erc20_alice.regular_approve( + safe_erc20_addr, + spender_addr, + U256::MAX + )); + + let value = uint!(1_U256); + + let err = send!(safe_erc20_alice.safeIncreaseAllowance( + erc20_address, + spender_addr, + value + )) + .expect_err("should not exceed U256::MAX"); + + assert!(err.panicked_with(PanicCode::ArithmeticOverflow)); + + Ok(()) + } + + #[e2e::test] + async fn reverts_when_decreasing_the_allowance( + alice: Account, + ) -> eyre::Result<()> { + let safe_erc20_addr = + alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = + SafeErc20::new(safe_erc20_addr, &alice.wallet); + let spender_addr = alice.address(); + + let erc20_address = erc20_no_return::deploy(&alice.wallet).await?; + let erc20_alice = + ERC20NoReturnMock::new(erc20_address, &alice.wallet); + + let _ = watch!(erc20_alice.regular_approve( + safe_erc20_addr, + spender_addr, + U256::ZERO + )); + + let value = uint!(10_U256); + + let err = send!(safe_erc20_alice.safeDecreaseAllowance( + erc20_address, + spender_addr, + value + )) + .expect_err("should not be able to succeed on 'decreaseAllowance'"); + assert!(err.reverted_with( + SafeErc20::SafeErc20FailedDecreaseAllowance { + spender: spender_addr, + currentAllowance: U256::ZERO, + requestedDecrease: value + } + )); + + Ok(()) + } + } + + mod with_non_zero_allowance { + use super::super::*; + + #[e2e::test] + async fn does_not_revert_when_force_approving_a_non_zero_allowance( + alice: Account, + ) -> eyre::Result<()> { + let safe_erc20_addr = + alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = + SafeErc20::new(safe_erc20_addr, &alice.wallet); + let spender_addr = alice.address(); + + let erc20_address = erc20_no_return::deploy(&alice.wallet).await?; + let erc20_alice = + ERC20NoReturnMock::new(erc20_address, &alice.wallet); + + let allowance = uint!(100_U256); + + let _ = watch!(erc20_alice.regular_approve( + safe_erc20_addr, + spender_addr, + allowance + )); + + let value = uint!(20_U256); + + let receipt = receipt!(safe_erc20_alice.forceApprove( + erc20_address, + spender_addr, + value + ))?; + + assert!(receipt.emits(Erc20::Approval { + owner: safe_erc20_addr, + spender: spender_addr, + value, + })); + + let spender_allowance = erc20_alice + .allowance(safe_erc20_addr, spender_addr) + .call() + .await? + ._0; + assert_eq!(spender_allowance, value); + + Ok(()) + } + + #[e2e::test] + async fn does_not_revert_when_force_approving_a_zero_allowance( + alice: Account, + ) -> eyre::Result<()> { + let safe_erc20_addr = + alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = + SafeErc20::new(safe_erc20_addr, &alice.wallet); + let spender_addr = alice.address(); + + let erc20_address = erc20_no_return::deploy(&alice.wallet).await?; + let erc20_alice = + ERC20NoReturnMock::new(erc20_address, &alice.wallet); + + let allowance = uint!(100_U256); + + let _ = watch!(erc20_alice.regular_approve( + safe_erc20_addr, + spender_addr, + allowance + )); + + let receipt = receipt!(safe_erc20_alice.forceApprove( + erc20_address, + spender_addr, + U256::ZERO + ))?; + + assert!(receipt.emits(Erc20::Approval { + owner: safe_erc20_addr, + spender: spender_addr, + value: U256::ZERO, + })); + + let spender_allowance = erc20_alice + .allowance(safe_erc20_addr, spender_addr) + .call() + .await? + ._0; + assert_eq!(spender_allowance, U256::ZERO); + + Ok(()) + } + + #[e2e::test] + async fn does_not_revert_when_increasing_the_allowance( + alice: Account, + ) -> eyre::Result<()> { + let safe_erc20_addr = + alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = + SafeErc20::new(safe_erc20_addr, &alice.wallet); + let spender_addr = alice.address(); + + let erc20_address = erc20_no_return::deploy(&alice.wallet).await?; + let erc20_alice = + ERC20NoReturnMock::new(erc20_address, &alice.wallet); + + let allowance = uint!(100_U256); + + let _ = watch!(erc20_alice.regular_approve( + safe_erc20_addr, + spender_addr, + allowance + )); + + let value = uint!(10_U256); + + let receipt = receipt!(safe_erc20_alice.safeIncreaseAllowance( + erc20_address, + spender_addr, + value + ))?; + + assert!(receipt.emits(Erc20::Approval { + owner: safe_erc20_addr, + spender: spender_addr, + value: allowance + value, + })); + + let spender_allowance = erc20_alice + .allowance(safe_erc20_addr, spender_addr) + .call() + .await? + ._0; + assert_eq!(spender_allowance, allowance + value); + + Ok(()) + } + + #[e2e::test] + async fn does_not_revert_when_decreasing_the_allowance_to_a_positive_value( + alice: Account, + ) -> eyre::Result<()> { + let safe_erc20_addr = + alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = + SafeErc20::new(safe_erc20_addr, &alice.wallet); + let spender_addr = alice.address(); + + let erc20_address = erc20_no_return::deploy(&alice.wallet).await?; + let erc20_alice = + ERC20NoReturnMock::new(erc20_address, &alice.wallet); + + let allowance = uint!(100_U256); + + let _ = watch!(erc20_alice.regular_approve( + safe_erc20_addr, + spender_addr, + allowance + )); + + let value = uint!(50_U256); + + let receipt = receipt!(safe_erc20_alice.safeDecreaseAllowance( + erc20_address, + spender_addr, + value + ))?; + + assert!(receipt.emits(Erc20::Approval { + owner: safe_erc20_addr, + spender: spender_addr, + value: allowance - value, + })); + + let spender_allowance = erc20_alice + .allowance(safe_erc20_addr, spender_addr) + .call() + .await? + ._0; + assert_eq!(spender_allowance, allowance - value); + + Ok(()) + } + + #[e2e::test] + async fn reverts_when_decreasing_the_allowance_to_a_negative_value( + alice: Account, + ) -> eyre::Result<()> { + let safe_erc20_addr = + alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = + SafeErc20::new(safe_erc20_addr, &alice.wallet); + let spender_addr = alice.address(); + + let erc20_address = erc20_no_return::deploy(&alice.wallet).await?; + let erc20_alice = + ERC20NoReturnMock::new(erc20_address, &alice.wallet); + + let allowance = uint!(100_U256); + + let _ = watch!(erc20_alice.regular_approve( + safe_erc20_addr, + spender_addr, + allowance + )); + + let value = uint!(200_U256); + + let err = send!(safe_erc20_alice.safeDecreaseAllowance( + erc20_address, + spender_addr, + value + )) + .expect_err("should not be able to succeed on 'decreaseAllowance'"); + assert!(err.reverted_with( + SafeErc20::SafeErc20FailedDecreaseAllowance { + spender: spender_addr, + currentAllowance: allowance, + requestedDecrease: value + } + )); + + Ok(()) + } + } +} diff --git a/examples/safe-erc20/tests/mock/erc20.rs b/examples/safe-erc20/tests/mock/erc20.rs new file mode 100644 index 00000000..68514dae --- /dev/null +++ b/examples/safe-erc20/tests/mock/erc20.rs @@ -0,0 +1,48 @@ +#![allow(dead_code)] +#![cfg(feature = "e2e")] +use alloy::{primitives::Address, sol}; +use e2e::Wallet; + +sol! { + #[allow(missing_docs)] + // Built with Remix IDE; solc v0.8.21+commit.d9974bed + #[sol(rpc, bytecode="608060405234801562000010575f80fd5b506040518060400160405280600981526020017f45524332304d6f636b00000000000000000000000000000000000000000000008152506040518060400160405280600381526020017f4d544b000000000000000000000000000000000000000000000000000000000081525081600390816200008e91906200030d565b508060049081620000a091906200030d565b505050620003f1565b5f81519050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b5f60028204905060018216806200012557607f821691505b6020821081036200013b576200013a620000e0565b5b50919050565b5f819050815f5260205f209050919050565b5f6020601f8301049050919050565b5f82821b905092915050565b5f600883026200019f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8262000162565b620001ab868362000162565b95508019841693508086168417925050509392505050565b5f819050919050565b5f819050919050565b5f620001f5620001ef620001e984620001c3565b620001cc565b620001c3565b9050919050565b5f819050919050565b6200021083620001d5565b620002286200021f82620001fc565b8484546200016e565b825550505050565b5f90565b6200023e62000230565b6200024b81848462000205565b505050565b5b818110156200027257620002665f8262000234565b60018101905062000251565b5050565b601f821115620002c1576200028b8162000141565b620002968462000153565b81016020851015620002a6578190505b620002be620002b58562000153565b83018262000250565b50505b505050565b5f82821c905092915050565b5f620002e35f1984600802620002c6565b1980831691505092915050565b5f620002fd8383620002d2565b9150826002028217905092915050565b6200031882620000a9565b67ffffffffffffffff811115620003345762000333620000b3565b5b6200034082546200010d565b6200034d82828562000276565b5f60209050601f83116001811462000383575f84156200036e578287015190505b6200037a8582620002f0565b865550620003e9565b601f198416620003938662000141565b5f5b82811015620003bc5784890151825560018201915060208501945060208101905062000395565b86831015620003dc5784890151620003d8601f891682620002d2565b8355505b6001600288020188555050505b505050505050565b610f4580620003ff5f395ff3fe608060405234801561000f575f80fd5b50600436106100a7575f3560e01c806340c10f191161006f57806340c10f191461016557806370a08231146101815780638483acfe146101b157806395d89b41146101cd578063a9059cbb146101eb578063dd62ed3e1461021b576100a7565b806306fdde03146100ab578063095ea7b3146100c957806318160ddd146100f957806323b872dd14610117578063313ce56714610147575b5f80fd5b6100b361024b565b6040516100c09190610bbe565b60405180910390f35b6100e360048036038101906100de9190610c6f565b6102db565b6040516100f09190610cc7565b60405180910390f35b6101016102ee565b60405161010e9190610cef565b60405180910390f35b610131600480360381019061012c9190610d08565b6102f7565b60405161013e9190610cc7565b60405180910390f35b61014f61030c565b60405161015c9190610d73565b60405180910390f35b61017f600480360381019061017a9190610c6f565b610314565b005b61019b60048036038101906101969190610d8c565b610322565b6040516101a89190610cef565b60405180910390f35b6101cb60048036038101906101c69190610d08565b610333565b005b6101d5610343565b6040516101e29190610bbe565b60405180910390f35b61020560048036038101906102009190610c6f565b6103d3565b6040516102129190610cc7565b60405180910390f35b61023560048036038101906102309190610db7565b6103e6565b6040516102429190610cef565b60405180910390f35b60606003805461025a90610e22565b80601f016020809104026020016040519081016040528092919081815260200182805461028690610e22565b80156102d15780601f106102a8576101008083540402835291602001916102d1565b820191905f5260205f20905b8154815290600101906020018083116102b457829003601f168201915b5050505050905090565b5f6102e683836103f9565b905092915050565b5f600254905090565b5f61030384848461041b565b90509392505050565b5f6012905090565b61031e8282610449565b5050565b5f61032c826104c8565b9050919050565b61033e83838361050d565b505050565b60606004805461035290610e22565b80601f016020809104026020016040519081016040528092919081815260200182805461037e90610e22565b80156103c95780601f106103a0576101008083540402835291602001916103c9565b820191905f5260205f20905b8154815290600101906020018083116103ac57829003601f168201915b5050505050905090565b5f6103de838361051f565b905092915050565b5f6103f18383610541565b905092915050565b5f806104036105c3565b905061041081858561050d565b600191505092915050565b5f806104256105c3565b90506104328582856105ca565b61043d85858561065c565b60019150509392505050565b5f73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036104b9575f6040517fec442f050000000000000000000000000000000000000000000000000000000081526004016104b09190610e61565b60405180910390fd5b6104c45f838361074c565b5050565b5f805f8373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f20549050919050565b61051a8383836001610965565b505050565b5f806105296105c3565b905061053681858561065c565b600191505092915050565b5f60015f8473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f2054905092915050565b5f33905090565b5f6105d584846103e6565b90507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81146106565781811015610647578281836040517ffb8f41b200000000000000000000000000000000000000000000000000000000815260040161063e93929190610e7a565b60405180910390fd5b61065584848484035f610965565b5b50505050565b5f73ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16036106cc575f6040517f96c6fd1e0000000000000000000000000000000000000000000000000000000081526004016106c39190610e61565b60405180910390fd5b5f73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff160361073c575f6040517fec442f050000000000000000000000000000000000000000000000000000000081526004016107339190610e61565b60405180910390fd5b61074783838361074c565b505050565b5f73ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff160361079c578060025f8282546107909190610edc565b9250508190555061086a565b5f805f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f2054905081811015610825578381836040517fe450d38c00000000000000000000000000000000000000000000000000000000815260040161081c93929190610e7a565b60405180910390fd5b8181035f808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f2081905550505b5f73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036108b1578060025f82825403925050819055506108fb565b805f808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f82825401925050819055505b8173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040516109589190610cef565b60405180910390a3505050565b5f73ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff16036109d5575f6040517fe602df050000000000000000000000000000000000000000000000000000000081526004016109cc9190610e61565b60405180910390fd5b5f73ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1603610a45575f6040517f94280d62000000000000000000000000000000000000000000000000000000008152600401610a3c9190610e61565b60405180910390fd5b8160015f8673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f20819055508015610b2e578273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92584604051610b259190610cef565b60405180910390a35b50505050565b5f81519050919050565b5f82825260208201905092915050565b5f5b83811015610b6b578082015181840152602081019050610b50565b5f8484015250505050565b5f601f19601f8301169050919050565b5f610b9082610b34565b610b9a8185610b3e565b9350610baa818560208601610b4e565b610bb381610b76565b840191505092915050565b5f6020820190508181035f830152610bd68184610b86565b905092915050565b5f80fd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f610c0b82610be2565b9050919050565b610c1b81610c01565b8114610c25575f80fd5b50565b5f81359050610c3681610c12565b92915050565b5f819050919050565b610c4e81610c3c565b8114610c58575f80fd5b50565b5f81359050610c6981610c45565b92915050565b5f8060408385031215610c8557610c84610bde565b5b5f610c9285828601610c28565b9250506020610ca385828601610c5b565b9150509250929050565b5f8115159050919050565b610cc181610cad565b82525050565b5f602082019050610cda5f830184610cb8565b92915050565b610ce981610c3c565b82525050565b5f602082019050610d025f830184610ce0565b92915050565b5f805f60608486031215610d1f57610d1e610bde565b5b5f610d2c86828701610c28565b9350506020610d3d86828701610c28565b9250506040610d4e86828701610c5b565b9150509250925092565b5f60ff82169050919050565b610d6d81610d58565b82525050565b5f602082019050610d865f830184610d64565b92915050565b5f60208284031215610da157610da0610bde565b5b5f610dae84828501610c28565b91505092915050565b5f8060408385031215610dcd57610dcc610bde565b5b5f610dda85828601610c28565b9250506020610deb85828601610c28565b9150509250929050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b5f6002820490506001821680610e3957607f821691505b602082108103610e4c57610e4b610df5565b5b50919050565b610e5b81610c01565b82525050565b5f602082019050610e745f830184610e52565b92915050565b5f606082019050610e8d5f830186610e52565b610e9a6020830185610ce0565b610ea76040830184610ce0565b949350505050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f610ee682610c3c565b9150610ef183610c3c565b9250828201905080821115610f0957610f08610eaf565b5b9291505056fea2646970667358221220383e898342e74543d1bfb6186eff00b4ae7a39d4ecde6190742c5e9f2a7a2e9364736f6c63430008150033")] + // SPDX-License-Identifier: MIT + contract ERC20Mock is ERC20 { + constructor() ERC20("ERC20Mock", "MTK") {} + + function approve(address spender, uint256 value) public override returns (bool) { + return super.approve(spender, value); + } + + function regular_approve(address owner, address spender, uint256 amount) public { + super._approve(owner, spender, amount); + } + + function balanceOf(address account) public override view returns (uint256) { + return super.balanceOf(account); + } + + function mint(address account, uint256 value) public { + super._mint(account, value); + } + + function transfer(address to, uint256 amount) public override returns (bool) { + return super.transfer(to, amount); + } + + function transferFrom(address from, address to, uint256 value) public override returns (bool) { + return super.transferFrom(from, to, value); + } + + function allowance(address owner, address spender) public view override returns (uint256) { + return super.allowance(owner, spender); + } + } +} + +pub async fn deploy(wallet: &Wallet) -> eyre::Result
{ + // Deploy the contract. + let contract = ERC20Mock::deploy(wallet).await?; + Ok(*contract.address()) +} diff --git a/examples/safe-erc20/tests/mock/erc20_force_approve.rs b/examples/safe-erc20/tests/mock/erc20_force_approve.rs new file mode 100644 index 00000000..ccf5c414 --- /dev/null +++ b/examples/safe-erc20/tests/mock/erc20_force_approve.rs @@ -0,0 +1,33 @@ +#![allow(dead_code)] +#![cfg(feature = "e2e")] +use alloy::{primitives::Address, sol}; +use e2e::Wallet; + +sol! { + #[allow(missing_docs)] + // Built with Remix IDE; solc v0.8.21+commit.d9974bed + #[sol(rpc, bytecode="608060405234801562000010575f80fd5b506040518060400160405280601581526020017f4552433230466f726365417070726f76654d6f636b00000000000000000000008152506040518060400160405280600381526020017f46414d000000000000000000000000000000000000000000000000000000000081525081600390816200008e91906200030d565b508060049081620000a091906200030d565b505050620003f1565b5f81519050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b5f60028204905060018216806200012557607f821691505b6020821081036200013b576200013a620000e0565b5b50919050565b5f819050815f5260205f209050919050565b5f6020601f8301049050919050565b5f82821b905092915050565b5f600883026200019f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8262000162565b620001ab868362000162565b95508019841693508086168417925050509392505050565b5f819050919050565b5f819050919050565b5f620001f5620001ef620001e984620001c3565b620001cc565b620001c3565b9050919050565b5f819050919050565b6200021083620001d5565b620002286200021f82620001fc565b8484546200016e565b825550505050565b5f90565b6200023e62000230565b6200024b81848462000205565b505050565b5b818110156200027257620002665f8262000234565b60018101905062000251565b5050565b601f821115620002c1576200028b8162000141565b620002968462000153565b81016020851015620002a6578190505b620002be620002b58562000153565b83018262000250565b50505b505050565b5f82821c905092915050565b5f620002e35f1984600802620002c6565b1980831691505092915050565b5f620002fd8383620002d2565b9150826002028217905092915050565b6200031882620000a9565b67ffffffffffffffff811115620003345762000333620000b3565b5b6200034082546200010d565b6200034d82828562000276565b5f60209050601f83116001811462000383575f84156200036e578287015190505b6200037a8582620002f0565b865550620003e9565b601f198416620003938662000141565b5f5b82811015620003bc5784890151825560018201915060208501945060208101905062000395565b86831015620003dc5784890151620003d8601f891682620002d2565b8355505b6001600288020188555050505b505050505050565b610f1580620003ff5f395ff3fe608060405234801561000f575f80fd5b506004361061009c575f3560e01c806370a082311161006457806370a082311461015a5780638483acfe1461018a57806395d89b41146101a6578063a9059cbb146101c4578063dd62ed3e146101f45761009c565b806306fdde03146100a0578063095ea7b3146100be57806318160ddd146100ee57806323b872dd1461010c578063313ce5671461013c575b5f80fd5b6100a8610224565b6040516100b59190610b26565b60405180910390f35b6100d860048036038101906100d39190610bd7565b6102b4565b6040516100e59190610c2f565b60405180910390f35b6100f661031c565b6040516101039190610c57565b60405180910390f35b61012660048036038101906101219190610c70565b610325565b6040516101339190610c2f565b60405180910390f35b610144610353565b6040516101519190610cdb565b60405180910390f35b610174600480360381019061016f9190610cf4565b61035b565b6040516101819190610c57565b60405180910390f35b6101a4600480360381019061019f9190610c70565b6103a0565b005b6101ae6103b0565b6040516101bb9190610b26565b60405180910390f35b6101de60048036038101906101d99190610bd7565b610440565b6040516101eb9190610c2f565b60405180910390f35b61020e60048036038101906102099190610d1f565b610462565b60405161021b9190610c57565b60405180910390f35b60606003805461023390610d8a565b80601f016020809104026020016040519081016040528092919081815260200182805461025f90610d8a565b80156102aa5780601f10610281576101008083540402835291602001916102aa565b820191905f5260205f20905b81548152906001019060200180831161028d57829003601f168201915b5050505050905090565b5f808214806102cb57505f6102c93385610462565b145b61030a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161030190610e04565b60405180910390fd5b6103148383610475565b905092915050565b5f600254905090565b5f8061032f610497565b905061033c85828561049e565b610347858585610530565b60019150509392505050565b5f6012905090565b5f805f8373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f20549050919050565b6103ab838383610620565b505050565b6060600480546103bf90610d8a565b80601f01602080910402602001604051908101604052809291908181526020018280546103eb90610d8a565b80156104365780601f1061040d57610100808354040283529160200191610436565b820191905f5260205f20905b81548152906001019060200180831161041957829003601f168201915b5050505050905090565b5f8061044a610497565b9050610457818585610530565b600191505092915050565b5f61046d8383610632565b905092915050565b5f8061047f610497565b905061048c818585610620565b600191505092915050565b5f33905090565b5f6104a98484610462565b90507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff811461052a578181101561051b578281836040517ffb8f41b200000000000000000000000000000000000000000000000000000000815260040161051293929190610e31565b60405180910390fd5b61052984848484035f6106b4565b5b50505050565b5f73ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16036105a0575f6040517f96c6fd1e0000000000000000000000000000000000000000000000000000000081526004016105979190610e66565b60405180910390fd5b5f73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603610610575f6040517fec442f050000000000000000000000000000000000000000000000000000000081526004016106079190610e66565b60405180910390fd5b61061b838383610883565b505050565b61062d83838360016106b4565b505050565b5f60015f8473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f2054905092915050565b5f73ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff1603610724575f6040517fe602df0500000000000000000000000000000000000000000000000000000000815260040161071b9190610e66565b60405180910390fd5b5f73ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1603610794575f6040517f94280d6200000000000000000000000000000000000000000000000000000000815260040161078b9190610e66565b60405180910390fd5b8160015f8673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f2081905550801561087d578273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040516108749190610c57565b60405180910390a35b50505050565b5f73ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16036108d3578060025f8282546108c79190610eac565b925050819055506109a1565b5f805f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205490508181101561095c578381836040517fe450d38c00000000000000000000000000000000000000000000000000000000815260040161095393929190610e31565b60405180910390fd5b8181035f808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f2081905550505b5f73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036109e8578060025f8282540392505081905550610a32565b805f808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f82825401925050819055505b8173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef83604051610a8f9190610c57565b60405180910390a3505050565b5f81519050919050565b5f82825260208201905092915050565b5f5b83811015610ad3578082015181840152602081019050610ab8565b5f8484015250505050565b5f601f19601f8301169050919050565b5f610af882610a9c565b610b028185610aa6565b9350610b12818560208601610ab6565b610b1b81610ade565b840191505092915050565b5f6020820190508181035f830152610b3e8184610aee565b905092915050565b5f80fd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f610b7382610b4a565b9050919050565b610b8381610b69565b8114610b8d575f80fd5b50565b5f81359050610b9e81610b7a565b92915050565b5f819050919050565b610bb681610ba4565b8114610bc0575f80fd5b50565b5f81359050610bd181610bad565b92915050565b5f8060408385031215610bed57610bec610b46565b5b5f610bfa85828601610b90565b9250506020610c0b85828601610bc3565b9150509250929050565b5f8115159050919050565b610c2981610c15565b82525050565b5f602082019050610c425f830184610c20565b92915050565b610c5181610ba4565b82525050565b5f602082019050610c6a5f830184610c48565b92915050565b5f805f60608486031215610c8757610c86610b46565b5b5f610c9486828701610b90565b9350506020610ca586828701610b90565b9250506040610cb686828701610bc3565b9150509250925092565b5f60ff82169050919050565b610cd581610cc0565b82525050565b5f602082019050610cee5f830184610ccc565b92915050565b5f60208284031215610d0957610d08610b46565b5b5f610d1684828501610b90565b91505092915050565b5f8060408385031215610d3557610d34610b46565b5b5f610d4285828601610b90565b9250506020610d5385828601610b90565b9150509250929050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b5f6002820490506001821680610da157607f821691505b602082108103610db457610db3610d5d565b5b50919050565b7f5553445420617070726f76616c206661696c75726500000000000000000000005f82015250565b5f610dee601583610aa6565b9150610df982610dba565b602082019050919050565b5f6020820190508181035f830152610e1b81610de2565b9050919050565b610e2b81610b69565b82525050565b5f606082019050610e445f830186610e22565b610e516020830185610c48565b610e5e6040830184610c48565b949350505050565b5f602082019050610e795f830184610e22565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f610eb682610ba4565b9150610ec183610ba4565b9250828201905080821115610ed957610ed8610e7f565b5b9291505056fea26469706673582212204360bd31161a5a21172e203aecf5b49d953f1b1c8a64d9d0e539c9c81467f7b064736f6c63430008150033")] + // SPDX-License-Identifier: MIT + contract ERC20ForceApproveMock is ERC20 { + constructor() ERC20("ERC20ForceApproveMock", "FAM") {} + + function approve(address spender, uint256 amount) public virtual override returns (bool) { + require(amount == 0 || allowance(msg.sender, spender) == 0, "USDT approval failure"); + return super.approve(spender, amount); + } + + function regular_approve(address owner, address spender, uint256 amount) public { + super._approve(owner, spender, amount); + } + + function allowance(address owner, address spender) public view override returns (uint256) { + return super.allowance(owner, spender); + } + } +} + +pub async fn deploy(wallet: &Wallet) -> eyre::Result
{ + // Deploy the contract. + let contract = ERC20ForceApproveMock::deploy(wallet).await?; + Ok(*contract.address()) +} diff --git a/examples/safe-erc20/tests/mock/erc20_no_return.rs b/examples/safe-erc20/tests/mock/erc20_no_return.rs new file mode 100644 index 00000000..2b1189d9 --- /dev/null +++ b/examples/safe-erc20/tests/mock/erc20_no_return.rs @@ -0,0 +1,57 @@ +#![allow(dead_code)] +#![cfg(feature = "e2e")] +use alloy::{primitives::Address, sol}; +use e2e::Wallet; + +sol! { + #[allow(missing_docs)] + // Built with Remix IDE; solc v0.8.21+commit.d9974bed + #[sol(rpc, bytecode="608060405234801562000010575f80fd5b506040518060400160405280601181526020017f45524332304e6f52657475726e4d6f636b0000000000000000000000000000008152506040518060400160405280600381526020017f4e524d000000000000000000000000000000000000000000000000000000000081525081600390816200008e91906200030d565b508060049081620000a091906200030d565b505050620003f1565b5f81519050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b5f60028204905060018216806200012557607f821691505b6020821081036200013b576200013a620000e0565b5b50919050565b5f819050815f5260205f209050919050565b5f6020601f8301049050919050565b5f82821b905092915050565b5f600883026200019f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8262000162565b620001ab868362000162565b95508019841693508086168417925050509392505050565b5f819050919050565b5f819050919050565b5f620001f5620001ef620001e984620001c3565b620001cc565b620001c3565b9050919050565b5f819050919050565b6200021083620001d5565b620002286200021f82620001fc565b8484546200016e565b825550505050565b5f90565b6200023e62000230565b6200024b81848462000205565b505050565b5b818110156200027257620002665f8262000234565b60018101905062000251565b5050565b601f821115620002c1576200028b8162000141565b620002968462000153565b81016020851015620002a6578190505b620002be620002b58562000153565b83018262000250565b50505b505050565b5f82821c905092915050565b5f620002e35f1984600802620002c6565b1980831691505092915050565b5f620002fd8383620002d2565b9150826002028217905092915050565b6200031882620000a9565b67ffffffffffffffff811115620003345762000333620000b3565b5b6200034082546200010d565b6200034d82828562000276565b5f60209050601f83116001811462000383575f84156200036e578287015190505b6200037a8582620002f0565b865550620003e9565b601f198416620003938662000141565b5f5b82811015620003bc5784890151825560018201915060208501945060208101905062000395565b86831015620003dc5784890151620003d8601f891682620002d2565b8355505b6001600288020188555050505b505050505050565b610f3880620003ff5f395ff3fe608060405234801561000f575f80fd5b50600436106100a7575f3560e01c806340c10f191161006f57806340c10f191461016557806370a08231146101815780638483acfe146101b157806395d89b41146101cd578063a9059cbb146101eb578063dd62ed3e1461021b576100a7565b806306fdde03146100ab578063095ea7b3146100c957806318160ddd146100f957806323b872dd14610117578063313ce56714610147575b5f80fd5b6100b361024b565b6040516100c09190610bb1565b60405180910390f35b6100e360048036038101906100de9190610c62565b6102db565b6040516100f09190610cba565b60405180910390f35b6101016102ea565b60405161010e9190610ce2565b60405180910390f35b610131600480360381019061012c9190610cfb565b6102f3565b60405161013e9190610cba565b60405180910390f35b61014f610303565b60405161015c9190610d66565b60405180910390f35b61017f600480360381019061017a9190610c62565b61030b565b005b61019b60048036038101906101969190610d7f565b610319565b6040516101a89190610ce2565b60405180910390f35b6101cb60048036038101906101c69190610cfb565b61032a565b005b6101d561033a565b6040516101e29190610bb1565b60405180910390f35b61020560048036038101906102009190610c62565b6103ca565b6040516102129190610cba565b60405180910390f35b61023560048036038101906102309190610daa565b6103d9565b6040516102429190610ce2565b60405180910390f35b60606003805461025a90610e15565b80601f016020809104026020016040519081016040528092919081815260200182805461028690610e15565b80156102d15780601f106102a8576101008083540402835291602001916102d1565b820191905f5260205f20905b8154815290600101906020018083116102b457829003601f168201915b5050505050905090565b5f6102e683836103ec565b5f80f35b5f600254905090565b5f6102ff84848461040e565b5f80f35b5f6012905090565b610315828261043c565b5050565b5f610323826104bb565b9050919050565b610335838383610500565b505050565b60606004805461034990610e15565b80601f016020809104026020016040519081016040528092919081815260200182805461037590610e15565b80156103c05780601f10610397576101008083540402835291602001916103c0565b820191905f5260205f20905b8154815290600101906020018083116103a357829003601f168201915b5050505050905090565b5f6103d58383610512565b5f80f35b5f6103e48383610534565b905092915050565b5f806103f66105b6565b9050610403818585610500565b600191505092915050565b5f806104186105b6565b90506104258582856105bd565b61043085858561064f565b60019150509392505050565b5f73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036104ac575f6040517fec442f050000000000000000000000000000000000000000000000000000000081526004016104a39190610e54565b60405180910390fd5b6104b75f838361073f565b5050565b5f805f8373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f20549050919050565b61050d8383836001610958565b505050565b5f8061051c6105b6565b905061052981858561064f565b600191505092915050565b5f60015f8473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f2054905092915050565b5f33905090565b5f6105c884846103d9565b90507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8114610649578181101561063a578281836040517ffb8f41b200000000000000000000000000000000000000000000000000000000815260040161063193929190610e6d565b60405180910390fd5b61064884848484035f610958565b5b50505050565b5f73ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16036106bf575f6040517f96c6fd1e0000000000000000000000000000000000000000000000000000000081526004016106b69190610e54565b60405180910390fd5b5f73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff160361072f575f6040517fec442f050000000000000000000000000000000000000000000000000000000081526004016107269190610e54565b60405180910390fd5b61073a83838361073f565b505050565b5f73ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff160361078f578060025f8282546107839190610ecf565b9250508190555061085d565b5f805f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f2054905081811015610818578381836040517fe450d38c00000000000000000000000000000000000000000000000000000000815260040161080f93929190610e6d565b60405180910390fd5b8181035f808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f2081905550505b5f73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036108a4578060025f82825403925050819055506108ee565b805f808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f82825401925050819055505b8173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8360405161094b9190610ce2565b60405180910390a3505050565b5f73ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff16036109c8575f6040517fe602df050000000000000000000000000000000000000000000000000000000081526004016109bf9190610e54565b60405180910390fd5b5f73ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1603610a38575f6040517f94280d62000000000000000000000000000000000000000000000000000000008152600401610a2f9190610e54565b60405180910390fd5b8160015f8673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f20819055508015610b21578273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92584604051610b189190610ce2565b60405180910390a35b50505050565b5f81519050919050565b5f82825260208201905092915050565b5f5b83811015610b5e578082015181840152602081019050610b43565b5f8484015250505050565b5f601f19601f8301169050919050565b5f610b8382610b27565b610b8d8185610b31565b9350610b9d818560208601610b41565b610ba681610b69565b840191505092915050565b5f6020820190508181035f830152610bc98184610b79565b905092915050565b5f80fd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f610bfe82610bd5565b9050919050565b610c0e81610bf4565b8114610c18575f80fd5b50565b5f81359050610c2981610c05565b92915050565b5f819050919050565b610c4181610c2f565b8114610c4b575f80fd5b50565b5f81359050610c5c81610c38565b92915050565b5f8060408385031215610c7857610c77610bd1565b5b5f610c8585828601610c1b565b9250506020610c9685828601610c4e565b9150509250929050565b5f8115159050919050565b610cb481610ca0565b82525050565b5f602082019050610ccd5f830184610cab565b92915050565b610cdc81610c2f565b82525050565b5f602082019050610cf55f830184610cd3565b92915050565b5f805f60608486031215610d1257610d11610bd1565b5b5f610d1f86828701610c1b565b9350506020610d3086828701610c1b565b9250506040610d4186828701610c4e565b9150509250925092565b5f60ff82169050919050565b610d6081610d4b565b82525050565b5f602082019050610d795f830184610d57565b92915050565b5f60208284031215610d9457610d93610bd1565b5b5f610da184828501610c1b565b91505092915050565b5f8060408385031215610dc057610dbf610bd1565b5b5f610dcd85828601610c1b565b9250506020610dde85828601610c1b565b9150509250929050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b5f6002820490506001821680610e2c57607f821691505b602082108103610e3f57610e3e610de8565b5b50919050565b610e4e81610bf4565b82525050565b5f602082019050610e675f830184610e45565b92915050565b5f606082019050610e805f830186610e45565b610e8d6020830185610cd3565b610e9a6040830184610cd3565b949350505050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f610ed982610c2f565b9150610ee483610c2f565b9250828201905080821115610efc57610efb610ea2565b5b9291505056fea2646970667358221220e66fa5f170a573315c29c69b9be493f577016746885cba3ad5a88b793a2bebac64736f6c63430008150033")] + // SPDX-License-Identifier: MIT + contract ERC20NoReturnMock is ERC20 { + constructor() ERC20("ERC20NoReturnMock", "NRM") {} + + function transfer(address to, uint256 amount) public override returns (bool) { + super.transfer(to, amount); + assembly { + return(0, 0) + } + } + + function transferFrom(address from, address to, uint256 amount) public override returns (bool) { + super.transferFrom(from, to, amount); + assembly { + return(0, 0) + } + } + + function approve(address spender, uint256 amount) public override returns (bool) { + super.approve(spender, amount); + assembly { + return(0, 0) + } + } + + function regular_approve(address owner, address spender, uint256 amount) public { + super._approve(owner, spender, amount); + } + + function balanceOf(address account) public override view returns (uint256) { + return super.balanceOf(account); + } + + function mint(address account, uint256 value) public { + super._mint(account, value); + } + + function allowance(address owner, address spender) public view override returns (uint256) { + return super.allowance(owner, spender); + } + } +} + +pub async fn deploy(wallet: &Wallet) -> eyre::Result
{ + // Deploy the contract. + let contract = ERC20NoReturnMock::deploy(wallet).await?; + Ok(*contract.address()) +} diff --git a/examples/safe-erc20/tests/mock/erc20_return_false.rs b/examples/safe-erc20/tests/mock/erc20_return_false.rs new file mode 100644 index 00000000..809c8062 --- /dev/null +++ b/examples/safe-erc20/tests/mock/erc20_return_false.rs @@ -0,0 +1,44 @@ +#![allow(dead_code)] +#![cfg(feature = "e2e")] +use alloy::{primitives::Address, sol}; +use e2e::Wallet; + +sol! { + #[allow(missing_docs)] + // Built with Remix IDE; solc v0.8.21+commit.d9974bed + #[sol(rpc, bytecode="608060405234801562000010575f80fd5b506040518060400160405280601481526020017f455243323052657475726e46616c73654d6f636b0000000000000000000000008152506040518060400160405280600381526020017f52464d000000000000000000000000000000000000000000000000000000000081525081600390816200008e91906200030d565b508060049081620000a091906200030d565b505050620003f1565b5f81519050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b5f60028204905060018216806200012557607f821691505b6020821081036200013b576200013a620000e0565b5b50919050565b5f819050815f5260205f209050919050565b5f6020601f8301049050919050565b5f82821b905092915050565b5f600883026200019f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8262000162565b620001ab868362000162565b95508019841693508086168417925050509392505050565b5f819050919050565b5f819050919050565b5f620001f5620001ef620001e984620001c3565b620001cc565b620001c3565b9050919050565b5f819050919050565b6200021083620001d5565b620002286200021f82620001fc565b8484546200016e565b825550505050565b5f90565b6200023e62000230565b6200024b81848462000205565b505050565b5b818110156200027257620002665f8262000234565b60018101905062000251565b5050565b601f821115620002c1576200028b8162000141565b620002968462000153565b81016020851015620002a6578190505b620002be620002b58562000153565b83018262000250565b50505b505050565b5f82821c905092915050565b5f620002e35f1984600802620002c6565b1980831691505092915050565b5f620002fd8383620002d2565b9150826002028217905092915050565b6200031882620000a9565b67ffffffffffffffff811115620003345762000333620000b3565b5b6200034082546200010d565b6200034d82828562000276565b5f60209050601f83116001811462000383575f84156200036e578287015190505b6200037a8582620002f0565b865550620003e9565b601f198416620003938662000141565b5f5b82811015620003bc5784890151825560018201915060208501945060208101905062000395565b86831015620003dc5784890151620003d8601f891682620002d2565b8355505b6001600288020188555050505b505050505050565b610b0d80620003ff5f395ff3fe608060405234801561000f575f80fd5b506004361061009c575f3560e01c806340c10f191161006457806340c10f191461015a57806370a082311461017657806395d89b41146101a6578063a9059cbb146101c4578063dd62ed3e146101f45761009c565b806306fdde03146100a0578063095ea7b3146100be57806318160ddd146100ee57806323b872dd1461010c578063313ce5671461013c575b5f80fd5b6100a8610224565b6040516100b59190610786565b60405180910390f35b6100d860048036038101906100d39190610837565b6102b4565b6040516100e5919061088f565b60405180910390f35b6100f66102bb565b60405161010391906108b7565b60405180910390f35b610126600480360381019061012191906108d0565b6102c4565b604051610133919061088f565b60405180910390f35b6101446102cc565b604051610151919061093b565b60405180910390f35b610174600480360381019061016f9190610837565b6102d4565b005b610190600480360381019061018b9190610954565b6102e2565b60405161019d91906108b7565b60405180910390f35b6101ae6102f3565b6040516101bb9190610786565b60405180910390f35b6101de60048036038101906101d99190610837565b610383565b6040516101eb919061088f565b60405180910390f35b61020e6004803603810190610209919061097f565b61038a565b60405161021b91906108b7565b60405180910390f35b606060038054610233906109ea565b80601f016020809104026020016040519081016040528092919081815260200182805461025f906109ea565b80156102aa5780601f10610281576101008083540402835291602001916102aa565b820191905f5260205f20905b81548152906001019060200180831161028d57829003601f168201915b5050505050905090565b5f92915050565b5f600254905090565b5f9392505050565b5f6012905090565b6102de828261039d565b5050565b5f6102ec8261041c565b9050919050565b606060048054610302906109ea565b80601f016020809104026020016040519081016040528092919081815260200182805461032e906109ea565b80156103795780601f1061035057610100808354040283529160200191610379565b820191905f5260205f20905b81548152906001019060200180831161035c57829003601f168201915b5050505050905090565b5f92915050565b5f6103958383610461565b905092915050565b5f73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff160361040d575f6040517fec442f050000000000000000000000000000000000000000000000000000000081526004016104049190610a29565b60405180910390fd5b6104185f83836104e3565b5050565b5f805f8373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f20549050919050565b5f60015f8473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f2054905092915050565b5f73ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1603610533578060025f8282546105279190610a6f565b92505081905550610601565b5f805f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f20549050818110156105bc578381836040517fe450d38c0000000000000000000000000000000000000000000000000000000081526004016105b393929190610aa2565b60405180910390fd5b8181035f808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f2081905550505b5f73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603610648578060025f8282540392505081905550610692565b805f808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f82825401925050819055505b8173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040516106ef91906108b7565b60405180910390a3505050565b5f81519050919050565b5f82825260208201905092915050565b5f5b83811015610733578082015181840152602081019050610718565b5f8484015250505050565b5f601f19601f8301169050919050565b5f610758826106fc565b6107628185610706565b9350610772818560208601610716565b61077b8161073e565b840191505092915050565b5f6020820190508181035f83015261079e818461074e565b905092915050565b5f80fd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6107d3826107aa565b9050919050565b6107e3816107c9565b81146107ed575f80fd5b50565b5f813590506107fe816107da565b92915050565b5f819050919050565b61081681610804565b8114610820575f80fd5b50565b5f813590506108318161080d565b92915050565b5f806040838503121561084d5761084c6107a6565b5b5f61085a858286016107f0565b925050602061086b85828601610823565b9150509250929050565b5f8115159050919050565b61088981610875565b82525050565b5f6020820190506108a25f830184610880565b92915050565b6108b181610804565b82525050565b5f6020820190506108ca5f8301846108a8565b92915050565b5f805f606084860312156108e7576108e66107a6565b5b5f6108f4868287016107f0565b9350506020610905868287016107f0565b925050604061091686828701610823565b9150509250925092565b5f60ff82169050919050565b61093581610920565b82525050565b5f60208201905061094e5f83018461092c565b92915050565b5f60208284031215610969576109686107a6565b5b5f610976848285016107f0565b91505092915050565b5f8060408385031215610995576109946107a6565b5b5f6109a2858286016107f0565b92505060206109b3858286016107f0565b9150509250929050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b5f6002820490506001821680610a0157607f821691505b602082108103610a1457610a136109bd565b5b50919050565b610a23816107c9565b82525050565b5f602082019050610a3c5f830184610a1a565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f610a7982610804565b9150610a8483610804565b9250828201905080821115610a9c57610a9b610a42565b5b92915050565b5f606082019050610ab55f830186610a1a565b610ac260208301856108a8565b610acf60408301846108a8565b94935050505056fea26469706673582212204aac6dd6254b82f37f30add0ed2937474eced0bafc505b611f66b99ebe39999e64736f6c63430008150033")] + // SPDX-License-Identifier: MIT + contract ERC20ReturnFalseMock is ERC20 { + constructor() ERC20("ERC20ReturnFalseMock", "RFM") {} + + function approve(address, uint256) public override returns (bool) { + return false; + } + + function transfer(address, uint256) public override returns (bool) { + return false; + } + + function transferFrom(address, address, uint256) public override returns (bool) { + return false; + } + + function balanceOf(address account) public override view returns (uint256) { + return super.balanceOf(account); + } + + function mint(address account, uint256 value) public { + super._mint(account, value); + } + + function allowance(address owner, address spender) public view override returns (uint256) { + return super.allowance(owner, spender); + } + } +} + +pub async fn deploy(wallet: &Wallet) -> eyre::Result
{ + // Deploy the contract. + let contract = ERC20ReturnFalseMock::deploy(wallet).await?; + Ok(*contract.address()) +} diff --git a/examples/safe-erc20/tests/mock/mod.rs b/examples/safe-erc20/tests/mock/mod.rs new file mode 100644 index 00000000..c8c60bd7 --- /dev/null +++ b/examples/safe-erc20/tests/mock/mod.rs @@ -0,0 +1,4 @@ +pub mod erc20; +pub mod erc20_force_approve; +pub mod erc20_no_return; +pub mod erc20_return_false; diff --git a/examples/safe-erc20/tests/usdt_approval_behavior.rs b/examples/safe-erc20/tests/usdt_approval_behavior.rs new file mode 100644 index 00000000..9530a3ca --- /dev/null +++ b/examples/safe-erc20/tests/usdt_approval_behavior.rs @@ -0,0 +1,138 @@ +#![cfg(feature = "e2e")] + +use abi::{Erc20, SafeErc20}; +use alloy::primitives::uint; +use e2e::{receipt, watch, Account, EventExt, ReceiptExt}; +use mock::{erc20_force_approve, erc20_force_approve::ERC20ForceApproveMock}; + +mod abi; +mod mock; + +#[e2e::test] +async fn safe_increase_allowance_works( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let safe_erc20_addr = alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = SafeErc20::new(safe_erc20_addr, &alice.wallet); + let bob_addr = bob.address(); + + let erc20_address = erc20_force_approve::deploy(&alice.wallet).await?; + let erc20_alice = ERC20ForceApproveMock::new(erc20_address, &alice.wallet); + + let init_approval = uint!(100_U256); + let value = uint!(10_U256); + + let _ = watch!(erc20_alice.regular_approve( + safe_erc20_addr, + bob_addr, + init_approval + )); + + let initial_bob_allowance = + erc20_alice.allowance(safe_erc20_addr, bob_addr).call().await?._0; + assert_eq!(initial_bob_allowance, init_approval); + + let receipt = receipt!(safe_erc20_alice.safeIncreaseAllowance( + erc20_address, + bob_addr, + value + ))?; + + assert!(receipt.emits(Erc20::Approval { + owner: safe_erc20_addr, + spender: bob_addr, + value: init_approval + value, + })); + + let bob_allowance = + erc20_alice.allowance(safe_erc20_addr, bob_addr).call().await?._0; + assert_eq!(bob_allowance, init_approval + value); + + Ok(()) +} + +#[e2e::test] +async fn safe_decrease_allowance_works( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let safe_erc20_addr = alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = SafeErc20::new(safe_erc20_addr, &alice.wallet); + let bob_addr = bob.address(); + + let erc20_address = erc20_force_approve::deploy(&alice.wallet).await?; + let erc20_alice = ERC20ForceApproveMock::new(erc20_address, &alice.wallet); + + let init_approval = uint!(100_U256); + let value = uint!(10_U256); + + let _ = watch!(erc20_alice.regular_approve( + safe_erc20_addr, + bob_addr, + init_approval + )); + + let initial_bob_allowance = + erc20_alice.allowance(safe_erc20_addr, bob_addr).call().await?._0; + assert_eq!(initial_bob_allowance, init_approval); + + let receipt = receipt!(safe_erc20_alice.safeDecreaseAllowance( + erc20_address, + bob_addr, + value + ))?; + + assert!(receipt.emits(Erc20::Approval { + owner: safe_erc20_addr, + spender: bob_addr, + value: init_approval - value, + })); + + let bob_allowance = + erc20_alice.allowance(safe_erc20_addr, bob_addr).call().await?._0; + assert_eq!(bob_allowance, init_approval - value); + + Ok(()) +} + +#[e2e::test] +async fn force_approve_works(alice: Account, bob: Account) -> eyre::Result<()> { + let safe_erc20_addr = alice.as_deployer().deploy().await?.address()?; + let safe_erc20_alice = SafeErc20::new(safe_erc20_addr, &alice.wallet); + let bob_addr = bob.address(); + + let erc20_address = erc20_force_approve::deploy(&alice.wallet).await?; + let erc20_alice = ERC20ForceApproveMock::new(erc20_address, &alice.wallet); + + let init_approval = uint!(100_U256); + let updated_approval = uint!(10_U256); + + let _ = watch!(erc20_alice.regular_approve( + safe_erc20_addr, + bob_addr, + init_approval + )); + + let initial_bob_allowance = + erc20_alice.allowance(safe_erc20_addr, bob_addr).call().await?._0; + assert_eq!(initial_bob_allowance, init_approval); + + let receipt = receipt!(safe_erc20_alice.forceApprove( + erc20_address, + bob_addr, + updated_approval + ))?; + + assert!(receipt.emits(Erc20::Approval { + owner: safe_erc20_addr, + spender: bob_addr, + value: updated_approval, + })); + + let bob_allowance = + erc20_alice.allowance(safe_erc20_addr, bob_addr).call().await?._0; + assert_eq!(bob_allowance, updated_approval); + + Ok(()) +} diff --git a/lib/crypto/Cargo.toml b/lib/crypto/Cargo.toml index f183c640..16e3eab0 100644 --- a/lib/crypto/Cargo.toml +++ b/lib/crypto/Cargo.toml @@ -10,10 +10,16 @@ version.workspace = true [dependencies] tiny-keccak.workspace = true +num-traits.workspace = true +zeroize.workspace = true +crypto-bigint.workspace = true +educe.workspace = true +hex-literal.workspace = true [dev-dependencies] -hex-literal = "0.4.1" +crypto-bigint = { workspace = true, default-features = false, features = ["rand"] } rand.workspace = true +proptest.workspace = true [features] std = [] diff --git a/lib/crypto/proptest-regressions/field/fp.txt b/lib/crypto/proptest-regressions/field/fp.txt new file mode 100644 index 00000000..341ba3ff --- /dev/null +++ b/lib/crypto/proptest-regressions/field/fp.txt @@ -0,0 +1,6 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. diff --git a/lib/crypto/src/bigint.rs b/lib/crypto/src/bigint.rs new file mode 100644 index 00000000..1d88ea4d --- /dev/null +++ b/lib/crypto/src/bigint.rs @@ -0,0 +1,277 @@ +//! This module provides a generic interface and constant +//! functions for big integers. + +use core::{ + fmt::{Debug, Display}, + ops::{ + BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Shl, + ShlAssign, Shr, ShrAssign, + }, +}; + +#[allow(clippy::module_name_repetitions)] +pub use crypto_bigint; +use crypto_bigint::{Integer, Limb, Uint, Word, Zero}; +use zeroize::Zeroize; + +use crate::bits::BitIteratorBE; + +/// Defines a big integer with a constant length. +pub trait BigInteger: + 'static + + Copy + + Clone + + Debug + + Default + + Display + + Eq + + Ord + + Send + + Sized + + Sync + + Zeroize + + From + + From + + From + + From + + BitXorAssign + + for<'a> BitXorAssign<&'a Self> + + BitXor + + for<'a> BitXor<&'a Self, Output = Self> + + BitAndAssign + + for<'a> BitAndAssign<&'a Self> + + BitAnd + + for<'a> BitAnd<&'a Self, Output = Self> + + BitOrAssign + + for<'a> BitOrAssign<&'a Self> + + BitOr + + for<'a> BitOr<&'a Self, Output = Self> + + Shr + + ShrAssign + + Shl + + ShlAssign +{ + /// Number of `usize` limbs representing `Self`. + const NUM_LIMBS: usize; + + /// Returns true if this number is odd. + /// # Example + /// + /// ``` + /// use openzeppelin_crypto::bigint::{BigInteger, crypto_bigint::U64}; + /// + /// let mut one = U64::from(1u64); + /// assert!(one.is_odd()); + /// ``` + fn is_odd(&self) -> bool; + + /// Returns true if this number is even. + /// + /// # Example + /// + /// ``` + /// use openzeppelin_crypto::bigint::{BigInteger, crypto_bigint::U64}; + /// + /// let mut two = U64::from(2u64); + /// assert!(two.is_even()); + /// ``` + fn is_even(&self) -> bool; + + /// Returns true if this number is zero. + /// + /// # Example + /// + /// ``` + /// use openzeppelin_crypto::bigint::{BigInteger, crypto_bigint::U64}; + /// + /// let mut zero = U64::from(0u64); + /// assert!(zero.is_zero()); + /// ``` + fn is_zero(&self) -> bool; + + /// Compute the minimum number of bits needed to encode this number. + /// # Example + /// ``` + /// use openzeppelin_crypto::bigint::{BigInteger, crypto_bigint::U64}; + /// + /// let zero = U64::from(0u64); + /// assert_eq!(zero.num_bits(), 0); + /// let one = U64::from(1u64); + /// assert_eq!(one.num_bits(), 1); + /// let max = U64::from(u64::MAX); + /// assert_eq!(max.num_bits(), 64); + /// let u32_max = U64::from(u32::MAX as u64); + /// assert_eq!(u32_max.num_bits(), 32); + /// ``` + fn num_bits(&self) -> usize; + + /// Compute the `i`-th bit of `self`. + /// # Example + /// + /// ``` + /// use openzeppelin_crypto::bigint::{BigInteger, crypto_bigint::U64}; + /// + /// let mut one = U64::from(1u64); + /// assert!(one.get_bit(0)); + /// assert!(!one.get_bit(1)); + /// ``` + fn get_bit(&self, i: usize) -> bool; +} + +impl BigInteger for Uint { + const NUM_LIMBS: usize = N; + + fn is_odd(&self) -> bool { + as Integer>::is_odd(self).into() + } + + fn is_even(&self) -> bool { + as Integer>::is_even(self).into() + } + + fn is_zero(&self) -> bool { + as Zero>::is_zero(self).into() + } + + fn num_bits(&self) -> usize { + self.bits() + } + + fn get_bit(&self, i: usize) -> bool { + self.bit(i).into() + } +} + +impl BitIteratorBE for Uint { + fn bit_be_iter(&self) -> impl Iterator { + self.as_words().iter().rev().flat_map(Word::bit_be_iter) + } +} + +/// Parse a number from a string in a given radix. +/// +/// I.e., convert string encoded integer `s` to base-`radix` number. +#[must_use] +pub const fn from_str_radix( + s: &str, + radix: u32, +) -> Uint { + let bytes = s.as_bytes(); + + // The lowest order number is at the end of the string. + // Begin parsing from the last index of the string. + let mut index = bytes.len() - 1; + + let mut uint = Uint::from_u32(0); + let mut order = Uint::from_u32(1); + let uint_radix = Uint::from_u32(radix); + + loop { + // Try to parse a digit from utf-8 byte + let ch = parse_utf8_byte(bytes[index]); + let digit = match ch.to_digit(radix) { + None => { + panic!("invalid digit"); + } + Some(digit) => Uint::from_u32(digit), + }; + + // Add a digit multiplied by order. + uint = add(&uint, &mul(&digit, &order)); + + // If we reached the beginning of the string, return the number. + if index == 0 { + return uint; + } + + // Increase the order of magnitude. + order = mul(&uint_radix, &order); + + // Move to the next digit. + index -= 1; + } +} + +/// Multiply two numbers and panic on overflow. +#[must_use] +pub const fn mul( + a: &Uint, + b: &Uint, +) -> Uint { + let (low, high) = a.mul_wide(b); + assert!(high.bits() == 0, "overflow on multiplication"); + low +} + +/// Add two numbers and panic on overflow. +#[must_use] +pub const fn add( + a: &Uint, + b: &Uint, +) -> Uint { + let (low, carry) = a.adc(b, Limb::ZERO); + assert!(carry.0 == 0, "overflow on addition"); + low +} + +/// Parse a single UTF-8 byte. +pub(crate) const fn parse_utf8_byte(byte: u8) -> char { + match byte { + 0x00..=0x7F => byte as char, + _ => panic!("non-ASCII character found"), + } +} + +/// This macro converts a string base-10 number to a big integer. +#[macro_export] +macro_rules! from_num { + ($num:literal) => { + $crate::bigint::from_str_radix($num, 10) + }; +} + +/// This macro converts a string hex number to a big integer. +#[macro_export] +macro_rules! from_hex { + ($num:literal) => { + $crate::bigint::crypto_bigint::Uint::from_be_hex($num) + }; +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn convert_from_str_radix() { + let uint_from_base10 = from_str_radix::<4>( + "28948022309329048855892746252171976963363056481941647379679742748393362948097", + 10 + ); + #[allow(clippy::unreadable_literal)] + let expected = Uint::<4>::from_words([ + 10108024940646105089u64, + 2469829653919213789u64, + 0u64, + 4611686018427387904u64, + ]); + assert_eq!(uint_from_base10, expected); + + let uint_from_base10 = from_str_radix::<1>("18446744069414584321", 10); + let uint_from_binary = from_str_radix::<1>( + "1111111111111111111111111111111100000000000000000000000000000001", + 2, + ); + assert_eq!(uint_from_base10, uint_from_binary); + } + + #[test] + fn uint_bit_iterator_be() { + let words: [Word; 4] = [0b1100, 0, 0, 0]; + let num = Uint::<4>::from_words(words); + let bits: Vec = num.bit_be_trimmed_iter().collect(); + + assert_eq!(bits.len(), 4); + assert_eq!(bits, vec![true, true, false, false]); + } +} diff --git a/lib/crypto/src/bits.rs b/lib/crypto/src/bits.rs new file mode 100644 index 00000000..42a3f859 --- /dev/null +++ b/lib/crypto/src/bits.rs @@ -0,0 +1,46 @@ +//! Bit manipulation utilities. + +/// Iterates over bits in big-endian order. +pub trait BitIteratorBE { + /// Returns an iterator over the bits of the integer, starting from the most + /// significant bit. + fn bit_be_iter(&self) -> impl Iterator; + + /// Returns an iterator over the bits of the integer, starting from the most + /// significant bit, and without leading zeroes. + fn bit_be_trimmed_iter(&self) -> impl Iterator { + self.bit_be_iter().skip_while(|&b| !b) + } +} + +macro_rules! impl_bit_iter_be { + ($int:ty) => { + impl BitIteratorBE for $int { + fn bit_be_iter(&self) -> impl Iterator { + (0..<$int>::BITS).rev().map(move |i| self & (1 << i) != 0) + } + } + }; +} + +impl_bit_iter_be!(u8); +impl_bit_iter_be!(u16); +impl_bit_iter_be!(u32); +impl_bit_iter_be!(u64); +impl_bit_iter_be!(u128); +impl_bit_iter_be!(usize); + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn u64_bit_iterator_be() { + let num: u64 = 0b1100; + let bits: Vec = num.bit_be_trimmed_iter().collect(); + + assert_eq!(bits.len(), 4); + assert_eq!(bits, vec![true, true, false, false]); + } +} diff --git a/lib/crypto/src/field/fp.rs b/lib/crypto/src/field/fp.rs new file mode 100644 index 00000000..3e43758c --- /dev/null +++ b/lib/crypto/src/field/fp.rs @@ -0,0 +1,996 @@ +//! This module contains the implementation of a prime field element [`Fp`], +//! altogether with exact implementations [`Fp64`] for 64-bit, [`Fp128`] for +//! 128-bit elements and so on. +//! +//! Finite field element [`Fp`] wraps a biginteger element in [motgomery form], +//! which is used for efficient multiplication and division. +//! +//! Note that implementation of `Ord` for [`Fp`] compares field elements viewing +//! them as integers in the range `0, 1, ..., P::MODULUS - 1`. +//! However, other implementations of `PrimeField` might choose a different +//! ordering, and as such, users should use this `Ord` for applications where +//! any ordering suffices (like in a `BTreeMap`), and not in applications +//! where a particular ordering is required. +//! +//! [motgomery form]: https://en.wikipedia.org/wiki/Montgomery_modular_multiplication +use alloc::string::ToString; +use core::{ + cmp::Ordering, + fmt::{Debug, Display, Formatter}, + hash::{Hash, Hasher}, + marker::PhantomData, +}; + +use crypto_bigint::{ + modular::{ + constant_mod::{Residue, ResidueParams}, + montgomery_reduction, + }, + Limb, Uint, Word, +}; +use educe::Educe; +use num_traits::{One, Zero}; + +use crate::field::{group::AdditiveGroup, prime::PrimeField, Field}; + +/// A trait that specifies the configuration of a prime field. +/// Also specifies how to perform arithmetic on field elements. +pub trait FpParams: Send + Sync + 'static + Sized { + /// The modulus of the field. + const MODULUS: Uint; + + /// A multiplicative generator of the field. + /// [`Self::GENERATOR`] is an element having multiplicative order + /// `MODULUS - 1`. + const GENERATOR: Fp; + + /// Set `a += b`. + fn add_assign(a: &mut Fp, b: &Fp) { + a.residue += b.residue; + } + + /// Set `a -= b`. + fn sub_assign(a: &mut Fp, b: &Fp) { + a.residue -= b.residue; + } + + /// Set `a = a + a`. + fn double_in_place(a: &mut Fp) { + a.residue += a.residue; + } + + /// Set `a = -a`; + fn neg_in_place(a: &mut Fp) { + a.residue = a.residue.neg(); + } + + /// Set `a *= b`. + fn mul_assign(a: &mut Fp, b: &Fp) { + a.residue *= b.residue; + } + + /// Set `a *= a`. + fn square_in_place(a: &mut Fp) { + a.residue = a.residue.square(); + } + + /// Compute `a^{-1}` if `a` is not zero. + #[must_use] + fn inverse(a: &Fp) -> Option> { + let (residue, choice) = a.residue.invert(); + let is_inverse: bool = choice.into(); + + is_inverse.then_some(Fp { residue }) + } + + /// Construct a field element from an integer. + /// + /// By the end element will be converted to a montgomery form and reduced. + #[must_use] + fn from_bigint(r: Uint) -> Fp { + Fp::new(r) + } + + /// Convert a field element to an integer less than [`Self::MODULUS`]. + #[must_use] + fn into_bigint(a: Fp) -> Uint { + a.residue.retrieve() + } +} + +/// Represents an element of the prime field `F_p`, where `p == P::MODULUS`. +/// +/// This type can represent elements in any field of size at most N * 64 bits +/// for 64-bit systems and N * 32 bits for 32-bit systems. +#[derive(Educe)] +#[educe(Default, Clone, Copy, PartialEq, Eq)] +pub struct Fp, const LIMBS: usize> { + /// Contains the element in Montgomery form for efficient multiplication. + /// To convert an element to a [`BigInt`], use [`FpParams::into_bigint`] + /// or `into`. + residue: Residue, LIMBS>, +} + +/// Declare [`Fp`] types for different bit sizes. +macro_rules! declare_fp { + ($fp:ident, $limbs:ident, $bits:expr) => { + #[doc = "Finite field with max"] + #[doc = stringify!($bits)] + #[doc = "bits size element."] + pub type $fp

= crate::field::fp::Fp< + P, + { usize::div_ceil($bits, ::crypto_bigint::Word::BITS as usize) }, + >; + + #[doc = "Number of limbs in the field with"] + #[doc = stringify!($bits)] + #[doc = "bits size element."] + pub const $limbs: usize = + usize::div_ceil($bits, ::crypto_bigint::Word::BITS as usize); + }; +} + +declare_fp!(Fp64, LIMBS_64, 64); +declare_fp!(Fp128, LIMBS_128, 128); +declare_fp!(Fp192, LIMBS_192, 192); +declare_fp!(Fp256, LIMBS_256, 256); +declare_fp!(Fp320, LIMBS_320, 320); +declare_fp!(Fp384, LIMBS_384, 384); +declare_fp!(Fp448, LIMBS_448, 448); +declare_fp!(Fp512, LIMBS_512, 512); +declare_fp!(Fp576, LIMBS_576, 576); +declare_fp!(Fp640, LIMBS_640, 640); +declare_fp!(Fp704, LIMBS_704, 704); +declare_fp!(Fp768, LIMBS_768, 768); +declare_fp!(Fp832, LIMBS_832, 832); + +#[derive(Educe)] +#[educe(Clone, Copy, Debug, Default, Eq, PartialEq)] +struct ResidueParam, const LIMBS: usize>(PhantomData

); + +impl, const LIMBS: usize> ResidueParams + for ResidueParam +{ + const LIMBS: usize = LIMBS; + const MODULUS: Uint = { + let modulus = P::MODULUS; + // Uint represents integer in low-endian form. + assert!(modulus.as_limbs()[0].0 & 1 == 1, "modulus must be odd"); + modulus + }; + const MOD_NEG_INV: Limb = Limb(Word::MIN.wrapping_sub( + P::MODULUS.inv_mod2k_vartime(Word::BITS as usize).as_limbs()[0].0, + )); + const R: Uint = + Uint::MAX.const_rem(&P::MODULUS).0.wrapping_add(&Uint::ONE); + const R2: Uint = + Uint::const_rem_wide(Self::R.square_wide(), &P::MODULUS).0; + const R3: Uint = montgomery_reduction( + &Self::R2.square_wide(), + &P::MODULUS, + Self::MOD_NEG_INV, + ); +} + +impl, const LIMBS: usize> Fp { + /// A multiplicative generator of the field. + /// [`Self::GENERATOR`] is an element having multiplicative order + /// `MODULUS - 1`. + /// + /// Every element of the field should be represented as `GENERATOR^i` + pub const GENERATOR: Fp = P::GENERATOR; + /// Multiplicative identity of the field, i.e., the element `e` + /// such that, for all elements `f` of the field, `e * f = f`. + pub const ONE: Fp = Fp::new_unchecked(Self::R); + /// Let `M` be the power of 2^64 nearest to [`Self::MODULUS_BITS`]. Then + /// `R = M % MODULUS`. + const R: Uint = ResidueParam::::R; + /// `R2 = R^2 % MODULUS` + #[allow(dead_code)] + const R2: Uint = ResidueParam::::R2; + /// Additive identity of the field, i.e., the element `e` + /// such that, for all elements `f` of the field, `e + f = f`. + pub const ZERO: Fp = Fp::new_unchecked(Uint::ZERO); + + /// Construct a new field element from [`Uint`] and convert it in + /// Montgomery form. + #[inline] + #[must_use] + pub const fn new(element: Uint) -> Self { + Fp { residue: Residue::, LIMBS>::new(&element) } + } + + /// Construct a new field element from [`Uint`]. + /// + /// Unlike [`Self::new`], this method does not perform Montgomery reduction. + /// This method should be used only when constructing an element from an + /// integer that has already been put in Montgomery form. + #[inline] + #[must_use] + pub const fn new_unchecked(element: Uint) -> Self { + Fp { + residue: Residue::, LIMBS>::from_montgomery( + element, + ), + } + } +} + +impl, const LIMBS: usize> Hash for Fp { + fn hash(&self, state: &mut H) { + self.residue.as_montgomery().hash(state); + } +} + +impl, const LIMBS: usize> Debug for Fp { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + Debug::fmt(&self.into_bigint(), f) + } +} + +impl, const LIMBS: usize> Zero for Fp { + #[inline] + fn zero() -> Self { + Self::ZERO + } + + #[inline] + fn is_zero(&self) -> bool { + *self == Self::ZERO + } +} + +impl, const LIMBS: usize> One for Fp { + #[inline] + fn one() -> Self { + Self::ONE + } + + #[inline] + fn is_one(&self) -> bool { + *self == Self::ONE + } +} + +impl, const LIMBS: usize> AdditiveGroup for Fp { + type Scalar = Self; + + const ZERO: Self = Self::ZERO; + + #[inline] + fn double(&self) -> Self { + let mut temp = *self; + temp.double_in_place(); + temp + } + + #[inline] + fn double_in_place(&mut self) -> &mut Self { + P::double_in_place(self); + self + } + + #[inline] + fn neg_in_place(&mut self) -> &mut Self { + P::neg_in_place(self); + self + } +} + +impl, const LIMBS: usize> Field for Fp { + const ONE: Self = Fp::new_unchecked(Self::R); + + #[inline] + fn square(&self) -> Self { + let mut temp = *self; + temp.square_in_place(); + temp + } + + fn square_in_place(&mut self) -> &mut Self { + P::square_in_place(self); + self + } + + #[inline] + fn inverse(&self) -> Option { + P::inverse(self) + } + + fn inverse_in_place(&mut self) -> Option<&mut Self> { + if let Some(inverse) = self.inverse() { + *self = inverse; + Some(self) + } else { + None + } + } +} + +impl, const LIMBS: usize> PrimeField for Fp { + type BigInt = Uint; + + const MODULUS: Self::BigInt = P::MODULUS; + const MODULUS_BIT_SIZE: usize = P::MODULUS.bits(); + + #[inline] + fn from_bigint(repr: Self::BigInt) -> Self { + P::from_bigint(repr) + } + + fn into_bigint(self) -> Uint { + P::into_bigint(self) + } +} + +impl, const LIMBS: usize> Ord for Fp { + fn cmp(&self, other: &Self) -> Ordering { + self.into_bigint().cmp(&other.into_bigint()) + } +} + +impl, const LIMBS: usize> PartialOrd for Fp { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +/// Auto implements conversion from unsigned integer of type `$int` to [`Fp`]. +macro_rules! impl_fp_from_unsigned_int { + ($int:ty) => { + impl, const LIMBS: usize> From<$int> + for Fp + { + fn from(other: $int) -> Self { + Fp::from_bigint(Uint::from(other)) + } + } + }; +} + +/// Auto implements conversion from signed integer of type `$int` to [`Fp`]. +macro_rules! impl_fp_from_signed_int { + ($int:ty) => { + impl, const LIMBS: usize> From<$int> + for Fp + { + fn from(other: $int) -> Self { + let abs = other.unsigned_abs().into(); + if other.is_positive() { + abs + } else { + -abs + } + } + } + }; +} + +impl_fp_from_unsigned_int!(u128); +impl_fp_from_unsigned_int!(u64); +impl_fp_from_unsigned_int!(u32); +impl_fp_from_unsigned_int!(u16); +impl_fp_from_unsigned_int!(u8); + +impl_fp_from_signed_int!(i128); +impl_fp_from_signed_int!(i64); +impl_fp_from_signed_int!(i32); +impl_fp_from_signed_int!(i16); +impl_fp_from_signed_int!(i8); + +impl, const LIMBS: usize> From for Fp { + fn from(other: bool) -> Self { + u8::from(other).into() + } +} + +/// Auto implements conversion from [`Fp`] to integer of type `$int`. +/// +/// Conversion is available only for a single limb field elements, +/// i.e. `LIMBS = 1`. +macro_rules! impl_int_from_fp { + ($int:ty) => { + impl> From> for $int { + fn from(other: Fp) -> Self { + let uint = other.into_bigint(); + let words = uint.as_words(); + <$int>::try_from(words[0]).unwrap_or_else(|_| { + panic!("should convert to {}", stringify!($int)) + }) + } + } + }; +} + +impl_int_from_fp!(u128); +impl_int_from_fp!(u64); +impl_int_from_fp!(u32); +impl_int_from_fp!(u16); +impl_int_from_fp!(u8); +impl_int_from_fp!(i128); +impl_int_from_fp!(i64); +impl_int_from_fp!(i32); +impl_int_from_fp!(i16); +impl_int_from_fp!(i8); + +#[cfg(test)] +impl, const LIMBS: usize> crypto_bigint::Random + for Fp +{ + #[inline] + fn random(rng: &mut impl crypto_bigint::rand_core::CryptoRngCore) -> Self { + Fp { residue: Residue::, LIMBS>::random(rng) } + } +} + +/// Outputs a string containing the value of `self`, +/// represented as a decimal without leading zeroes. +impl, const LIMBS: usize> Display for Fp { + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + let str = self.into_bigint().to_string(); + write!(f, "{str}") + } +} + +impl, const LIMBS: usize> core::ops::Neg for Fp { + type Output = Self; + + #[inline] + fn neg(mut self) -> Self { + P::neg_in_place(&mut self); + self + } +} + +impl, const LIMBS: usize> core::ops::Add<&Fp> + for Fp +{ + type Output = Self; + + #[inline] + fn add(mut self, other: &Self) -> Self { + use core::ops::AddAssign; + self.add_assign(other); + self + } +} + +impl, const LIMBS: usize> core::ops::Sub<&Fp> + for Fp +{ + type Output = Self; + + #[inline] + fn sub(mut self, other: &Self) -> Self { + use core::ops::SubAssign; + self.sub_assign(other); + self + } +} + +impl, const LIMBS: usize> core::ops::Mul<&Fp> + for Fp +{ + type Output = Self; + + #[inline] + fn mul(mut self, other: &Self) -> Self { + use core::ops::MulAssign; + self.mul_assign(other); + self + } +} + +impl, const LIMBS: usize> core::ops::Div<&Fp> + for Fp +{ + type Output = Self; + + /// Returns `self * other.inverse()` if `other.inverse()` is `Some`, and + /// panics otherwise. + #[inline] + fn div(mut self, other: &Self) -> Self { + use core::ops::MulAssign; + self.mul_assign(&other.inverse().unwrap()); + self + } +} + +impl, const LIMBS: usize> core::ops::Add<&Fp> + for &Fp +{ + type Output = Fp; + + #[inline] + fn add(self, other: &Fp) -> Fp { + use core::ops::AddAssign; + let mut result = *self; + result.add_assign(other); + result + } +} + +impl, const LIMBS: usize> core::ops::Sub<&Fp> + for &Fp +{ + type Output = Fp; + + #[inline] + fn sub(self, other: &Fp) -> Fp { + use core::ops::SubAssign; + let mut result = *self; + result.sub_assign(other); + result + } +} + +impl, const LIMBS: usize> core::ops::Mul<&Fp> + for &Fp +{ + type Output = Fp; + + #[inline] + fn mul(self, other: &Fp) -> Fp { + use core::ops::MulAssign; + let mut result = *self; + result.mul_assign(other); + result + } +} + +impl, const LIMBS: usize> core::ops::Div<&Fp> + for &Fp +{ + type Output = Fp; + + #[inline] + fn div(self, other: &Fp) -> Fp { + use core::ops::DivAssign; + let mut result = *self; + result.div_assign(other); + result + } +} + +impl, const LIMBS: usize> core::ops::AddAssign<&Self> + for Fp +{ + #[inline] + fn add_assign(&mut self, other: &Self) { + P::add_assign(self, other); + } +} + +impl, const LIMBS: usize> core::ops::SubAssign<&Self> + for Fp +{ + #[inline] + fn sub_assign(&mut self, other: &Self) { + P::sub_assign(self, other); + } +} + +#[allow(unused_qualifications)] +impl, const LIMBS: usize> core::ops::Add + for Fp +{ + type Output = Self; + + #[inline] + fn add(mut self, other: Self) -> Self { + use core::ops::AddAssign; + self.add_assign(&other); + self + } +} + +#[allow(unused_qualifications)] +impl, const LIMBS: usize> core::ops::Add<&mut Self> + for Fp +{ + type Output = Self; + + #[inline] + fn add(mut self, other: &mut Self) -> Self { + use core::ops::AddAssign; + self.add_assign(&*other); + self + } +} + +#[allow(unused_qualifications)] +impl, const LIMBS: usize> core::ops::Sub + for Fp +{ + type Output = Self; + + #[inline] + fn sub(mut self, other: Self) -> Self { + use core::ops::SubAssign; + self.sub_assign(&other); + self + } +} + +#[allow(unused_qualifications)] +impl, const LIMBS: usize> core::ops::Sub<&mut Self> + for Fp +{ + type Output = Self; + + #[inline] + fn sub(mut self, other: &mut Self) -> Self { + use core::ops::SubAssign; + self.sub_assign(&*other); + self + } +} + +#[allow(unused_qualifications)] +impl, const LIMBS: usize> core::iter::Sum + for Fp +{ + fn sum>(iter: I) -> Self { + iter.fold(Self::zero(), core::ops::Add::add) + } +} + +#[allow(unused_qualifications)] +impl<'a, P: FpParams, const LIMBS: usize> core::iter::Sum<&'a Self> + for Fp +{ + fn sum>(iter: I) -> Self { + iter.fold(Self::zero(), core::ops::Add::add) + } +} + +#[allow(unused_qualifications)] +impl, const LIMBS: usize> core::ops::AddAssign + for Fp +{ + #[inline] + fn add_assign(&mut self, other: Self) { + self.add_assign(&other); + } +} + +#[allow(unused_qualifications)] +impl, const LIMBS: usize> core::ops::SubAssign + for Fp +{ + #[inline] + fn sub_assign(&mut self, other: Self) { + self.sub_assign(&other); + } +} + +#[allow(unused_qualifications)] +impl, const LIMBS: usize> core::ops::AddAssign<&mut Self> + for Fp +{ + #[inline] + fn add_assign(&mut self, other: &mut Self) { + self.add_assign(&*other); + } +} + +#[allow(unused_qualifications)] +impl, const LIMBS: usize> core::ops::SubAssign<&mut Self> + for Fp +{ + #[inline] + fn sub_assign(&mut self, other: &mut Self) { + self.sub_assign(&*other); + } +} + +impl, const LIMBS: usize> core::ops::MulAssign<&Self> + for Fp +{ + fn mul_assign(&mut self, other: &Self) { + P::mul_assign(self, other); + } +} + +/// Computes `self *= other.inverse()` if `other.inverse()` is `Some`, and +/// panics otherwise. +impl, const LIMBS: usize> core::ops::DivAssign<&Self> + for Fp +{ + #[inline] + fn div_assign(&mut self, other: &Self) { + use core::ops::MulAssign; + self.mul_assign(&other.inverse().expect("should not divide by zero")); + } +} + +#[allow(unused_qualifications)] +impl, const LIMBS: usize> core::ops::Mul + for Fp +{ + type Output = Self; + + #[inline] + fn mul(mut self, other: Self) -> Self { + use core::ops::MulAssign; + self.mul_assign(&other); + self + } +} + +#[allow(unused_qualifications)] +impl, const LIMBS: usize> core::ops::Div + for Fp +{ + type Output = Self; + + #[inline] + fn div(mut self, other: Self) -> Self { + use core::ops::DivAssign; + self.div_assign(&other); + self + } +} + +#[allow(unused_qualifications)] +impl, const LIMBS: usize> core::ops::Mul<&mut Self> + for Fp +{ + type Output = Self; + + #[inline] + fn mul(mut self, other: &mut Self) -> Self { + use core::ops::MulAssign; + self.mul_assign(&*other); + self + } +} + +#[allow(unused_qualifications)] +impl, const LIMBS: usize> core::ops::Div<&mut Self> + for Fp +{ + type Output = Self; + + #[inline] + fn div(mut self, other: &mut Self) -> Self { + use core::ops::DivAssign; + self.div_assign(&*other); + self + } +} + +#[allow(unused_qualifications)] +impl, const LIMBS: usize> core::iter::Product + for Fp +{ + fn product>(iter: I) -> Self { + iter.fold(Self::one(), core::ops::Mul::mul) + } +} + +#[allow(unused_qualifications)] +impl<'a, P: FpParams, const LIMBS: usize> core::iter::Product<&'a Self> + for Fp +{ + fn product>(iter: I) -> Self { + iter.fold(Self::one(), core::ops::Mul::mul) + } +} + +#[allow(unused_qualifications)] +impl, const LIMBS: usize> core::ops::MulAssign + for Fp +{ + #[inline] + fn mul_assign(&mut self, other: Self) { + self.mul_assign(&other); + } +} + +#[allow(unused_qualifications)] +impl, const LIMBS: usize> core::ops::DivAssign<&mut Self> + for Fp +{ + #[inline] + fn div_assign(&mut self, other: &mut Self) { + self.div_assign(&*other); + } +} + +#[allow(unused_qualifications)] +impl, const LIMBS: usize> core::ops::MulAssign<&mut Self> + for Fp +{ + #[inline] + fn mul_assign(&mut self, other: &mut Self) { + self.mul_assign(&*other); + } +} + +#[allow(unused_qualifications)] +impl, const LIMBS: usize> core::ops::DivAssign + for Fp +{ + #[inline] + fn div_assign(&mut self, other: Self) { + self.div_assign(&other); + } +} + +impl, const LIMBS: usize> zeroize::Zeroize for Fp { + // The phantom data does not contain element-specific data + // and thus does not need to be zeroized. + fn zeroize(&mut self) { + self.residue.zeroize(); + } +} + +impl, const LIMBS: usize> From> + for Uint +{ + #[inline] + fn from(fp: Fp) -> Self { + fp.into_bigint() + } +} + +impl, const LIMBS: usize> From> + for Fp +{ + /// Converts `Self::BigInteger` into `Self` + #[inline] + fn from(int: Uint) -> Self { + Self::from_bigint(int) + } +} + +/// This macro converts a string base-10 number to a field element. +#[macro_export] +macro_rules! fp_from_num { + ($num:literal) => { + $crate::field::fp::Fp::new($crate::bigint::from_str_radix($num, 10)) + }; +} + +/// This macro converts a string hex number to a field element. +#[macro_export] +macro_rules! fp_from_hex { + ($num:literal) => {{ + $crate::field::fp::Fp::new( + $crate::bigint::crypto_bigint::Uint::from_be_hex($num), + ) + }}; +} + +#[cfg(test)] +mod tests { + use proptest::prelude::*; + + use super::*; + use crate::{ + bigint::crypto_bigint::U64, + field::{ + fp::{Fp64, FpParams, LIMBS_64}, + group::AdditiveGroup, + }, + fp_from_num, from_num, + }; + + type Field64 = Fp64; + struct Fp64Param; + impl FpParams for Fp64Param { + const GENERATOR: Fp64 = fp_from_num!("3"); + const MODULUS: U64 = from_num!("1000003"); // Prime number + } + + const MODULUS: i128 = 1000003; // Prime number + + proptest! { + #[test] + fn add(a: i64, b: i64) { + let res = Field64::from(a) + Field64::from(b); + let res: i128 = res.into(); + let a = a as i128; + let b = b as i128; + prop_assert_eq!(res, (a + b).rem_euclid(MODULUS)); + } + + #[test] + fn double(a: i64) { + let res = Field64::from(a).double(); + let res: i128 = res.into(); + let a = a as i128; + prop_assert_eq!(res, (a + a).rem_euclid(MODULUS)); + } + + #[test] + fn sub(a: i64, b: i64) { + let res = Field64::from(a) - Field64::from(b); + let res: i128 = res.into(); + let a = a as i128; + let b = b as i128; + prop_assert_eq!(res, (a - b).rem_euclid(MODULUS)); + } + + #[test] + fn mul(a: i64, b: i64) { + let res = Field64::from(a) * Field64::from(b); + let res: i128 = res.into(); + let a = a as i128; + let b = b as i128; + prop_assert_eq!(res, (a * b).rem_euclid(MODULUS)); + } + + #[test] + fn square(a: i64) { + let res = Field64::from(a).square(); + let res: i128 = res.into(); + let a = a as i128; + prop_assert_eq!(res, (a * a).rem_euclid(MODULUS)); + } + + #[test] + fn div(a: i64, b: i64) { + // Skip if `b` is zero. + if (b as i128) % MODULUS == 0 { + return Ok(()); + } + + let res = Field64::from(a) / Field64::from(b); + let res: i128 = res.into(); + let a = a as i128; + let b = b as i128; + // a / b = res mod M => res * b = a mod M + prop_assert_eq!((res * b).rem_euclid(MODULUS), a.rem_euclid(MODULUS)); + } + + #[test] + fn pow(a: i64, b in 0_u32..1000) { + /// Compute a^b in an expensive and iterative way. + fn dumb_pow(a: i128, b: i128) -> i128 { + (0..b).fold(1, |acc, _| (acc * a).rem_euclid(MODULUS)) + } + + let res = Field64::from(a).pow(b); + let res: i128 = res.into(); + let a = a as i128; + let b = b as i128; + prop_assert_eq!(res, dumb_pow(a, b)); + } + + #[test] + fn neg(a: i64) { + let res = -Field64::from(a); + let res: i128 = res.into(); + let a = a as i128; + prop_assert_eq!(res, (-a).rem_euclid(MODULUS)); + } + + #[test] + fn one(a: i64) { + let res = Field64::one(); + let res: i128 = res.into(); + prop_assert_eq!(res, 1); + + let res = Field64::one() * Field64::from(a); + let res: i128 = res.into(); + let a: i128 = a.into(); + prop_assert_eq!(res, a.rem_euclid(MODULUS)); + } + + #[test] + fn zero(a: i64) { + let res = Field64::zero(); + let res: i128 = res.into(); + prop_assert_eq!(res, 0); + + let res = Field64::zero() + Field64::from(a); + let res: i128 = res.into(); + let a: i128 = a.into(); + prop_assert_eq!(res, a.rem_euclid(MODULUS)); + } + } +} diff --git a/lib/crypto/src/field/group.rs b/lib/crypto/src/field/group.rs new file mode 100644 index 00000000..26dd8ef3 --- /dev/null +++ b/lib/crypto/src/field/group.rs @@ -0,0 +1,82 @@ +//! This module provides a generic interface for groups with additive notation. + +use core::{ + fmt::{Debug, Display}, + hash::Hash, + iter::Sum, + ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}, +}; + +use num_traits::Zero; +use zeroize::Zeroize; + +use crate::field::Field; + +/// Defines an abstract group with additive notation. +/// Support addition and subtraction with itself and multiplication by scalar. +/// Scalar and group can be different types. +/// +/// E.g., points on an elliptic curve define an additive group and can be +/// multiplied by a scalar. +pub trait AdditiveGroup: + Eq + + 'static + + Sized + + Copy + + Clone + + Default + + Send + + Sync + + Hash + + Debug + + Display + + Zeroize + + Zero + + Neg + + Add + + Sub + + Mul<::Scalar, Output = Self> + + AddAssign + + SubAssign + + MulAssign<::Scalar> + + for<'a> Add<&'a Self, Output = Self> + + for<'a> Sub<&'a Self, Output = Self> + + for<'a> Mul<&'a ::Scalar, Output = Self> + + for<'a> AddAssign<&'a Self> + + for<'a> SubAssign<&'a Self> + + for<'a> MulAssign<&'a ::Scalar> + + for<'a> Add<&'a mut Self, Output = Self> + + for<'a> Sub<&'a mut Self, Output = Self> + + for<'a> Mul<&'a mut ::Scalar, Output = Self> + + for<'a> AddAssign<&'a mut Self> + + for<'a> SubAssign<&'a mut Self> + + for<'a> MulAssign<&'a mut ::Scalar> + + Sum + + for<'a> Sum<&'a Self> +{ + /// Scalar associated with the group. + type Scalar: Field; + + /// Additive identity of the group. + const ZERO: Self; + + /// Doubles `self`. + #[must_use] + fn double(&self) -> Self { + let mut copy = *self; + copy.double_in_place(); + copy + } + + /// Doubles `self` in place. + fn double_in_place(&mut self) -> &mut Self { + self.add_assign(*self); + self + } + + /// Negates `self` in place. + fn neg_in_place(&mut self) -> &mut Self { + *self = -(*self); + self + } +} diff --git a/lib/crypto/src/field/instance.rs b/lib/crypto/src/field/instance.rs new file mode 100644 index 00000000..afada146 --- /dev/null +++ b/lib/crypto/src/field/instance.rs @@ -0,0 +1,51 @@ +//! This module contains the field instances for some popular curves. + +#![allow(missing_docs)] +use crypto_bigint::{U256, U64}; + +use crate::{ + field::fp::{Fp256, Fp64, FpParams, LIMBS_256, LIMBS_64}, + fp_from_num, from_num, +}; + +pub type FpVesta = Fp256; +pub struct VestaParam; +impl FpParams for VestaParam { + const GENERATOR: Fp256 = fp_from_num!("5"); + const MODULUS: U256 = from_num!("28948022309329048855892746252171976963363056481941647379679742748393362948097"); +} + +pub type FpBabyBear = Fp64; +pub struct BabyBearParam; +impl FpParams for BabyBearParam { + const GENERATOR: Fp64 = fp_from_num!("31"); + const MODULUS: U64 = from_num!("2013265921"); +} + +pub type FpBLS12 = Fp256; +pub struct BLS12Param; +impl FpParams for BLS12Param { + const GENERATOR: Fp256 = fp_from_num!("7"); + const MODULUS: U256 = from_num!("52435875175126190479447740508185965837690552500527637822603658699938581184513"); +} + +pub type FpBN256 = Fp256; +pub struct BN256Param; +impl FpParams for BN256Param { + const GENERATOR: Fp256 = fp_from_num!("7"); + const MODULUS: U256 = from_num!("21888242871839275222246405745257275088548364400416034343698204186575808495617"); +} + +pub type FpGoldiLocks = Fp64; +pub struct GoldiLocksParam; +impl FpParams for GoldiLocksParam { + const GENERATOR: Fp64 = fp_from_num!("7"); + const MODULUS: U64 = from_num!("18446744069414584321"); +} + +pub type FpPallas = Fp256; +pub struct PallasParam; +impl FpParams for PallasParam { + const GENERATOR: Fp256 = fp_from_num!("5"); + const MODULUS: U256 = from_num!("28948022309329048855892746252171976963363056481941560715954676764349967630337"); +} diff --git a/lib/crypto/src/field/mod.rs b/lib/crypto/src/field/mod.rs new file mode 100644 index 00000000..03333f6d --- /dev/null +++ b/lib/crypto/src/field/mod.rs @@ -0,0 +1,133 @@ +//! This module provides common arithmetics to work with finite fields. +//! Implementations of some used fields provided in the [`instance`] +//! module. +//! +//! Here is an example operations over a prime finite field (aka Fp) with a +//! prime modulus `17` and generator element `3`. +//! +//! ## Example +//! ```rust +//! use openzeppelin_crypto::{ +//! bigint::crypto_bigint::U64, +//! field::{ +//! fp::{Fp64, FpParams, LIMBS_64}, +//! group::AdditiveGroup, +//! Field, +//! }, +//! fp_from_num, +//! from_num, +//! }; +//! +//! pub type ExampleField = Fp64; +//! pub struct FpParam; +//! impl FpParams for FpParam { +//! const MODULUS: U64 = from_num!("17"); +//! const GENERATOR: Fp64 = fp_from_num!("3"); +//! } +//! +//! # fn main() { +//! let a = ExampleField::from(9); +//! let b = ExampleField::from(10); +//! +//! assert_eq!(a, ExampleField::from(26)); // 26 = 9 mod 17 +//! assert_eq!(a - b, ExampleField::from(16)); // -1 = 16 mod 17 +//! assert_eq!(a + b, ExampleField::from(2)); // 19 = 2 mod 17 +//! assert_eq!(a * b, ExampleField::from(5)); // 90 = 5 mod 17 +//! assert_eq!(a.square(), ExampleField::from(13)); // 81 = 13 mod 17 +//! assert_eq!(b.double(), ExampleField::from(3)); // 20 = 3 mod 17 +//! assert_eq!(a / b, a * b.inverse().unwrap()); // need to unwrap since `b` could be 0 which is not invertible +//! # } +//! ``` +use core::{ + fmt::{Debug, Display}, + hash::Hash, + iter::Product, + ops::{Div, DivAssign, Neg}, +}; + +use group::AdditiveGroup; +use num_traits::{One, Zero}; +use zeroize::Zeroize; + +use crate::bits::BitIteratorBE; + +pub mod fp; +pub mod group; +pub mod instance; +pub mod prime; + +/// Defines an abstract field. +/// Types implementing [`Field`] support common field operations such as +/// addition, subtraction, multiplication, and inverses. +pub trait Field: + 'static + + Copy + + Clone + + Debug + + Display + + Default + + Send + + Sync + + Eq + + Zero + + One + + Ord + + Neg + + Zeroize + + Sized + + Hash + + AdditiveGroup + + Div + + DivAssign + + for<'a> Div<&'a Self, Output = Self> + + for<'a> DivAssign<&'a Self> + + for<'a> Div<&'a mut Self, Output = Self> + + for<'a> DivAssign<&'a mut Self> + + for<'a> Product<&'a Self> + + From + + From + + From + + From + + From + + From + + From + + From + + From + + From + + From + + Product +{ + /// The multiplicative identity of the field. + const ONE: Self; + + /// Returns `self * self`. + #[must_use] + fn square(&self) -> Self; + + /// Squares `self` in place. + fn square_in_place(&mut self) -> &mut Self; + + /// Computes the multiplicative inverse of `self` if `self` is nonzero. + fn inverse(&self) -> Option; + + /// If `self.inverse().is_none()`, this just returns `None`. Otherwise, it + /// sets `self` to `self.inverse().unwrap()`. + fn inverse_in_place(&mut self) -> Option<&mut Self>; + + /// Returns `self^exp`, where `exp` is an integer represented with `u64` + /// limbs. + /// Least significant limb first. + #[must_use] + fn pow(&self, exp: S) -> Self { + let mut res = Self::one(); + + for i in exp.bit_be_trimmed_iter() { + res.square_in_place(); + + if i { + res *= self; + } + } + res + } +} diff --git a/lib/crypto/src/field/prime.rs b/lib/crypto/src/field/prime.rs new file mode 100644 index 00000000..379b6aa6 --- /dev/null +++ b/lib/crypto/src/field/prime.rs @@ -0,0 +1,39 @@ +//! This module provides a generic interface for finite prime fields. + +use crate::{bigint::BigInteger, field::Field}; + +/// Defines an abstract prime field. +/// I.e., the field of integers of prime module [`Self::MODULUS`]. +pub trait PrimeField: + Field + From<::BigInt> + Into<::BigInt> +{ + /// A `BigInteger` type that can represent elements of this field. + type BigInt: BigInteger; + + /// The modulus `p`. + const MODULUS: Self::BigInt; + + /// The size of the modulus in bits. + const MODULUS_BIT_SIZE: usize; + + /// Returns the characteristic of the field, + /// in little-endian representation. + #[must_use] + fn characteristic() -> Self::BigInt { + Self::MODULUS + } + + /// Returns the extension degree of this field with respect + /// to `Self::BasePrimeField`. + #[must_use] + fn extension_degree() -> usize { + 1 + } + + /// Construct a prime field element from a big integer. + fn from_bigint(repr: Self::BigInt) -> Self; + + /// Converts an element of the prime field into an integer less than + /// [`Self::MODULUS`]. + fn into_bigint(self) -> Self::BigInt; +} diff --git a/lib/crypto/src/hash.rs b/lib/crypto/src/hash.rs index b8f98fd5..eb60ebec 100644 --- a/lib/crypto/src/hash.rs +++ b/lib/crypto/src/hash.rs @@ -127,7 +127,6 @@ pub trait BuildHasher { } /// Hash the pair `(a, b)` with `state`. -#[allow(clippy::module_name_repetitions)] #[inline] pub fn hash_pair(a: &H, b: &H, mut state: S) -> S::Output where diff --git a/lib/crypto/src/keccak.rs b/lib/crypto/src/keccak.rs index 136d4afb..7a52a7d8 100644 --- a/lib/crypto/src/keccak.rs +++ b/lib/crypto/src/keccak.rs @@ -8,7 +8,6 @@ use crate::hash::{BuildHasher, Hash, Hasher}; /// proofs][crate]. /// /// It instantiates a [`Keccak256`] hasher. -#[allow(clippy::module_name_repetitions)] pub struct KeccakBuilder; impl BuildHasher for KeccakBuilder { @@ -24,7 +23,6 @@ impl BuildHasher for KeccakBuilder { /// /// The underlying implementation is guaranteed to match that of the /// `keccak256` algorithm, commonly used in Ethereum. -#[allow(clippy::module_name_repetitions)] pub struct Keccak256(Keccak); impl Hasher for Keccak256 { diff --git a/lib/crypto/src/lib.rs b/lib/crypto/src/lib.rs index 8c7fc012..2077f5dc 100644 --- a/lib/crypto/src/lib.rs +++ b/lib/crypto/src/lib.rs @@ -18,11 +18,17 @@ Common cryptographic procedures for a blockchain environment. */ +#![allow(clippy::module_name_repetitions)] #![cfg_attr(not(feature = "std"), no_std, no_main)] extern crate alloc; +extern crate core; +pub mod bigint; +pub mod bits; +#[macro_use] +pub mod field; pub mod hash; +pub mod keccak; pub mod merkle; -pub mod keccak; pub use keccak::KeccakBuilder; diff --git a/lib/motsu-proc/Cargo.toml b/lib/motsu-proc/Cargo.toml index 99cbdb6d..58375a3e 100644 --- a/lib/motsu-proc/Cargo.toml +++ b/lib/motsu-proc/Cargo.toml @@ -6,7 +6,7 @@ categories = ["development-tools::testing", "cryptography::cryptocurrencies"] keywords = ["arbitrum", "ethereum", "stylus", "unit-tests", "tests"] license.workspace = true repository.workspace = true -version = "0.1.0" +version = "0.2.0" [dependencies] proc-macro2.workspace = true