Repository: OpenZeppelin/rust-contracts-stylus
Pull request #394 (2024-11-07 02:46:17)

build(deps): bump crate-ci/typos from 1.27.0 to 1.27.2
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.27.0 to 1.27.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/crate-ci/typos/releases">crate-ci/typos's releases</a>.</em></p>
<blockquote>
<h2>v1.27.2</h2>
<h2>[1.27.2] - 2024-11-06</h2>
<h3>Fixes</h3>
<ul>
<li>Correct <code>fand</code></li>
</ul>
<h2>v1.27.1</h2>
<h2>[1.27.1] - 2024-11-06</h2>
<h3>Fixes</h3>
<ul>
<li>Correct <code>alingment</code> as <code>alignment</code>, rather than <code>alinement</code></li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/crate-ci/typos/blob/master/CHANGELOG.md">crate-ci/typos's changelog</a>.</em></p>
<blockquote>
<h2>[1.27.2] - 2024-11-06</h2>
<h3>Fixes</h3>
<ul>
<li>Correct <code>fand</code></li>
</ul>
<h2>[1.27.1] - 2024-11-06</h2>
<h3>Fixes</h3>
<ul>
<li>Correct <code>alingment</code> as <code>alignment</code>, rather than <code>alinement</code></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/crate-ci/typos/commit/98325b27809ff86ca808bacada386a051a3515a3"><code>98325b2</code></a> chore: Release</li>
<li><a href="https://github.com/crate-ci/typos/commit/ddde6db73ab0778f15d99e123cebe8b85d38b22f"><code>ddde6db</code></a> docs: Update changelog</li>
<li><a href="https://github.com/crate-ci/typos/commit/a0962bf415edf53cb359ffd009b26a6975f85950"><code>a0962bf</code></a> Merge pull request <a href="https://redirect.github.com/crate-ci/typos/issues/1147">#1147</a> from ianthetechie/add-fand</li>
<li><a href="https://github.com/crate-ci/typos/commit/51e9d58a14d6c43eedf9356ab5b30ef4715d14c0"><code>51e9d58</code></a> chore: Release</li>
<li><a href="https://github.com/crate-ci/typos/commit/6966735fd784566e742527a4042baf5a2546946f"><code>6966735</code></a> docs: Update changelog</li>
<li><a href="https://github.com/crate-ci/typos/commit/b0c364bcdf09d0b753c074cfc8de834c40c3fb8d"><code>b0c364b</code></a> Merge pull request <a href="https://redirect.github.com/crate-ci/typos/issues/1146">#1146</a> from epage/aline</li>
<li><a href="https://github.com/crate-ci/typos/commit/2ad0621918ea58fa1f6047cb683e28569789159f"><code>2ad0621</code></a> Add &quot;fand&quot; corrections to the dictionary</li>
<li><a href="https://github.com/crate-ci/typos/commit/5afabf72f99459511438b22147fc6df29f4a232a"><code>5afabf7</code></a> fix(dict): Prefer correcting alingment to alignment</li>
<li><a href="https://github.com/crate-ci/typos/commit/59988673881001659a7ffe57b52c413e3e85f1b0"><code>5998867</code></a> Merge pull request <a href="https://redirect.github.com/crate-ci/typos/issues/1145">#1145</a> from epage/dict</li>
<li><a href="https://github.com/crate-ci/typos/commit/cc962509493aba7e44fae027a4acfef285f74930"><code>cc96250</code></a> test(dict): Don't use unverified varcon</li>
<li>Additional commits viewable in <a href="https://github.com/crate-ci/typos/compare/v1.27.0...v1.27.2">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=crate-ci/typos&package-manager=github_actions&previous-version=1.27.0&new-version=1.27.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>

netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *contracts-stylus* processing.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | f2338a524576c7812301503ea155c28ac25bb1a1 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/contracts-stylus/deploys/672c29fc29aaa600083965d5 |

Patch:
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index b2ce1d6..0420c5e 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 a3bfdb0..fd754d3 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 d36efed..594bacd 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"
@@ -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]]
@@ -3205,6 +3242,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 1043b69..a866cd2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -18,6 +18,7 @@ members = [
   "examples/basic/token",
   "examples/basic/script",
   "examples/ecdsa",
+  "examples/safe-erc20",
   "benches",
 ]
 default-members = [
@@ -32,6 +33,7 @@ default-members = [
   "examples/erc721",
   "examples/erc721-consecutive",
   "examples/erc721-metadata",
+  "examples/safe-erc20",
   "examples/merkle-proofs",
   "examples/ownable",
   "examples/access-control",
@@ -94,6 +96,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"] }
@@ -104,10 +112,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 = { path = "lib/motsu" }
 motsu-proc = { path = "lib/motsu-proc", version = "0.1.0" }
 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 19fba1e..91399f4 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 0000000..955a3f6
--- /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 0000000..1669a69
--- /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<Self>)`. 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<alloc::vec::Vec<u8>>;
+
+    /// 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<U256, Error> {
+        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 0000000..15408bd
--- /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 0000000..fcc64a9
--- /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/lib/crypto/Cargo.toml b/lib/crypto/Cargo.toml
index f183c64..16e3eab 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/src/bigint.rs b/lib/crypto/src/bigint.rs
new file mode 100644
index 0000000..1d88ea4
--- /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<u64>
+    + From<u32>
+    + From<u16>
+    + From<u8>
+    + BitXorAssign<Self>
+    + for<'a> BitXorAssign<&'a Self>
+    + BitXor<Self, Output = Self>
+    + for<'a> BitXor<&'a Self, Output = Self>
+    + BitAndAssign<Self>
+    + for<'a> BitAndAssign<&'a Self>
+    + BitAnd<Self, Output = Self>
+    + for<'a> BitAnd<&'a Self, Output = Self>
+    + BitOrAssign<Self>
+    + for<'a> BitOrAssign<&'a Self>
+    + BitOr<Self, Output = Self>
+    + for<'a> BitOr<&'a Self, Output = Self>
+    + Shr<usize, Output = Self>
+    + ShrAssign<usize>
+    + Shl<usize, Output = Self>
+    + ShlAssign<usize>
+{
+    /// 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<const N: usize> BigInteger for Uint<N> {
+    const NUM_LIMBS: usize = N;
+
+    fn is_odd(&self) -> bool {
+        <Uint<N> as Integer>::is_odd(self).into()
+    }
+
+    fn is_even(&self) -> bool {
+        <Uint<N> as Integer>::is_even(self).into()
+    }
+
+    fn is_zero(&self) -> bool {
+        <Uint<N> 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<const N: usize> BitIteratorBE for Uint<N> {
+    fn bit_be_iter(&self) -> impl Iterator<Item = bool> {
+        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<const LIMBS: usize>(
+    s: &str,
+    radix: u32,
+) -> Uint<LIMBS> {
+    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<const LIMBS: usize>(
+    a: &Uint<LIMBS>,
+    b: &Uint<LIMBS>,
+) -> Uint<LIMBS> {
+    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<const LIMBS: usize>(
+    a: &Uint<LIMBS>,
+    b: &Uint<LIMBS>,
+) -> Uint<LIMBS> {
+    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<bool> = 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 0000000..42a3f85
--- /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<Item = bool>;
+
+    /// 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<Item = bool> {
+        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<Item = bool> {
+                (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<bool> = 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 0000000..3e43758
--- /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<const LIMBS: usize>: Send + Sync + 'static + Sized {
+    /// The modulus of the field.
+    const MODULUS: Uint<LIMBS>;
+
+    /// A multiplicative generator of the field.
+    /// [`Self::GENERATOR`] is an element having multiplicative order
+    /// `MODULUS - 1`.
+    const GENERATOR: Fp<Self, LIMBS>;
+
+    /// Set `a += b`.
+    fn add_assign(a: &mut Fp<Self, LIMBS>, b: &Fp<Self, LIMBS>) {
+        a.residue += b.residue;
+    }
+
+    /// Set `a -= b`.
+    fn sub_assign(a: &mut Fp<Self, LIMBS>, b: &Fp<Self, LIMBS>) {
+        a.residue -= b.residue;
+    }
+
+    /// Set `a = a + a`.
+    fn double_in_place(a: &mut Fp<Self, LIMBS>) {
+        a.residue += a.residue;
+    }
+
+    /// Set `a = -a`;
+    fn neg_in_place(a: &mut Fp<Self, LIMBS>) {
+        a.residue = a.residue.neg();
+    }
+
+    /// Set `a *= b`.
+    fn mul_assign(a: &mut Fp<Self, LIMBS>, b: &Fp<Self, LIMBS>) {
+        a.residue *= b.residue;
+    }
+
+    /// Set `a *= a`.
+    fn square_in_place(a: &mut Fp<Self, LIMBS>) {
+        a.residue = a.residue.square();
+    }
+
+    /// Compute `a^{-1}` if `a` is not zero.
+    #[must_use]
+    fn inverse(a: &Fp<Self, LIMBS>) -> Option<Fp<Self, LIMBS>> {
+        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<LIMBS>) -> Fp<Self, LIMBS> {
+        Fp::new(r)
+    }
+
+    /// Convert a field element to an integer less than [`Self::MODULUS`].
+    #[must_use]
+    fn into_bigint(a: Fp<Self, LIMBS>) -> Uint<LIMBS> {
+        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<P: FpParams<LIMBS>, 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<ResidueParam<P, LIMBS>, 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<P> = 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<P: FpParams<LIMBS>, const LIMBS: usize>(PhantomData<P>);
+
+impl<P: FpParams<LIMBS>, const LIMBS: usize> ResidueParams<LIMBS>
+    for ResidueParam<P, LIMBS>
+{
+    const LIMBS: usize = LIMBS;
+    const MODULUS: Uint<LIMBS> = {
+        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<LIMBS> =
+        Uint::MAX.const_rem(&P::MODULUS).0.wrapping_add(&Uint::ONE);
+    const R2: Uint<LIMBS> =
+        Uint::const_rem_wide(Self::R.square_wide(), &P::MODULUS).0;
+    const R3: Uint<LIMBS> = montgomery_reduction(
+        &Self::R2.square_wide(),
+        &P::MODULUS,
+        Self::MOD_NEG_INV,
+    );
+}
+
+impl<P: FpParams<LIMBS>, const LIMBS: usize> Fp<P, LIMBS> {
+    /// 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, LIMBS> = 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<P, LIMBS> = 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<LIMBS> = ResidueParam::<P, LIMBS>::R;
+    /// `R2 = R^2 % MODULUS`
+    #[allow(dead_code)]
+    const R2: Uint<LIMBS> = ResidueParam::<P, LIMBS>::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<P, LIMBS> = 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<LIMBS>) -> Self {
+        Fp { residue: Residue::<ResidueParam<P, LIMBS>, 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<LIMBS>) -> Self {
+        Fp {
+            residue: Residue::<ResidueParam<P, LIMBS>, LIMBS>::from_montgomery(
+                element,
+            ),
+        }
+    }
+}
+
+impl<P: FpParams<LIMBS>, const LIMBS: usize> Hash for Fp<P, LIMBS> {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        self.residue.as_montgomery().hash(state);
+    }
+}
+
+impl<P: FpParams<LIMBS>, const LIMBS: usize> Debug for Fp<P, LIMBS> {
+    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
+        Debug::fmt(&self.into_bigint(), f)
+    }
+}
+
+impl<P: FpParams<LIMBS>, const LIMBS: usize> Zero for Fp<P, LIMBS> {
+    #[inline]
+    fn zero() -> Self {
+        Self::ZERO
+    }
+
+    #[inline]
+    fn is_zero(&self) -> bool {
+        *self == Self::ZERO
+    }
+}
+
+impl<P: FpParams<LIMBS>, const LIMBS: usize> One for Fp<P, LIMBS> {
+    #[inline]
+    fn one() -> Self {
+        Self::ONE
+    }
+
+    #[inline]
+    fn is_one(&self) -> bool {
+        *self == Self::ONE
+    }
+}
+
+impl<P: FpParams<LIMBS>, const LIMBS: usize> AdditiveGroup for Fp<P, LIMBS> {
+    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<P: FpParams<LIMBS>, const LIMBS: usize> Field for Fp<P, LIMBS> {
+    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<Self> {
+        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<P: FpParams<LIMBS>, const LIMBS: usize> PrimeField for Fp<P, LIMBS> {
+    type BigInt = Uint<LIMBS>;
+
+    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<LIMBS> {
+        P::into_bigint(self)
+    }
+}
+
+impl<P: FpParams<LIMBS>, const LIMBS: usize> Ord for Fp<P, LIMBS> {
+    fn cmp(&self, other: &Self) -> Ordering {
+        self.into_bigint().cmp(&other.into_bigint())
+    }
+}
+
+impl<P: FpParams<LIMBS>, const LIMBS: usize> PartialOrd for Fp<P, LIMBS> {
+    #[inline]
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        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<P: FpParams<LIMBS>, const LIMBS: usize> From<$int>
+            for Fp<P, LIMBS>
+        {
+            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<P: FpParams<LIMBS>, const LIMBS: usize> From<$int>
+            for Fp<P, LIMBS>
+        {
+            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<P: FpParams<LIMBS>, const LIMBS: usize> From<bool> for Fp<P, LIMBS> {
+    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<P: FpParams<1>> From<Fp<P, 1>> for $int {
+            fn from(other: Fp<P, 1>) -> 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<P: FpParams<LIMBS>, const LIMBS: usize> crypto_bigint::Random
+    for Fp<P, LIMBS>
+{
+    #[inline]
+    fn random(rng: &mut impl crypto_bigint::rand_core::CryptoRngCore) -> Self {
+        Fp { residue: Residue::<ResidueParam<P, LIMBS>, LIMBS>::random(rng) }
+    }
+}
+
+/// Outputs a string containing the value of `self`,
+/// represented as a decimal without leading zeroes.
+impl<P: FpParams<LIMBS>, const LIMBS: usize> Display for Fp<P, LIMBS> {
+    #[inline]
+    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
+        let str = self.into_bigint().to_string();
+        write!(f, "{str}")
+    }
+}
+
+impl<P: FpParams<LIMBS>, const LIMBS: usize> core::ops::Neg for Fp<P, LIMBS> {
+    type Output = Self;
+
+    #[inline]
+    fn neg(mut self) -> Self {
+        P::neg_in_place(&mut self);
+        self
+    }
+}
+
+impl<P: FpParams<LIMBS>, const LIMBS: usize> core::ops::Add<&Fp<P, LIMBS>>
+    for Fp<P, LIMBS>
+{
+    type Output = Self;
+
+    #[inline]
+    fn add(mut self, other: &Self) -> Self {
+        use core::ops::AddAssign;
+        self.add_assign(other);
+        self
+    }
+}
+
+impl<P: FpParams<LIMBS>, const LIMBS: usize> core::ops::Sub<&Fp<P, LIMBS>>
+    for Fp<P, LIMBS>
+{
+    type Output = Self;
+
+    #[inline]
+    fn sub(mut self, other: &Self) -> Self {
+        use core::ops::SubAssign;
+        self.sub_assign(other);
+        self
+    }
+}
+
+impl<P: FpParams<LIMBS>, const LIMBS: usize> core::ops::Mul<&Fp<P, LIMBS>>
+    for Fp<P, LIMBS>
+{
+    type Output = Self;
+
+    #[inline]
+    fn mul(mut self, other: &Self) -> Self {
+        use core::ops::MulAssign;
+        self.mul_assign(other);
+        self
+    }
+}
+
+impl<P: FpParams<LIMBS>, const LIMBS: usize> core::ops::Div<&Fp<P, LIMBS>>
+    for Fp<P, LIMBS>
+{
+    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<P: FpParams<LIMBS>, const LIMBS: usize> core::ops::Add<&Fp<P, LIMBS>>
+    for &Fp<P, LIMBS>
+{
+    type Output = Fp<P, LIMBS>;
+
+    #[inline]
+    fn add(self, other: &Fp<P, LIMBS>) -> Fp<P, LIMBS> {
+        use core::ops::AddAssign;
+        let mut result = *self;
+        result.add_assign(other);
+        result
+    }
+}
+
+impl<P: FpParams<LIMBS>, const LIMBS: usize> core::ops::Sub<&Fp<P, LIMBS>>
+    for &Fp<P, LIMBS>
+{
+    type Output = Fp<P, LIMBS>;
+
+    #[inline]
+    fn sub(self, other: &Fp<P, LIMBS>) -> Fp<P, LIMBS> {
+        use core::ops::SubAssign;
+        let mut result = *self;
+        result.sub_assign(other);
+        result
+    }
+}
+
+impl<P: FpParams<LIMBS>, const LIMBS: usize> core::ops::Mul<&Fp<P, LIMBS>>
+    for &Fp<P, LIMBS>
+{
+    type Output = Fp<P, LIMBS>;
+
+    #[inline]
+    fn mul(self, other: &Fp<P, LIMBS>) -> Fp<P, LIMBS> {
+        use core::ops::MulAssign;
+        let mut result = *self;
+        result.mul_assign(other);
+        result
+    }
+}
+
+impl<P: FpParams<LIMBS>, const LIMBS: usize> core::ops::Div<&Fp<P, LIMBS>>
+    for &Fp<P, LIMBS>
+{
+    type Output = Fp<P, LIMBS>;
+
+    #[inline]
+    fn div(self, other: &Fp<P, LIMBS>) -> Fp<P, LIMBS> {
+        use core::ops::DivAssign;
+        let mut result = *self;
+        result.div_assign(other);
+        result
+    }
+}
+
+impl<P: FpParams<LIMBS>, const LIMBS: usize> core::ops::AddAssign<&Self>
+    for Fp<P, LIMBS>
+{
+    #[inline]
+    fn add_assign(&mut self, other: &Self) {
+        P::add_assign(self, other);
+    }
+}
+
+impl<P: FpParams<LIMBS>, const LIMBS: usize> core::ops::SubAssign<&Self>
+    for Fp<P, LIMBS>
+{
+    #[inline]
+    fn sub_assign(&mut self, other: &Self) {
+        P::sub_assign(self, other);
+    }
+}
+
+#[allow(unused_qualifications)]
+impl<P: FpParams<LIMBS>, const LIMBS: usize> core::ops::Add<Self>
+    for Fp<P, LIMBS>
+{
+    type Output = Self;
+
+    #[inline]
+    fn add(mut self, other: Self) -> Self {
+        use core::ops::AddAssign;
+        self.add_assign(&other);
+        self
+    }
+}
+
+#[allow(unused_qualifications)]
+impl<P: FpParams<LIMBS>, const LIMBS: usize> core::ops::Add<&mut Self>
+    for Fp<P, LIMBS>
+{
+    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<P: FpParams<LIMBS>, const LIMBS: usize> core::ops::Sub<Self>
+    for Fp<P, LIMBS>
+{
+    type Output = Self;
+
+    #[inline]
+    fn sub(mut self, other: Self) -> Self {
+        use core::ops::SubAssign;
+        self.sub_assign(&other);
+        self
+    }
+}
+
+#[allow(unused_qualifications)]
+impl<P: FpParams<LIMBS>, const LIMBS: usize> core::ops::Sub<&mut Self>
+    for Fp<P, LIMBS>
+{
+    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<P: FpParams<LIMBS>, const LIMBS: usize> core::iter::Sum<Self>
+    for Fp<P, LIMBS>
+{
+    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
+        iter.fold(Self::zero(), core::ops::Add::add)
+    }
+}
+
+#[allow(unused_qualifications)]
+impl<'a, P: FpParams<LIMBS>, const LIMBS: usize> core::iter::Sum<&'a Self>
+    for Fp<P, LIMBS>
+{
+    fn sum<I: Iterator<Item = &'a Self>>(iter: I) -> Self {
+        iter.fold(Self::zero(), core::ops::Add::add)
+    }
+}
+
+#[allow(unused_qualifications)]
+impl<P: FpParams<LIMBS>, const LIMBS: usize> core::ops::AddAssign<Self>
+    for Fp<P, LIMBS>
+{
+    #[inline]
+    fn add_assign(&mut self, other: Self) {
+        self.add_assign(&other);
+    }
+}
+
+#[allow(unused_qualifications)]
+impl<P: FpParams<LIMBS>, const LIMBS: usize> core::ops::SubAssign<Self>
+    for Fp<P, LIMBS>
+{
+    #[inline]
+    fn sub_assign(&mut self, other: Self) {
+        self.sub_assign(&other);
+    }
+}
+
+#[allow(unused_qualifications)]
+impl<P: FpParams<LIMBS>, const LIMBS: usize> core::ops::AddAssign<&mut Self>
+    for Fp<P, LIMBS>
+{
+    #[inline]
+    fn add_assign(&mut self, other: &mut Self) {
+        self.add_assign(&*other);
+    }
+}
+
+#[allow(unused_qualifications)]
+impl<P: FpParams<LIMBS>, const LIMBS: usize> core::ops::SubAssign<&mut Self>
+    for Fp<P, LIMBS>
+{
+    #[inline]
+    fn sub_assign(&mut self, other: &mut Self) {
+        self.sub_assign(&*other);
+    }
+}
+
+impl<P: FpParams<LIMBS>, const LIMBS: usize> core::ops::MulAssign<&Self>
+    for Fp<P, LIMBS>
+{
+    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<P: FpParams<LIMBS>, const LIMBS: usize> core::ops::DivAssign<&Self>
+    for Fp<P, LIMBS>
+{
+    #[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<P: FpParams<LIMBS>, const LIMBS: usize> core::ops::Mul<Self>
+    for Fp<P, LIMBS>
+{
+    type Output = Self;
+
+    #[inline]
+    fn mul(mut self, other: Self) -> Self {
+        use core::ops::MulAssign;
+        self.mul_assign(&other);
+        self
+    }
+}
+
+#[allow(unused_qualifications)]
+impl<P: FpParams<LIMBS>, const LIMBS: usize> core::ops::Div<Self>
+    for Fp<P, LIMBS>
+{
+    type Output = Self;
+
+    #[inline]
+    fn div(mut self, other: Self) -> Self {
+        use core::ops::DivAssign;
+        self.div_assign(&other);
+        self
+    }
+}
+
+#[allow(unused_qualifications)]
+impl<P: FpParams<LIMBS>, const LIMBS: usize> core::ops::Mul<&mut Self>
+    for Fp<P, LIMBS>
+{
+    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<P: FpParams<LIMBS>, const LIMBS: usize> core::ops::Div<&mut Self>
+    for Fp<P, LIMBS>
+{
+    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<P: FpParams<LIMBS>, const LIMBS: usize> core::iter::Product<Self>
+    for Fp<P, LIMBS>
+{
+    fn product<I: Iterator<Item = Self>>(iter: I) -> Self {
+        iter.fold(Self::one(), core::ops::Mul::mul)
+    }
+}
+
+#[allow(unused_qualifications)]
+impl<'a, P: FpParams<LIMBS>, const LIMBS: usize> core::iter::Product<&'a Self>
+    for Fp<P, LIMBS>
+{
+    fn product<I: Iterator<Item = &'a Self>>(iter: I) -> Self {
+        iter.fold(Self::one(), core::ops::Mul::mul)
+    }
+}
+
+#[allow(unused_qualifications)]
+impl<P: FpParams<LIMBS>, const LIMBS: usize> core::ops::MulAssign<Self>
+    for Fp<P, LIMBS>
+{
+    #[inline]
+    fn mul_assign(&mut self, other: Self) {
+        self.mul_assign(&other);
+    }
+}
+
+#[allow(unused_qualifications)]
+impl<P: FpParams<LIMBS>, const LIMBS: usize> core::ops::DivAssign<&mut Self>
+    for Fp<P, LIMBS>
+{
+    #[inline]
+    fn div_assign(&mut self, other: &mut Self) {
+        self.div_assign(&*other);
+    }
+}
+
+#[allow(unused_qualifications)]
+impl<P: FpParams<LIMBS>, const LIMBS: usize> core::ops::MulAssign<&mut Self>
+    for Fp<P, LIMBS>
+{
+    #[inline]
+    fn mul_assign(&mut self, other: &mut Self) {
+        self.mul_assign(&*other);
+    }
+}
+
+#[allow(unused_qualifications)]
+impl<P: FpParams<LIMBS>, const LIMBS: usize> core::ops::DivAssign<Self>
+    for Fp<P, LIMBS>
+{
+    #[inline]
+    fn div_assign(&mut self, other: Self) {
+        self.div_assign(&other);
+    }
+}
+
+impl<P: FpParams<LIMBS>, const LIMBS: usize> zeroize::Zeroize for Fp<P, LIMBS> {
+    // 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<P: FpParams<LIMBS>, const LIMBS: usize> From<Fp<P, LIMBS>>
+    for Uint<LIMBS>
+{
+    #[inline]
+    fn from(fp: Fp<P, LIMBS>) -> Self {
+        fp.into_bigint()
+    }
+}
+
+impl<P: FpParams<LIMBS>, const LIMBS: usize> From<Uint<LIMBS>>
+    for Fp<P, LIMBS>
+{
+    /// Converts `Self::BigInteger` into `Self`
+    #[inline]
+    fn from(int: Uint<LIMBS>) -> 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<Fp64Param>;
+    struct Fp64Param;
+    impl FpParams<LIMBS_64> for Fp64Param {
+        const GENERATOR: Fp64<Fp64Param> = 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 0000000..26dd8ef
--- /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<Output = Self>
+    + Add<Self, Output = Self>
+    + Sub<Self, Output = Self>
+    + Mul<<Self as AdditiveGroup>::Scalar, Output = Self>
+    + AddAssign<Self>
+    + SubAssign<Self>
+    + MulAssign<<Self as AdditiveGroup>::Scalar>
+    + for<'a> Add<&'a Self, Output = Self>
+    + for<'a> Sub<&'a Self, Output = Self>
+    + for<'a> Mul<&'a <Self as AdditiveGroup>::Scalar, Output = Self>
+    + for<'a> AddAssign<&'a Self>
+    + for<'a> SubAssign<&'a Self>
+    + for<'a> MulAssign<&'a <Self as AdditiveGroup>::Scalar>
+    + for<'a> Add<&'a mut Self, Output = Self>
+    + for<'a> Sub<&'a mut Self, Output = Self>
+    + for<'a> Mul<&'a mut <Self as AdditiveGroup>::Scalar, Output = Self>
+    + for<'a> AddAssign<&'a mut Self>
+    + for<'a> SubAssign<&'a mut Self>
+    + for<'a> MulAssign<&'a mut <Self as AdditiveGroup>::Scalar>
+    + Sum<Self>
+    + 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 0000000..afada14
--- /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<VestaParam>;
+pub struct VestaParam;
+impl FpParams<LIMBS_256> for VestaParam {
+    const GENERATOR: Fp256<VestaParam> = fp_from_num!("5");
+    const MODULUS: U256 = from_num!("28948022309329048855892746252171976963363056481941647379679742748393362948097");
+}
+
+pub type FpBabyBear = Fp64<BabyBearParam>;
+pub struct BabyBearParam;
+impl FpParams<LIMBS_64> for BabyBearParam {
+    const GENERATOR: Fp64<BabyBearParam> = fp_from_num!("31");
+    const MODULUS: U64 = from_num!("2013265921");
+}
+
+pub type FpBLS12 = Fp256<BLS12Param>;
+pub struct BLS12Param;
+impl FpParams<LIMBS_256> for BLS12Param {
+    const GENERATOR: Fp256<BLS12Param> = fp_from_num!("7");
+    const MODULUS: U256 = from_num!("52435875175126190479447740508185965837690552500527637822603658699938581184513");
+}
+
+pub type FpBN256 = Fp256<BN256Param>;
+pub struct BN256Param;
+impl FpParams<LIMBS_256> for BN256Param {
+    const GENERATOR: Fp256<BN256Param> = fp_from_num!("7");
+    const MODULUS: U256 = from_num!("21888242871839275222246405745257275088548364400416034343698204186575808495617");
+}
+
+pub type FpGoldiLocks = Fp64<GoldiLocksParam>;
+pub struct GoldiLocksParam;
+impl FpParams<LIMBS_64> for GoldiLocksParam {
+    const GENERATOR: Fp64<GoldiLocksParam> = fp_from_num!("7");
+    const MODULUS: U64 = from_num!("18446744069414584321");
+}
+
+pub type FpPallas = Fp256<PallasParam>;
+pub struct PallasParam;
+impl FpParams<LIMBS_256> for PallasParam {
+    const GENERATOR: Fp256<PallasParam> = 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 0000000..03333f6
--- /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<FpParam>;
+//! pub struct FpParam;
+//! impl FpParams<LIMBS_64> for FpParam {
+//!     const MODULUS: U64 = from_num!("17");
+//!     const GENERATOR: Fp64<FpParam> = 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<Output = Self>
+    + Zeroize
+    + Sized
+    + Hash
+    + AdditiveGroup<Scalar = Self>
+    + Div<Self, Output = Self>
+    + DivAssign<Self>
+    + 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<u128>
+    + From<u64>
+    + From<u32>
+    + From<u16>
+    + From<u8>
+    + From<i128>
+    + From<i64>
+    + From<i32>
+    + From<i16>
+    + From<i8>
+    + From<bool>
+    + Product<Self>
+{
+    /// 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<Self>;
+
+    /// 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<S: BitIteratorBE>(&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 0000000..379b6aa
--- /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<<Self as PrimeField>::BigInt> + Into<<Self as PrimeField>::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 b8f98fd..eb60ebe 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<S, H>(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 136d4af..7a52a7d 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 8c7fc01..2077f5d 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;


Test patch:
diff --git a/examples/safe-erc20/tests/abi/mod.rs b/examples/safe-erc20/tests/abi/mod.rs
new file mode 100644
index 0000000..8a44187
--- /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 0000000..221be97
--- /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 0000000..ae53433
--- /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 0000000..a3fa785
--- /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 0000000..0770962
--- /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 0000000..68514da
--- /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<Address> {
+    // 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 0000000..ccf5c41
--- /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<Address> {
+    // 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 0000000..2b1189d
--- /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<Address> {
+    // 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 0000000..809c806
--- /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<Address> {
+    // 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 0000000..c8c60bd
--- /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 0000000..9530a3c
--- /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/proptest-regressions/field/fp.txt b/lib/crypto/proptest-regressions/field/fp.txt
new file mode 100644
index 0000000..341ba3f
--- /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.