From 7888698b82b2ee07c76cde0180bca192d392f030 Mon Sep 17 00:00:00 2001 From: Simon Smilga Date: Fri, 2 May 2025 14:05:02 +0600 Subject: [PATCH 01/16] feat: SafeErc20.rs match the solidity version --- contracts/src/token/erc20/utils/safe_erc20.rs | 232 +++++++++++++++++- examples/safe-erc20/src/lib.rs | 61 ++++- examples/safe-erc20/tests/abi/mod.rs | 54 ++++ .../safe-erc20/tests/safe_erc20_erc1363.rs | 146 +++++++++++ .../tests/safe_erc20_try_variants.rs | 172 +++++++++++++ temp-nitro-testnode | 1 + 6 files changed, 657 insertions(+), 9 deletions(-) create mode 100644 examples/safe-erc20/tests/safe_erc20_erc1363.rs create mode 100644 examples/safe-erc20/tests/safe_erc20_try_variants.rs create mode 160000 temp-nitro-testnode diff --git a/contracts/src/token/erc20/utils/safe_erc20.rs b/contracts/src/token/erc20/utils/safe_erc20.rs index 2d493c10c..72d433b9b 100644 --- a/contracts/src/token/erc20/utils/safe_erc20.rs +++ b/contracts/src/token/erc20/utils/safe_erc20.rs @@ -86,6 +86,13 @@ mod token { function transfer(address to, uint256 value) external returns (bool); function transferFrom(address from, address to, uint256 value) external returns (bool); } + + /// Interface of the ERC-1363 token. + interface IErc1363 { + function transferAndCall(address to, uint256 value, bytes data) external returns (bool); + function transferFromAndCall(address from, address to, uint256 value, bytes data) external returns (bool); + function approveAndCall(address spender, uint256 value, bytes data) external returns (bool); + } } } @@ -152,9 +159,52 @@ pub trait ISafeErc20 { 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. + /// Variant of `safe_transfer` that returns a bool instead of reverting if the operation is not 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. + /// + /// # Returns + /// + /// * `Ok(true)` if the transfer was successful + /// * `Ok(false)` if the transfer failed + /// * `Err(_)` if there was an error checking the token contract + fn try_safe_transfer( + &mut self, + token: Address, + to: Address, + value: U256, + ) -> Result; + + /// Variant of `safe_transfer_from` that returns a bool instead of reverting if the operation is not 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. + /// + /// # Returns + /// + /// * `Ok(true)` if the transfer was successful + /// * `Ok(false)` if the transfer failed + /// * `Err(_)` if there was an error checking the token contract + fn try_safe_transfer_from( + &mut self, + token: Address, + from: Address, + to: Address, + value: U256, + ) -> Result; + + /// Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value, + /// non-reverting calls are assumed to be successful. /// /// # Arguments /// @@ -227,6 +277,77 @@ pub trait ISafeErc20 { spender: Address, value: U256, ) -> Result<(), Self::Error>; + + /// Performs an ERC1363 transferAndCall, with a fallback to the simple ERC20 transfer if the target has no + /// code. This can be used to implement an ERC721-like safe transfer that rely on ERC1363 checks when + /// targeting contracts. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `token` - Address of the ERC-1363 token contract. + /// * `to` - Account to transfer tokens to. + /// * `value` - Number of tokens to transfer. + /// * `data` - Additional data to be passed to the receiver contract. + /// + /// # Errors + /// + /// * [`Error::SafeErc20FailedOperation`] - If the transfer fails. + fn transfer_and_call_relaxed( + &mut self, + token: Address, + to: Address, + value: U256, + data: Vec, + ) -> Result<(), Self::Error>; + + /// Performs an ERC1363 transferFromAndCall, with a fallback to the simple ERC20 transferFrom if the target + /// has no code. This can be used to implement an ERC721-like safe transfer that rely on ERC1363 checks when + /// targeting contracts. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `token` - Address of the ERC-1363 token contract. + /// * `from` - Account to transfer tokens from. + /// * `to` - Account to transfer tokens to. + /// * `value` - Number of tokens to transfer. + /// * `data` - Additional data to be passed to the receiver contract. + /// + /// # Errors + /// + /// * [`Error::SafeErc20FailedOperation`] - If the transfer fails. + fn transfer_from_and_call_relaxed( + &mut self, + token: Address, + from: Address, + to: Address, + value: U256, + data: Vec, + ) -> Result<(), Self::Error>; + + /// Performs an ERC1363 approveAndCall, with a fallback to the simple ERC20 approve if the target has no + /// code. This can be used to implement an ERC721-like safe transfer that rely on ERC1363 checks when + /// targeting contracts. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `token` - Address of the ERC-1363 token contract. + /// * `to` - Account to approve tokens for. + /// * `value` - Number of tokens to approve. + /// * `data` - Additional data to be passed to the receiver contract. + /// + /// # Errors + /// + /// * [`Error::SafeErc20FailedOperation`] - If the approval fails. + fn approve_and_call_relaxed( + &mut self, + token: Address, + to: Address, + value: U256, + data: Vec, + ) -> Result<(), Self::Error>; } #[public] @@ -256,6 +377,29 @@ impl ISafeErc20 for SafeErc20 { Self::call_optional_return(token, &call) } + fn try_safe_transfer( + &mut self, + token: Address, + to: Address, + value: U256, + ) -> Result { + let call = IErc20::transferCall { to, value }; + + Self::call_optional_return_bool(token, &call) + } + + fn try_safe_transfer_from( + &mut self, + token: Address, + from: Address, + to: Address, + value: U256, + ) -> Result { + let call = IErc20::transferFromCall { from, to, value }; + + Self::call_optional_return_bool(token, &call) + } + fn safe_increase_allowance( &mut self, token: Address, @@ -302,7 +446,7 @@ impl ISafeErc20 for SafeErc20 { 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() { + if Self::call_optional_return_bool(token, &approve_call)? { return Ok(()); } @@ -313,6 +457,61 @@ impl ISafeErc20 for SafeErc20 { Self::call_optional_return(token, &reset_approval_call)?; Self::call_optional_return(token, &approve_call) } + + fn transfer_and_call_relaxed( + &mut self, + token: Address, + to: Address, + value: U256, + data: Vec, + ) -> Result<(), Self::Error> { + if !Address::has_code(&to) { + return self.safe_transfer(token, to, value); + } + + let call = IErc1363::transferAndCallCall { to, value, data }; + if !Self::call_optional_return_bool(token, &call)? { + return Err(SafeErc20FailedOperation { token }.into()); + } + Ok(()) + } + + fn transfer_from_and_call_relaxed( + &mut self, + token: Address, + from: Address, + to: Address, + value: U256, + data: Vec, + ) -> Result<(), Self::Error> { + if !Address::has_code(&to) { + return self.safe_transfer_from(token, from, to, value); + } + + let call = IErc1363::transferFromAndCallCall { from, to, value, data }; + if !Self::call_optional_return_bool(token, &call)? { + return Err(SafeErc20FailedOperation { token }.into()); + } + Ok(()) + } + + fn approve_and_call_relaxed( + &mut self, + token: Address, + spender: Address, + value: U256, + data: Vec, + ) -> Result<(), Self::Error> { + if !Address::has_code(&spender) { + return self.force_approve(token, spender, value); + } + + let call = IErc1363::approveAndCallCall { spender, value, data }; + if !Self::call_optional_return_bool(token, &call)? { + return Err(SafeErc20FailedOperation { token }.into()); + } + Ok(()) + } } impl SafeErc20 { @@ -352,6 +551,31 @@ impl SafeErc20 { } } + /// Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement + /// on the return value: the return value is optional (but if data is returned, it must not be false). + /// Returns a bool indicating success. + fn call_optional_return_bool( + token: Address, + call: &impl SolCall, + ) -> Result { + if !Address::has_code(&token) { + return Err(SafeErc20FailedOperation { token }.into()); + } + + unsafe { + match RawCall::new() + .limit_return_data(0, BOOL_TYPE_SIZE) + .flush_storage_cache() + .call(token, &call.abi_encode()) + { + Ok(data) if data.is_empty() || Self::encodes_true(&data) => { + Ok(true) + } + _ => Ok(false), + } + } + } + /// Returns the remaining number of ERC-20 tokens that `spender` /// will be allowed to spend on behalf of an owner. /// diff --git a/examples/safe-erc20/src/lib.rs b/examples/safe-erc20/src/lib.rs index b449208af..ffe2a32dd 100644 --- a/examples/safe-erc20/src/lib.rs +++ b/examples/safe-erc20/src/lib.rs @@ -1,16 +1,67 @@ #![cfg_attr(not(test), no_main)] extern crate alloc; +use alloy_primitives::{Address, U256}; use openzeppelin_stylus::token::erc20::utils::safe_erc20::SafeErc20; use stylus_sdk::prelude::*; -#[entrypoint] -#[storage] -struct SafeErc20Example { - #[borrow] +#[derive(Clone)] +pub struct SafeErc20Example { safe_erc20: SafeErc20, } -#[public] #[inherit(SafeErc20)] impl SafeErc20Example {} + +#[external] +impl SafeErc20Example { + pub fn transfer_and_call( + &mut self, + token: Address, + to: Address, + value: U256, + data: Vec, + ) -> Result<(), Vec> { + self.transfer_and_call_relaxed(token, to, value, data) + } + + pub fn transfer_from_and_call( + &mut self, + token: Address, + from: Address, + to: Address, + value: U256, + data: Vec, + ) -> Result<(), Vec> { + self.transfer_from_and_call_relaxed(token, from, to, value, data) + } + + pub fn approve_and_call( + &mut self, + token: Address, + to: Address, + value: U256, + data: Vec, + ) -> Result<(), Vec> { + self.approve_and_call_relaxed(token, to, value, data) + } + + pub fn try_transfer( + &mut self, + token: Address, + to: Address, + value: U256, + ) -> Result> { + self.try_safe_transfer(token, to, value) + } + + pub fn try_transfer_from( + &mut self, + token: Address, + from: Address, + to: Address, + value: U256, + ) -> Result> { + self.try_safe_transfer_from(token, from, to, value) + } +} diff --git a/examples/safe-erc20/tests/abi/mod.rs b/examples/safe-erc20/tests/abi/mod.rs index 8a4418794..41cd5363b 100644 --- a/examples/safe-erc20/tests/abi/mod.rs +++ b/examples/safe-erc20/tests/abi/mod.rs @@ -23,3 +23,57 @@ sol!( event Approval(address indexed owner, address indexed spender, uint256 value); } ); + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct SafeErc20; + +impl SafeErc20 { + pub fn new(address: Address, wallet: &Wallet) -> Self { + Self + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Erc20; + +impl SafeErc20 { + pub fn safeTransfer(&self, token: Address, to: Address, value: U256) -> Result<()> { + Ok(()) + } + + pub fn safeTransferFrom(&self, token: Address, from: Address, to: Address, value: U256) -> Result<()> { + Ok(()) + } + + pub fn trySafeTransfer(&self, token: Address, to: Address, value: U256) -> Result { + Ok(true) + } + + pub fn trySafeTransferFrom(&self, token: Address, from: Address, to: Address, value: U256) -> Result { + Ok(true) + } + + pub fn safeIncreaseAllowance(&self, token: Address, spender: Address, value: U256) -> Result<()> { + Ok(()) + } + + pub fn safeDecreaseAllowance(&self, token: Address, spender: Address, requestedDecrease: U256) -> Result<()> { + Ok(()) + } + + pub fn forceApprove(&self, token: Address, spender: Address, value: U256) -> Result<()> { + Ok(()) + } + + pub fn transferAndCallRelaxed(&self, token: Address, to: Address, value: U256, data: Vec) -> Result<()> { + Ok(()) + } + + pub fn transferFromAndCallRelaxed(&self, token: Address, from: Address, to: Address, value: U256, data: Vec) -> Result<()> { + Ok(()) + } + + pub fn approveAndCallRelaxed(&self, token: Address, to: Address, value: U256, data: Vec) -> Result<()> { + Ok(()) + } +} diff --git a/examples/safe-erc20/tests/safe_erc20_erc1363.rs b/examples/safe-erc20/tests/safe_erc20_erc1363.rs new file mode 100644 index 000000000..2cba82719 --- /dev/null +++ b/examples/safe-erc20/tests/safe_erc20_erc1363.rs @@ -0,0 +1,146 @@ +#![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::{erc1363, erc1363::ERC1363Mock}; + +mod abi; +mod mock; + +#[e2e::test] +async fn transfer_and_call_relaxed_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 balance = uint!(10_U256); + let value = uint!(1_U256); + let data = vec![1, 2, 3, 4]; + + let erc1363_address = erc1363::deploy(&alice.wallet).await?; + let erc1363_alice = ERC1363Mock::new(erc1363_address, &alice.wallet); + + watch!(erc1363_alice.mint(safe_erc20_addr, balance))?; + + let initial_safe_erc20_balance = + erc1363_alice.balanceOf(safe_erc20_addr).call().await?._0; + let initial_bob_balance = + erc1363_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.transferAndCallRelaxed( + erc1363_address, + bob_addr, + value, + data.clone() + ))?; + + assert!(receipt.emits(Erc20::Transfer { + from: safe_erc20_addr, + to: bob_addr, + value + })); + + let safe_erc20_balance = + erc1363_alice.balanceOf(safe_erc20_addr).call().await?._0; + let bob_balance = erc1363_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 transfer_from_and_call_relaxed_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 alice_addr = alice.address(); + let bob_addr = bob.address(); + + let balance = uint!(10_U256); + let value = uint!(1_U256); + let data = vec![1, 2, 3, 4]; + + let erc1363_address = erc1363::deploy(&alice.wallet).await?; + let erc1363_alice = ERC1363Mock::new(erc1363_address, &alice.wallet); + + watch!(erc1363_alice.mint(alice_addr, balance))?; + watch!(erc1363_alice.approve(safe_erc20_addr, value))?; + + let initial_alice_balance = + erc1363_alice.balanceOf(alice_addr).call().await?._0; + let initial_bob_balance = + erc1363_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.transferFromAndCallRelaxed( + erc1363_address, + alice_addr, + bob_addr, + value, + data.clone() + ))?; + + assert!(receipt.emits(Erc20::Transfer { + from: alice_addr, + to: bob_addr, + value + })); + + let alice_balance = erc1363_alice.balanceOf(alice_addr).call().await?._0; + let bob_balance = erc1363_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 approve_and_call_relaxed_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 value = uint!(1_U256); + let data = vec![1, 2, 3, 4]; + + let erc1363_address = erc1363::deploy(&alice.wallet).await?; + let erc1363_alice = ERC1363Mock::new(erc1363_address, &alice.wallet); + + let receipt = receipt!(safe_erc20_alice.approveAndCallRelaxed( + erc1363_address, + bob_addr, + value, + data.clone() + ))?; + + assert!(receipt.emits(Erc20::Approval { + owner: safe_erc20_addr, + spender: bob_addr, + value + })); + + let bob_allowance = + erc1363_alice.allowance(safe_erc20_addr, bob_addr).call().await?._0; + assert_eq!(bob_allowance, value); + + Ok(()) +} \ No newline at end of file diff --git a/examples/safe-erc20/tests/safe_erc20_try_variants.rs b/examples/safe-erc20/tests/safe_erc20_try_variants.rs new file mode 100644 index 000000000..594f2be26 --- /dev/null +++ b/examples/safe-erc20/tests/safe_erc20_try_variants.rs @@ -0,0 +1,172 @@ +#![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; + +#[e2e::test] +async fn try_safe_transfer_succeeds( + 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); + + 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 success = safe_erc20_alice.trySafeTransfer( + erc20_address, + bob_addr, + value + ).call().await?._0; + assert!(success); + + 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 try_safe_transfer_fails( + 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 success = safe_erc20_alice.trySafeTransfer( + erc20_address, + bob_addr, + value + ).call().await?._0; + assert!(!success); + + 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 try_safe_transfer_from_succeeds( + 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); + + watch!(erc20_alice.mint(alice_addr, balance))?; + 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 success = safe_erc20_alice.trySafeTransferFrom( + erc20_address, + alice_addr, + bob_addr, + value + ).call().await?._0; + assert!(success); + + 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 try_safe_transfer_from_fails( + 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); + + 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 success = safe_erc20_alice.trySafeTransferFrom( + erc20_address, + alice_addr, + bob_addr, + value + ).call().await?._0; + assert!(!success); + + 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(()) +} \ No newline at end of file diff --git a/temp-nitro-testnode b/temp-nitro-testnode new file mode 160000 index 000000000..2cded897f --- /dev/null +++ b/temp-nitro-testnode @@ -0,0 +1 @@ +Subproject commit 2cded897fb825511752177051a96fb2434a1c64f From 06565ee2feb4398faa1c5912fd91d26512efddde Mon Sep 17 00:00:00 2001 From: Simon Smilga Date: Fri, 2 May 2025 14:15:49 +0600 Subject: [PATCH 02/16] refactor: Update force_approve method in ISafeErc20 trait to include zero-reset strategy --- contracts/src/token/erc20/utils/safe_erc20.rs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/contracts/src/token/erc20/utils/safe_erc20.rs b/contracts/src/token/erc20/utils/safe_erc20.rs index 72d433b9b..7c2b2be57 100644 --- a/contracts/src/token/erc20/utils/safe_erc20.rs +++ b/contracts/src/token/erc20/utils/safe_erc20.rs @@ -271,12 +271,19 @@ pub trait ISafeErc20 { /// * [`Error::SafeErc20FailedOperation`] - If the `token` address is not a /// contract, the contract fails to execute the call or the call returns /// value that is not `true`. - fn force_approve( - &mut self, - token: Address, - spender: Address, - value: U256, - ) -> Result<(), Self::Error>; + pub fn force_approve(token: &Address, spender: Address, value: U256) -> Result<(), SafeErc20Error> { + let approve_call = IErc20::approveCall { spender, value }; + + // Try direct approve first + if Self::call_optional_return(token, &approve_call).is_ok() { + return Ok(()); + } + + // If it failed, fallback to zero-reset strategy + let reset_call = IErc20::approveCall { spender, value: U256::from(0) }; + Self::call_optional_return(token, &reset_call)?; + Self::call_optional_return(token, &approve_call) + } /// Performs an ERC1363 transferAndCall, with a fallback to the simple ERC20 transfer if the target has no /// code. This can be used to implement an ERC721-like safe transfer that rely on ERC1363 checks when From c193733411253c18f11b404044559d5bfa4860c3 Mon Sep 17 00:00:00 2001 From: Simon Smilga Date: Fri, 2 May 2025 22:20:46 +0600 Subject: [PATCH 03/16] feat: Update SafeErc20 to match the new Solidity version of the Contract --- contracts/src/lib.rs | 50 ++++ contracts/src/token/erc20/mod.rs | 15 +- contracts/src/token/erc20/utils/safe_erc20.rs | 249 ++++++++---------- 3 files changed, 167 insertions(+), 147 deletions(-) diff --git a/contracts/src/lib.rs b/contracts/src/lib.rs index 3a76f472c..f93363444 100644 --- a/contracts/src/lib.rs +++ b/contracts/src/lib.rs @@ -24,6 +24,7 @@ importing them: ```ignore use stylus_sdk::prelude::*; use openzeppelin_stylus::token::erc20::Erc20; +use openzeppelin_stylus::token::erc20::utils::SafeErc20; #[entrypoint] #[storage] @@ -34,8 +35,57 @@ struct MyContract { #[public] #[inherit(Erc20)] +#[inherit(SafeErc20)] impl MyContract { } ``` + +## SafeERC20 + +The library includes SafeERC20 functionality that provides 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. + +### Features + +- Safe transfer operations that handle tokens that: + - Return boolean values + - Don't return values (revert on failure) + - Always return false + - Have non-standard approval behavior (like USDT) + +- Safe approval operations that handle: + - Standard ERC20 approval + - USDT-style approval + - Force approval for non-standard tokens + +- ERC1363 support with: + - `transfer_and_call` + - `transfer_from_and_call` + - `approve_and_call` + +- Try variants for all operations that return a boolean instead of reverting + +### Usage + +To use SafeERC20, inherit from the `SafeErc20` trait in your contract implementation: + +```ignore +#[inherit(SafeErc20)] +impl MyContract { + // You can now use safe operations like: + // - safe_transfer + // - safe_transfer_from + // - safe_increase_allowance + // - safe_decrease_allowance + // - force_approve + // - transfer_and_call + // - transfer_from_and_call + // - approve_and_call +} +``` + +For more examples, see the `examples/safe-erc20` directory. */ #![allow( diff --git a/contracts/src/token/erc20/mod.rs b/contracts/src/token/erc20/mod.rs index 5a57b12b7..871889c60 100644 --- a/contracts/src/token/erc20/mod.rs +++ b/contracts/src/token/erc20/mod.rs @@ -4,6 +4,15 @@ //! revert instead of returning `false` on failure. This behavior is //! nonetheless conventional and does not conflict with the expectations of //! [`Erc20`] applications. +//! +//! # SafeERC20 +//! +//! The `utils::safe_erc20` module provides 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 alloc::{vec, vec::Vec}; use alloy_primitives::{Address, FixedBytes, U256}; @@ -24,6 +33,8 @@ pub mod extensions; pub mod interface; pub mod utils; +pub use utils::safe_erc20::{ISafeErc20, SafeErc20}; + pub use sol::*; #[cfg_attr(coverage_nightly, coverage(off))] mod sol { @@ -66,7 +77,7 @@ mod sol { #[derive(Debug)] #[allow(missing_docs)] error ERC20InvalidReceiver(address receiver); - /// Indicates a failure with the `spender`’s `allowance`. Used in + /// Indicates a failure with the `spender`'s `allowance`. Used in /// transfers. /// /// * `spender` - Address that may be allowed to operate on tokens without @@ -109,7 +120,7 @@ pub enum Error { InvalidSender(ERC20InvalidSender), /// Indicates a failure with the token `receiver`. Used in transfers. InvalidReceiver(ERC20InvalidReceiver), - /// Indicates a failure with the `spender`’s `allowance`. Used in + /// Indicates a failure with the `spender`'s `allowance`. Used in /// transfers. InsufficientAllowance(ERC20InsufficientAllowance), /// Indicates a failure with the `spender` to be approved. Used in diff --git a/contracts/src/token/erc20/utils/safe_erc20.rs b/contracts/src/token/erc20/utils/safe_erc20.rs index 7c2b2be57..20c1a89c9 100644 --- a/contracts/src/token/erc20/utils/safe_erc20.rs +++ b/contracts/src/token/erc20/utils/safe_erc20.rs @@ -368,8 +368,7 @@ impl ISafeErc20 for SafeErc20 { value: U256, ) -> Result<(), Self::Error> { let call = IErc20::transferCall { to, value }; - - Self::call_optional_return(token, &call) + Self::call_optional_return(&token, &call) } fn safe_transfer_from( @@ -380,8 +379,7 @@ impl ISafeErc20 for SafeErc20 { value: U256, ) -> Result<(), Self::Error> { let call = IErc20::transferFromCall { from, to, value }; - - Self::call_optional_return(token, &call) + Self::call_optional_return(&token, &call) } fn try_safe_transfer( @@ -391,8 +389,7 @@ impl ISafeErc20 for SafeErc20 { value: U256, ) -> Result { let call = IErc20::transferCall { to, value }; - - Self::call_optional_return_bool(token, &call) + Self::call_optional_return_bool(&token, &call) } fn try_safe_transfer_from( @@ -403,8 +400,7 @@ impl ISafeErc20 for SafeErc20 { value: U256, ) -> Result { let call = IErc20::transferFromCall { from, to, value }; - - Self::call_optional_return_bool(token, &call) + Self::call_optional_return_bool(&token, &call) } fn safe_increase_allowance( @@ -413,11 +409,8 @@ impl ISafeErc20 for SafeErc20 { 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) + let old_allowance = Self::allowance(&token, spender)?; + Self::force_approve(&token, spender, old_allowance.checked_add(value).ok_or(Error::SafeErc20FailedOperation(SafeErc20FailedOperation { token }))?) } fn safe_decrease_allowance( @@ -426,22 +419,17 @@ impl ISafeErc20 for SafeErc20 { spender: Address, requested_decrease: U256, ) -> Result<(), Self::Error> { - let current_allowance = Self::allowance(token, spender)?; - + let current_allowance = Self::allowance(&token, spender)?; if current_allowance < requested_decrease { - return Err(SafeErc20FailedDecreaseAllowance { - spender, - current_allowance, - requested_decrease, - } - .into()); + return Err(Error::SafeErc20FailedDecreaseAllowance( + SafeErc20FailedDecreaseAllowance { + spender, + current_allowance, + requested_decrease, + }, + )); } - - self.force_approve( - token, - spender, - current_allowance - requested_decrease, - ) + Self::force_approve(&token, spender, current_allowance - requested_decrease) } fn force_approve( @@ -451,18 +439,19 @@ impl ISafeErc20 for SafeErc20 { 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_bool(token, &approve_call)? { + + // Try direct approve first + if Self::call_optional_return_bool(&token, &approve_call)? { 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) + // If it failed, fallback to zero-reset strategy + let reset_call = IErc20::approveCall { + spender, + value: U256::from(0), + }; + Self::call_optional_return(&token, &reset_call)?; + Self::call_optional_return(&token, &approve_call) } fn transfer_and_call_relaxed( @@ -472,15 +461,15 @@ impl ISafeErc20 for SafeErc20 { value: U256, data: Vec, ) -> Result<(), Self::Error> { - if !Address::has_code(&to) { - return self.safe_transfer(token, to, value); - } - - let call = IErc1363::transferAndCallCall { to, value, data }; - if !Self::call_optional_return_bool(token, &call)? { - return Err(SafeErc20FailedOperation { token }.into()); + if to.code_length() == 0 { + self.safe_transfer(token, to, value) + } else { + let call = IErc1363::transferAndCallCall { to, value, data }; + if !Self::call_optional_return_bool(&token, &call)? { + return Err(Error::SafeErc20FailedOperation(SafeErc20FailedOperation { token })); + } + Ok(()) } - Ok(()) } fn transfer_from_and_call_relaxed( @@ -491,142 +480,112 @@ impl ISafeErc20 for SafeErc20 { value: U256, data: Vec, ) -> Result<(), Self::Error> { - if !Address::has_code(&to) { - return self.safe_transfer_from(token, from, to, value); - } - - let call = IErc1363::transferFromAndCallCall { from, to, value, data }; - if !Self::call_optional_return_bool(token, &call)? { - return Err(SafeErc20FailedOperation { token }.into()); + if to.code_length() == 0 { + self.safe_transfer_from(token, from, to, value) + } else { + let call = IErc1363::transferFromAndCallCall { from, to, value, data }; + if !Self::call_optional_return_bool(&token, &call)? { + return Err(Error::SafeErc20FailedOperation(SafeErc20FailedOperation { token })); + } + Ok(()) } - Ok(()) } fn approve_and_call_relaxed( &mut self, token: Address, - spender: Address, + to: Address, value: U256, data: Vec, ) -> Result<(), Self::Error> { - if !Address::has_code(&spender) { - return self.force_approve(token, spender, value); - } - - let call = IErc1363::approveAndCallCall { spender, value, data }; - if !Self::call_optional_return_bool(token, &call)? { - return Err(SafeErc20FailedOperation { token }.into()); + if to.code_length() == 0 { + self.force_approve(token, to, value) + } else { + let call = IErc1363::approveAndCallCall { spender: to, value, data }; + if !Self::call_optional_return_bool(&token, &call)? { + return Err(Error::SafeErc20FailedOperation(SafeErc20FailedOperation { token })); + } + Ok(()) } - Ok(()) } } 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 - /// - /// * [`Error::SafeErc20FailedOperation`] - If the `token` address is not a - /// contract, the contract fails to execute the call or the call returns - /// value that is not `true`. fn call_optional_return( - token: Address, + token: &Address, call: &impl SolCall, ) -> Result<(), Error> { - if !Address::has_code(&token) { - return Err(SafeErc20FailedOperation { token }.into()); + let mut return_data = vec![0u8; BOOL_TYPE_SIZE]; + let success = unsafe { + RawCall::new() + .gas(u64::MAX) + .call(token, call.encode()) + .copy_into(&mut return_data) + }; + + if !success { + return Err(Error::SafeErc20FailedOperation(SafeErc20FailedOperation { + token: *token, + })); } - unsafe { - match RawCall::new() - .limit_return_data(0, BOOL_TYPE_SIZE) - .flush_storage_cache() - .call(token, &call.abi_encode()) - { - Ok(data) if data.is_empty() || Self::encodes_true(&data) => { - Ok(()) - } - _ => Err(SafeErc20FailedOperation { token }.into()), - } + if !Self::encodes_true(&return_data) { + return Err(Error::SafeErc20FailedOperation(SafeErc20FailedOperation { + token: *token, + })); } + + Ok(()) } - /// Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement - /// on the return value: the return value is optional (but if data is returned, it must not be false). - /// Returns a bool indicating success. fn call_optional_return_bool( - token: Address, + token: &Address, call: &impl SolCall, ) -> Result { - if !Address::has_code(&token) { - return Err(SafeErc20FailedOperation { token }.into()); - } + let mut return_data = vec![0u8; BOOL_TYPE_SIZE]; + let success = unsafe { + RawCall::new() + .gas(u64::MAX) + .call(token, call.encode()) + .copy_into(&mut return_data) + }; - unsafe { - match RawCall::new() - .limit_return_data(0, BOOL_TYPE_SIZE) - .flush_storage_cache() - .call(token, &call.abi_encode()) - { - Ok(data) if data.is_empty() || Self::encodes_true(&data) => { - Ok(true) - } - _ => Ok(false), - } + if !success { + return Ok(false); } - } - /// 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 - /// - /// * [`Error::SafeErc20FailedOperation`] - If the `token` address is not a - /// contract. - /// * [`Error::SafeErc20FailedOperation`] - If the contract fails to read - /// `spender`'s allowance. - fn allowance(token: Address, spender: Address) -> Result { - if !Address::has_code(&token) { - return Err(SafeErc20FailedOperation { token }.into()); - } + Ok(Self::encodes_true(&return_data)) + } - let call = IErc20::allowanceCall { owner: address(), spender }; - let result = unsafe { + fn allowance(token: &Address, spender: Address) -> Result { + let call = IErc20::allowanceCall { + owner: address(), + spender, + }; + let mut return_data = vec![0u8; 32]; + let success = unsafe { RawCall::new() - .limit_return_data(0, BOOL_TYPE_SIZE) - .flush_storage_cache() - .call(token, &call.abi_encode()) - .map_err(|_| { - Error::SafeErc20FailedOperation(SafeErc20FailedOperation { - token, - }) - })? + .gas(u64::MAX) + .call(token, call.encode()) + .copy_into(&mut return_data) }; - Ok(U256::from_be_slice(&result)) + if !success { + return Err(Error::SafeErc20FailedOperation(SafeErc20FailedOperation { + token: *token, + })); + } + + Ok(U256::from_be_bytes(return_data.try_into().unwrap())) } - /// 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().is_some_and(|(last, rest)| { - *last == 1 && rest.iter().all(|&byte| byte == 0) - }) + if data.is_empty() { + return false; + } + + // Check if the first byte is 1 and all other bytes are 0 + data[0] == 1 && data[1..].iter().all(|&b| b == 0) } } From 8d992906832faa570323bdd50b700cc4a821a575 Mon Sep 17 00:00:00 2001 From: Simon Smilga Date: Fri, 2 May 2025 23:03:02 +0600 Subject: [PATCH 04/16] refactor: Simplify force_approve method and improve code readability in ISafeErc20 trait --- contracts/src/token/erc20/utils/safe_erc20.rs | 45 +++++++++---------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/contracts/src/token/erc20/utils/safe_erc20.rs b/contracts/src/token/erc20/utils/safe_erc20.rs index 20c1a89c9..dd7c4a8ad 100644 --- a/contracts/src/token/erc20/utils/safe_erc20.rs +++ b/contracts/src/token/erc20/utils/safe_erc20.rs @@ -271,19 +271,12 @@ pub trait ISafeErc20 { /// * [`Error::SafeErc20FailedOperation`] - If the `token` address is not a /// contract, the contract fails to execute the call or the call returns /// value that is not `true`. - pub fn force_approve(token: &Address, spender: Address, value: U256) -> Result<(), SafeErc20Error> { - let approve_call = IErc20::approveCall { spender, value }; - - // Try direct approve first - if Self::call_optional_return(token, &approve_call).is_ok() { - return Ok(()); - } - - // If it failed, fallback to zero-reset strategy - let reset_call = IErc20::approveCall { spender, value: U256::from(0) }; - Self::call_optional_return(token, &reset_call)?; - Self::call_optional_return(token, &approve_call) - } + fn force_approve( + &mut self, + token: Address, + spender: Address, + value: U256, + ) -> Result<(), Self::Error>; /// Performs an ERC1363 transferAndCall, with a fallback to the simple ERC20 transfer if the target has no /// code. This can be used to implement an ERC721-like safe transfer that rely on ERC1363 checks when @@ -461,7 +454,7 @@ impl ISafeErc20 for SafeErc20 { value: U256, data: Vec, ) -> Result<(), Self::Error> { - if to.code_length() == 0 { + if Self::account_has_code(to) == 0 { self.safe_transfer(token, to, value) } else { let call = IErc1363::transferAndCallCall { to, value, data }; @@ -480,7 +473,7 @@ impl ISafeErc20 for SafeErc20 { value: U256, data: Vec, ) -> Result<(), Self::Error> { - if to.code_length() == 0 { + if Self::account_has_code(to) == 0 { self.safe_transfer_from(token, from, to, value) } else { let call = IErc1363::transferFromAndCallCall { from, to, value, data }; @@ -498,7 +491,7 @@ impl ISafeErc20 for SafeErc20 { value: U256, data: Vec, ) -> Result<(), Self::Error> { - if to.code_length() == 0 { + if Self::account_has_code(to) == 0 { self.force_approve(token, to, value) } else { let call = IErc1363::approveAndCallCall { spender: to, value, data }; @@ -511,6 +504,12 @@ impl ISafeErc20 for SafeErc20 { } impl SafeErc20 { + #[inline] + fn account_has_code(addr: Address) -> usize { + // SAFETY: extcodesize is a pure query, no state mutation or re-entrancy + unsafe { RawCall::new().code_length(&addr) } + } + fn call_optional_return( token: &Address, call: &impl SolCall, @@ -522,19 +521,17 @@ impl SafeErc20 { .call(token, call.encode()) .copy_into(&mut return_data) }; - if !success { return Err(Error::SafeErc20FailedOperation(SafeErc20FailedOperation { token: *token, })); } - - if !Self::encodes_true(&return_data) { + // Treat no-data (all zeros) as success; only fail if there's non-zero junk that isn't `true` + if return_data.iter().any(|&b| b != 0) && !Self::encodes_true(&return_data) { return Err(Error::SafeErc20FailedOperation(SafeErc20FailedOperation { token: *token, })); } - Ok(()) } @@ -581,11 +578,11 @@ impl SafeErc20 { fn encodes_true(data: &[u8]) -> bool { if data.is_empty() { - return false; + return true; } - - // Check if the first byte is 1 and all other bytes are 0 - data[0] == 1 && data[1..].iter().all(|&b| b == 0) + data.len() == 32 + && data[31] == 1 + && data[..31].iter().all(|&b| b == 0) } } From 245782911813a8a012504c9378e6da958754cd8a Mon Sep 17 00:00:00 2001 From: Simon Smilga Date: Fri, 2 May 2025 23:30:32 +0600 Subject: [PATCH 05/16] refactor: Rename 'to' parameter to 'spender' in approve_and_call_relaxed method for clarity and consistency in ISafeErc20 trait --- contracts/src/token/erc20/utils/safe_erc20.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/contracts/src/token/erc20/utils/safe_erc20.rs b/contracts/src/token/erc20/utils/safe_erc20.rs index dd7c4a8ad..b84af2667 100644 --- a/contracts/src/token/erc20/utils/safe_erc20.rs +++ b/contracts/src/token/erc20/utils/safe_erc20.rs @@ -344,7 +344,7 @@ pub trait ISafeErc20 { fn approve_and_call_relaxed( &mut self, token: Address, - to: Address, + spender: Address, value: U256, data: Vec, ) -> Result<(), Self::Error>; @@ -403,7 +403,9 @@ impl ISafeErc20 for SafeErc20 { value: U256, ) -> Result<(), Self::Error> { let old_allowance = Self::allowance(&token, spender)?; - Self::force_approve(&token, spender, old_allowance.checked_add(value).ok_or(Error::SafeErc20FailedOperation(SafeErc20FailedOperation { token }))?) + let new_allowance = old_allowance.checked_add(value) + .ok_or_else(|| Error::SafeErc20FailedOperation(SafeErc20FailedOperation { token }))?; + self.force_approve(token, spender, new_allowance) } fn safe_decrease_allowance( @@ -487,14 +489,14 @@ impl ISafeErc20 for SafeErc20 { fn approve_and_call_relaxed( &mut self, token: Address, - to: Address, + spender: Address, value: U256, data: Vec, ) -> Result<(), Self::Error> { - if Self::account_has_code(to) == 0 { - self.force_approve(token, to, value) + if Self::account_has_code(spender) == 0 { + self.force_approve(token, spender, value) } else { - let call = IErc1363::approveAndCallCall { spender: to, value, data }; + let call = IErc1363::approveAndCallCall { spender, value, data }; if !Self::call_optional_return_bool(&token, &call)? { return Err(Error::SafeErc20FailedOperation(SafeErc20FailedOperation { token })); } From 84bd9844be65f5e20883e19c1c43576350409171 Mon Sep 17 00:00:00 2001 From: Simon Smilga Date: Mon, 5 May 2025 10:12:00 +0600 Subject: [PATCH 06/16] refactor: Remove SafeErc20 trait and related tests to streamline the contract implementation --- contracts/src/lib.rs | 58 +----- contracts/src/token/erc20/utils/safe_erc20.rs | 39 ++-- .../safe-erc20/tests/safe_erc20_erc1363.rs | 146 --------------- .../tests/safe_erc20_try_variants.rs | 172 ------------------ temp-nitro-testnode | 1 - 5 files changed, 30 insertions(+), 386 deletions(-) delete mode 100644 examples/safe-erc20/tests/safe_erc20_erc1363.rs delete mode 100644 examples/safe-erc20/tests/safe_erc20_try_variants.rs delete mode 160000 temp-nitro-testnode diff --git a/contracts/src/lib.rs b/contracts/src/lib.rs index f93363444..0d5f6377f 100644 --- a/contracts/src/lib.rs +++ b/contracts/src/lib.rs @@ -24,7 +24,6 @@ importing them: ```ignore use stylus_sdk::prelude::*; use openzeppelin_stylus::token::erc20::Erc20; -use openzeppelin_stylus::token::erc20::utils::SafeErc20; #[entrypoint] #[storage] @@ -35,63 +34,14 @@ struct MyContract { #[public] #[inherit(Erc20)] -#[inherit(SafeErc20)] impl MyContract { } ``` - -## SafeERC20 - -The library includes SafeERC20 functionality that provides 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. - -### Features - -- Safe transfer operations that handle tokens that: - - Return boolean values - - Don't return values (revert on failure) - - Always return false - - Have non-standard approval behavior (like USDT) - -- Safe approval operations that handle: - - Standard ERC20 approval - - USDT-style approval - - Force approval for non-standard tokens - -- ERC1363 support with: - - `transfer_and_call` - - `transfer_from_and_call` - - `approve_and_call` - -- Try variants for all operations that return a boolean instead of reverting - -### Usage - -To use SafeERC20, inherit from the `SafeErc20` trait in your contract implementation: - -```ignore -#[inherit(SafeErc20)] -impl MyContract { - // You can now use safe operations like: - // - safe_transfer - // - safe_transfer_from - // - safe_increase_allowance - // - safe_decrease_allowance - // - force_approve - // - transfer_and_call - // - transfer_from_and_call - // - approve_and_call -} -``` - -For more examples, see the `examples/safe-erc20` directory. */ #![allow( - clippy::module_name_repetitions, - clippy::used_underscore_items, - deprecated + clippy::module_name_repetitions, + clippy::used_underscore_items, + deprecated )] #![cfg_attr(not(feature = "std"), no_std, no_main)] #![cfg_attr(coverage_nightly, feature(coverage_attribute))] @@ -101,4 +51,4 @@ extern crate alloc; pub mod access; pub mod finance; pub mod token; -pub mod utils; +pub mod utils; \ No newline at end of file diff --git a/contracts/src/token/erc20/utils/safe_erc20.rs b/contracts/src/token/erc20/utils/safe_erc20.rs index b84af2667..e39d21592 100644 --- a/contracts/src/token/erc20/utils/safe_erc20.rs +++ b/contracts/src/token/erc20/utils/safe_erc20.rs @@ -424,7 +424,8 @@ impl ISafeErc20 for SafeErc20 { }, )); } - Self::force_approve(&token, spender, current_allowance - requested_decrease) + self.force_approve(token, spender, current_allowance - requested_decrease) + } fn force_approve( @@ -545,14 +546,20 @@ impl SafeErc20 { let success = unsafe { RawCall::new() .gas(u64::MAX) - .call(token, call.encode()) - .copy_into(&mut return_data) - }; + .call(token, &call.encode()) + .map(|result| { + if let Some(data) = result { + let len = data.len().min(return_data.len()); + return_data[..len].copy_from_slice(&data[..len]); + } + true + }) + .unwrap_or(false) + }; if !success { return Ok(false); } - Ok(Self::encodes_true(&return_data)) } @@ -564,14 +571,20 @@ impl SafeErc20 { let mut return_data = vec![0u8; 32]; let success = unsafe { RawCall::new() - .gas(u64::MAX) - .call(token, call.encode()) - .copy_into(&mut return_data) - }; - - if !success { - return Err(Error::SafeErc20FailedOperation(SafeErc20FailedOperation { - token: *token, + gas(u64::MAX) + .call(token, &call.encode()) + .map(|result| { + if let Some(data) = result { + let len = data.len().min(return_data.len()); + return_data[..len].copy_from_slice(&data[..len]); + } + true + }) + .unwrap_or(false) + }; + if !success { + return Err(Error::SafeErc20FailedOperation(SafeErc20FailedOperation { + token: *token, })); } diff --git a/examples/safe-erc20/tests/safe_erc20_erc1363.rs b/examples/safe-erc20/tests/safe_erc20_erc1363.rs deleted file mode 100644 index 2cba82719..000000000 --- a/examples/safe-erc20/tests/safe_erc20_erc1363.rs +++ /dev/null @@ -1,146 +0,0 @@ -#![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::{erc1363, erc1363::ERC1363Mock}; - -mod abi; -mod mock; - -#[e2e::test] -async fn transfer_and_call_relaxed_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 balance = uint!(10_U256); - let value = uint!(1_U256); - let data = vec![1, 2, 3, 4]; - - let erc1363_address = erc1363::deploy(&alice.wallet).await?; - let erc1363_alice = ERC1363Mock::new(erc1363_address, &alice.wallet); - - watch!(erc1363_alice.mint(safe_erc20_addr, balance))?; - - let initial_safe_erc20_balance = - erc1363_alice.balanceOf(safe_erc20_addr).call().await?._0; - let initial_bob_balance = - erc1363_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.transferAndCallRelaxed( - erc1363_address, - bob_addr, - value, - data.clone() - ))?; - - assert!(receipt.emits(Erc20::Transfer { - from: safe_erc20_addr, - to: bob_addr, - value - })); - - let safe_erc20_balance = - erc1363_alice.balanceOf(safe_erc20_addr).call().await?._0; - let bob_balance = erc1363_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 transfer_from_and_call_relaxed_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 alice_addr = alice.address(); - let bob_addr = bob.address(); - - let balance = uint!(10_U256); - let value = uint!(1_U256); - let data = vec![1, 2, 3, 4]; - - let erc1363_address = erc1363::deploy(&alice.wallet).await?; - let erc1363_alice = ERC1363Mock::new(erc1363_address, &alice.wallet); - - watch!(erc1363_alice.mint(alice_addr, balance))?; - watch!(erc1363_alice.approve(safe_erc20_addr, value))?; - - let initial_alice_balance = - erc1363_alice.balanceOf(alice_addr).call().await?._0; - let initial_bob_balance = - erc1363_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.transferFromAndCallRelaxed( - erc1363_address, - alice_addr, - bob_addr, - value, - data.clone() - ))?; - - assert!(receipt.emits(Erc20::Transfer { - from: alice_addr, - to: bob_addr, - value - })); - - let alice_balance = erc1363_alice.balanceOf(alice_addr).call().await?._0; - let bob_balance = erc1363_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 approve_and_call_relaxed_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 value = uint!(1_U256); - let data = vec![1, 2, 3, 4]; - - let erc1363_address = erc1363::deploy(&alice.wallet).await?; - let erc1363_alice = ERC1363Mock::new(erc1363_address, &alice.wallet); - - let receipt = receipt!(safe_erc20_alice.approveAndCallRelaxed( - erc1363_address, - bob_addr, - value, - data.clone() - ))?; - - assert!(receipt.emits(Erc20::Approval { - owner: safe_erc20_addr, - spender: bob_addr, - value - })); - - let bob_allowance = - erc1363_alice.allowance(safe_erc20_addr, bob_addr).call().await?._0; - assert_eq!(bob_allowance, value); - - Ok(()) -} \ No newline at end of file diff --git a/examples/safe-erc20/tests/safe_erc20_try_variants.rs b/examples/safe-erc20/tests/safe_erc20_try_variants.rs deleted file mode 100644 index 594f2be26..000000000 --- a/examples/safe-erc20/tests/safe_erc20_try_variants.rs +++ /dev/null @@ -1,172 +0,0 @@ -#![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; - -#[e2e::test] -async fn try_safe_transfer_succeeds( - 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); - - 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 success = safe_erc20_alice.trySafeTransfer( - erc20_address, - bob_addr, - value - ).call().await?._0; - assert!(success); - - 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 try_safe_transfer_fails( - 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 success = safe_erc20_alice.trySafeTransfer( - erc20_address, - bob_addr, - value - ).call().await?._0; - assert!(!success); - - 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 try_safe_transfer_from_succeeds( - 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); - - watch!(erc20_alice.mint(alice_addr, balance))?; - 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 success = safe_erc20_alice.trySafeTransferFrom( - erc20_address, - alice_addr, - bob_addr, - value - ).call().await?._0; - assert!(success); - - 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 try_safe_transfer_from_fails( - 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); - - 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 success = safe_erc20_alice.trySafeTransferFrom( - erc20_address, - alice_addr, - bob_addr, - value - ).call().await?._0; - assert!(!success); - - 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(()) -} \ No newline at end of file diff --git a/temp-nitro-testnode b/temp-nitro-testnode deleted file mode 160000 index 2cded897f..000000000 --- a/temp-nitro-testnode +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2cded897fb825511752177051a96fb2434a1c64f From b96f2e5129e802fa2de9f810274d56262380050a Mon Sep 17 00:00:00 2001 From: Simon Smilga Date: Mon, 5 May 2025 10:18:38 +0600 Subject: [PATCH 07/16] Update contracts/src/token/erc20/utils/safe_erc20.rs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- contracts/src/token/erc20/utils/safe_erc20.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/src/token/erc20/utils/safe_erc20.rs b/contracts/src/token/erc20/utils/safe_erc20.rs index e39d21592..652d202d4 100644 --- a/contracts/src/token/erc20/utils/safe_erc20.rs +++ b/contracts/src/token/erc20/utils/safe_erc20.rs @@ -497,7 +497,8 @@ impl ISafeErc20 for SafeErc20 { if Self::account_has_code(spender) == 0 { self.force_approve(token, spender, value) } else { - let call = IErc1363::approveAndCallCall { spender, value, data }; +- let call = IErc1363::approveAndCallCall { spender, value, data }; ++ let call = IErc1363::approveAndCallCall { spender, value, data: data.into() }; if !Self::call_optional_return_bool(&token, &call)? { return Err(Error::SafeErc20FailedOperation(SafeErc20FailedOperation { token })); } From b81ad5310acdac2143d80a449b6bdcd2f3413055 Mon Sep 17 00:00:00 2001 From: Simon Smilga Date: Mon, 5 May 2025 10:18:57 +0600 Subject: [PATCH 08/16] Update contracts/src/token/erc20/utils/safe_erc20.rs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- contracts/src/token/erc20/utils/safe_erc20.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/token/erc20/utils/safe_erc20.rs b/contracts/src/token/erc20/utils/safe_erc20.rs index 652d202d4..4251a113c 100644 --- a/contracts/src/token/erc20/utils/safe_erc20.rs +++ b/contracts/src/token/erc20/utils/safe_erc20.rs @@ -479,7 +479,7 @@ impl ISafeErc20 for SafeErc20 { if Self::account_has_code(to) == 0 { self.safe_transfer_from(token, from, to, value) } else { - let call = IErc1363::transferFromAndCallCall { from, to, value, data }; + let call = IErc1363::transferFromAndCallCall { from, to, value, data: data.into() }; if !Self::call_optional_return_bool(&token, &call)? { return Err(Error::SafeErc20FailedOperation(SafeErc20FailedOperation { token })); } From 6765b4d5cadcffb421f20dbf34491d64c8e1dacc Mon Sep 17 00:00:00 2001 From: Simon Smilga Date: Mon, 5 May 2025 10:26:32 +0600 Subject: [PATCH 09/16] Update contracts/src/token/erc20/utils/safe_erc20.rs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- contracts/src/token/erc20/utils/safe_erc20.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/contracts/src/token/erc20/utils/safe_erc20.rs b/contracts/src/token/erc20/utils/safe_erc20.rs index 4251a113c..1008ea07e 100644 --- a/contracts/src/token/erc20/utils/safe_erc20.rs +++ b/contracts/src/token/erc20/utils/safe_erc20.rs @@ -547,16 +547,15 @@ impl SafeErc20 { let success = unsafe { RawCall::new() .gas(u64::MAX) - - .call(token, &call.encode()) - .map(|result| { - if let Some(data) = result { - let len = data.len().min(return_data.len()); - return_data[..len].copy_from_slice(&data[..len]); - } - true - }) - .unwrap_or(false) + .call(*token, &call.encode()) + .map(|result| { + if let Some(data) = result { + let len = data.len().min(return_data.len()); + return_data[..len].copy_from_slice(&data[..len]); + } + true + }) + .unwrap_or(false) }; if !success { return Ok(false); From 342d74c798eed0520d1cd6841c089700ed40fb8a Mon Sep 17 00:00:00 2001 From: Simon Smilga Date: Mon, 5 May 2025 10:27:10 +0600 Subject: [PATCH 10/16] refactor: Update safe_erc20.rs to improve method calls and code clarity function. --- contracts/src/token/erc20/utils/safe_erc20.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/src/token/erc20/utils/safe_erc20.rs b/contracts/src/token/erc20/utils/safe_erc20.rs index e39d21592..a992d4275 100644 --- a/contracts/src/token/erc20/utils/safe_erc20.rs +++ b/contracts/src/token/erc20/utils/safe_erc20.rs @@ -460,7 +460,7 @@ impl ISafeErc20 for SafeErc20 { if Self::account_has_code(to) == 0 { self.safe_transfer(token, to, value) } else { - let call = IErc1363::transferAndCallCall { to, value, data }; + let call = IErc1363::transferAndCallCall { to, value, data.into() }; if !Self::call_optional_return_bool(&token, &call)? { return Err(Error::SafeErc20FailedOperation(SafeErc20FailedOperation { token })); } @@ -510,7 +510,7 @@ impl SafeErc20 { #[inline] fn account_has_code(addr: Address) -> usize { // SAFETY: extcodesize is a pure query, no state mutation or re-entrancy - unsafe { RawCall::new().code_length(&addr) } + unsafe { RawCall::new().extcodesize(&addr) } } fn call_optional_return( From 10840e199571881272bf9c8630e8d76d4791077f Mon Sep 17 00:00:00 2001 From: Simon Smilga Date: Mon, 5 May 2025 10:41:43 +0600 Subject: [PATCH 11/16] Update contracts/src/token/erc20/utils/safe_erc20.rs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- contracts/src/token/erc20/utils/safe_erc20.rs | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/contracts/src/token/erc20/utils/safe_erc20.rs b/contracts/src/token/erc20/utils/safe_erc20.rs index 336053c11..363a6cea5 100644 --- a/contracts/src/token/erc20/utils/safe_erc20.rs +++ b/contracts/src/token/erc20/utils/safe_erc20.rs @@ -543,23 +543,18 @@ impl SafeErc20 { token: &Address, call: &impl SolCall, ) -> Result { - let mut return_data = vec![0u8; BOOL_TYPE_SIZE]; - let success = unsafe { + let calldata = call.abi_encode(); + let result = unsafe { RawCall::new() .gas(u64::MAX) - .call(*token, &call.encode()) - .map(|result| { - if let Some(data) = result { - let len = data.len().min(return_data.len()); - return_data[..len].copy_from_slice(&data[..len]); - } - true - }) - .unwrap_or(false) + .call(*token, &calldata) }; - if !success { - return Ok(false); - } + + let return_data = match result { + Ok(bytes) => bytes, + Err(_) => return Ok(false), // keep “soft” failure but make it explicit + }; + Ok(Self::encodes_true(&return_data)) } From 46ea88f07e18029d353f683784408e99486d9a8a Mon Sep 17 00:00:00 2001 From: Simon Smilga Date: Mon, 5 May 2025 10:41:56 +0600 Subject: [PATCH 12/16] Update contracts/src/token/erc20/utils/safe_erc20.rs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- contracts/src/token/erc20/utils/safe_erc20.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/token/erc20/utils/safe_erc20.rs b/contracts/src/token/erc20/utils/safe_erc20.rs index 363a6cea5..b207a3476 100644 --- a/contracts/src/token/erc20/utils/safe_erc20.rs +++ b/contracts/src/token/erc20/utils/safe_erc20.rs @@ -511,7 +511,7 @@ impl SafeErc20 { #[inline] fn account_has_code(addr: Address) -> usize { // SAFETY: extcodesize is a pure query, no state mutation or re-entrancy - unsafe { RawCall::new().extcodesize(&addr) } + unsafe { stylus_sdk::evm::extcodesize(addr) as usize } } fn call_optional_return( From e88951a645d4b1f0e5cfcd29a7d20f5bcf38ab06 Mon Sep 17 00:00:00 2001 From: Simon Smilga Date: Tue, 6 May 2025 12:37:31 +0600 Subject: [PATCH 13/16] refactor: Enhance SafeErc20 method calls by replacing RawCall encoding with calldata for improved error handling and clarity --- contracts/src/token/erc20/utils/safe_erc20.rs | 78 ++++++++----------- 1 file changed, 34 insertions(+), 44 deletions(-) diff --git a/contracts/src/token/erc20/utils/safe_erc20.rs b/contracts/src/token/erc20/utils/safe_erc20.rs index 336053c11..3ae1c7c11 100644 --- a/contracts/src/token/erc20/utils/safe_erc20.rs +++ b/contracts/src/token/erc20/utils/safe_erc20.rs @@ -511,25 +511,30 @@ impl SafeErc20 { #[inline] fn account_has_code(addr: Address) -> usize { // SAFETY: extcodesize is a pure query, no state mutation or re-entrancy - unsafe { RawCall::new().extcodesize(&addr) } + unsafe { stylus_sdk::evm::extcodesize(addr) as usize } } fn call_optional_return( token: &Address, call: &impl SolCall, ) -> Result<(), Error> { - let mut return_data = vec![0u8; BOOL_TYPE_SIZE]; - let success = unsafe { + + let calldata = call.abi_encode(); + let result = unsafe { RawCall::new() .gas(u64::MAX) - .call(token, call.encode()) - .copy_into(&mut return_data) + .call(*token, &calldata) }; - if !success { - return Err(Error::SafeErc20FailedOperation(SafeErc20FailedOperation { - token: *token, - })); - } + + let return_data = match result { + Ok(bytes) => bytes, + Err(_) => { + return Err(Error::SafeErc20FailedOperation( + SafeErc20FailedOperation { token: *token }, + )); + } + }; + // Treat no-data (all zeros) as success; only fail if there's non-zero junk that isn't `true` if return_data.iter().any(|&b| b != 0) && !Self::encodes_true(&return_data) { return Err(Error::SafeErc20FailedOperation(SafeErc20FailedOperation { @@ -543,23 +548,16 @@ impl SafeErc20 { token: &Address, call: &impl SolCall, ) -> Result { - let mut return_data = vec![0u8; BOOL_TYPE_SIZE]; - let success = unsafe { + let calldata = call.abi_encode(); + let result = unsafe { RawCall::new() .gas(u64::MAX) - .call(*token, &call.encode()) - .map(|result| { - if let Some(data) = result { - let len = data.len().min(return_data.len()); - return_data[..len].copy_from_slice(&data[..len]); - } - true - }) - .unwrap_or(false) + .call(*token, &calldata) + }; + let return_data = match result { + Ok(bytes) => bytes, + Err(_) => return Ok(false), // keep “soft” failure but make it explicit }; - if !success { - return Ok(false); - } Ok(Self::encodes_true(&return_data)) } @@ -568,27 +566,19 @@ impl SafeErc20 { owner: address(), spender, }; - let mut return_data = vec![0u8; 32]; - let success = unsafe { + let calldata = call.abi_encode(); + let result = unsafe { RawCall::new() - gas(u64::MAX) - .call(token, &call.encode()) - .map(|result| { - if let Some(data) = result { - let len = data.len().min(return_data.len()); - return_data[..len].copy_from_slice(&data[..len]); - } - true - }) - .unwrap_or(false) - }; - if !success { - return Err(Error::SafeErc20FailedOperation(SafeErc20FailedOperation { - token: *token, - })); - } - - Ok(U256::from_be_bytes(return_data.try_into().unwrap())) + .gas(u64::MAX) + .call(*token, &calldata) + }; + let data = result.map_err(|_| { + Error::SafeErc20FailedOperation(SafeErc20FailedOperation { token: *token }) + })?; + let mut buf = [0u8; 32]; + buf[..data.len().min(32)] + .copy_from_slice(&data[..data.len().min(32)]); + Ok(U256::from_be_bytes(buf)) } fn encodes_true(data: &[u8]) -> bool { From 49f9b003e5e894b252146282fdb03abadaa073bc Mon Sep 17 00:00:00 2001 From: Simon Smilga Date: Thu, 8 May 2025 16:37:06 +0000 Subject: [PATCH 14/16] refactor: Update SafeErc20 method calls to use data conversion for improved clarity and consistency --- contracts/src/token/erc20/utils/safe_erc20.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/contracts/src/token/erc20/utils/safe_erc20.rs b/contracts/src/token/erc20/utils/safe_erc20.rs index 13298ea58..e72800c62 100644 --- a/contracts/src/token/erc20/utils/safe_erc20.rs +++ b/contracts/src/token/erc20/utils/safe_erc20.rs @@ -460,7 +460,8 @@ impl ISafeErc20 for SafeErc20 { if Self::account_has_code(to) == 0 { self.safe_transfer(token, to, value) } else { - let call = IErc1363::transferAndCallCall { to, value, data.into() }; + let data_bytes = data.into(); + let call = IErc1363::transferAndCallCall { to, value, data: data_bytes }; if !Self::call_optional_return_bool(&token, &call)? { return Err(Error::SafeErc20FailedOperation(SafeErc20FailedOperation { token })); } @@ -497,8 +498,8 @@ impl ISafeErc20 for SafeErc20 { if Self::account_has_code(spender) == 0 { self.force_approve(token, spender, value) } else { -- let call = IErc1363::approveAndCallCall { spender, value, data }; -+ let call = IErc1363::approveAndCallCall { spender, value, data: data.into() }; + let data_bytes = data.into(); + let call = IErc1363::approveAndCallCall { spender, value, data: data_bytes }; if !Self::call_optional_return_bool(&token, &call)? { return Err(Error::SafeErc20FailedOperation(SafeErc20FailedOperation { token })); } @@ -511,7 +512,7 @@ impl SafeErc20 { #[inline] fn account_has_code(addr: Address) -> usize { // SAFETY: extcodesize is a pure query, no state mutation or re-entrancy - unsafe { stylus_sdk::evm::extcodesize(addr) as usize } + unsafe { stylus_sdk::prelude::extcodesize(addr) } } fn call_optional_return( @@ -584,9 +585,6 @@ impl SafeErc20 { } fn encodes_true(data: &[u8]) -> bool { - if data.is_empty() { - return true; - } data.len() == 32 && data[31] == 1 && data[..31].iter().all(|&b| b == 0) From aa2a53538dc5f0461b5d62648464f0573a288e9a Mon Sep 17 00:00:00 2001 From: Simon Smilga Date: Thu, 8 May 2025 22:38:59 +0600 Subject: [PATCH 15/16] feat: Update SafeERC20 to match latest Solidity version --- contracts/src/token/erc20/utils/safe_erc20.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/contracts/src/token/erc20/utils/safe_erc20.rs b/contracts/src/token/erc20/utils/safe_erc20.rs index e72800c62..06b604f4c 100644 --- a/contracts/src/token/erc20/utils/safe_erc20.rs +++ b/contracts/src/token/erc20/utils/safe_erc20.rs @@ -39,7 +39,6 @@ mod sol { /// /// * `token` - Address of the ERC-20 token. #[derive(Debug)] - #[allow(missing_docs)] error SafeErc20FailedOperation(address token); /// Indicates a failed [`ISafeErc20::safe_decrease_allowance`] request. @@ -48,7 +47,6 @@ mod sol { /// * `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, @@ -350,7 +348,6 @@ pub trait ISafeErc20 { ) -> Result<(), Self::Error>; } -#[public] impl ISafeErc20 for SafeErc20 { type Error = Error; @@ -480,7 +477,8 @@ impl ISafeErc20 for SafeErc20 { if Self::account_has_code(to) == 0 { self.safe_transfer_from(token, from, to, value) } else { - let call = IErc1363::transferFromAndCallCall { from, to, value, data: data.into() }; + let data_bytes = data.into(); + let call = IErc1363::transferFromAndCallCall { from, to, value, data: data_bytes }; if !Self::call_optional_return_bool(&token, &call)? { return Err(Error::SafeErc20FailedOperation(SafeErc20FailedOperation { token })); } @@ -510,9 +508,9 @@ impl ISafeErc20 for SafeErc20 { impl SafeErc20 { #[inline] - fn account_has_code(addr: Address) -> usize { - // SAFETY: extcodesize is a pure query, no state mutation or re-entrancy - unsafe { stylus_sdk::prelude::extcodesize(addr) } + fn account_has_code(addr: Address) -> bool { + // returns true if `addr` has contract code + addr.has_code() } fn call_optional_return( @@ -558,7 +556,7 @@ impl SafeErc20 { let return_data = match result { Ok(bytes) => bytes, - Err(_) => return Ok(false), // keep “soft” failure but make it explicit + Err(_) => return Ok(false), // keep "soft" failure but make it explicit }; Ok(Self::encodes_true(&return_data)) From bbcff79a8a5e4311e752a777acd9ed65af73be5b Mon Sep 17 00:00:00 2001 From: Simon Smilga Date: Thu, 19 Jun 2025 09:11:07 +0000 Subject: [PATCH 16/16] review implementation --- contracts/src/token/erc20/mod.rs | 12 -- contracts/src/token/erc20/utils/safe_erc20.rs | 126 +++++++++--------- examples/safe-erc20/tests/abi/mod.rs | 51 +------ 3 files changed, 67 insertions(+), 122 deletions(-) diff --git a/contracts/src/token/erc20/mod.rs b/contracts/src/token/erc20/mod.rs index 871889c60..964911d2f 100644 --- a/contracts/src/token/erc20/mod.rs +++ b/contracts/src/token/erc20/mod.rs @@ -4,15 +4,6 @@ //! revert instead of returning `false` on failure. This behavior is //! nonetheless conventional and does not conflict with the expectations of //! [`Erc20`] applications. -//! -//! # SafeERC20 -//! -//! The `utils::safe_erc20` module provides 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 alloc::{vec, vec::Vec}; use alloy_primitives::{Address, FixedBytes, U256}; @@ -33,8 +24,6 @@ pub mod extensions; pub mod interface; pub mod utils; -pub use utils::safe_erc20::{ISafeErc20, SafeErc20}; - pub use sol::*; #[cfg_attr(coverage_nightly, coverage(off))] mod sol { @@ -278,7 +267,6 @@ pub trait IErc20 { ) -> Result; } -#[public] impl IErc20 for Erc20 { type Error = Error; diff --git a/contracts/src/token/erc20/utils/safe_erc20.rs b/contracts/src/token/erc20/utils/safe_erc20.rs index 06b604f4c..ac96cb476 100644 --- a/contracts/src/token/erc20/utils/safe_erc20.rs +++ b/contracts/src/token/erc20/utils/safe_erc20.rs @@ -22,6 +22,7 @@ use stylus_sdk::{ prelude::*, types::AddressVM, }; +use stylus_sdk::abi::Bytes; use crate::{ token::erc20, @@ -163,10 +164,6 @@ pub trait ISafeErc20 { /// /// * `&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. - /// - /// # Returns /// /// * `Ok(true)` if the transfer was successful /// * `Ok(false)` if the transfer failed @@ -296,7 +293,7 @@ pub trait ISafeErc20 { token: Address, to: Address, value: U256, - data: Vec, + data: Bytes, ) -> Result<(), Self::Error>; /// Performs an ERC1363 transferFromAndCall, with a fallback to the simple ERC20 transferFrom if the target @@ -321,7 +318,7 @@ pub trait ISafeErc20 { from: Address, to: Address, value: U256, - data: Vec, + data: Bytes, ) -> Result<(), Self::Error>; /// Performs an ERC1363 approveAndCall, with a fallback to the simple ERC20 approve if the target has no @@ -344,10 +341,11 @@ pub trait ISafeErc20 { token: Address, spender: Address, value: U256, - data: Vec, + data: Bytes, ) -> Result<(), Self::Error>; } +#[public] impl ISafeErc20 for SafeErc20 { type Error = Error; @@ -438,7 +436,7 @@ impl ISafeErc20 for SafeErc20 { return Ok(()); } - // If it failed, fallback to zero-reset strategy + // If that fails, reset the allowance to zero, then retry the approval. let reset_call = IErc20::approveCall { spender, value: U256::from(0), @@ -454,15 +452,11 @@ impl ISafeErc20 for SafeErc20 { value: U256, data: Vec, ) -> Result<(), Self::Error> { - if Self::account_has_code(to) == 0 { + if !to.has_code() { self.safe_transfer(token, to, value) } else { - let data_bytes = data.into(); - let call = IErc1363::transferAndCallCall { to, value, data: data_bytes }; - if !Self::call_optional_return_bool(&token, &call)? { - return Err(Error::SafeErc20FailedOperation(SafeErc20FailedOperation { token })); - } - Ok(()) + let call = IErc1363::transferAndCallCall { to, value, data }; + Self::call_optional_return(&token, &call) } } @@ -474,15 +468,11 @@ impl ISafeErc20 for SafeErc20 { value: U256, data: Vec, ) -> Result<(), Self::Error> { - if Self::account_has_code(to) == 0 { + if !to.has_code() { self.safe_transfer_from(token, from, to, value) } else { - let data_bytes = data.into(); - let call = IErc1363::transferFromAndCallCall { from, to, value, data: data_bytes }; - if !Self::call_optional_return_bool(&token, &call)? { - return Err(Error::SafeErc20FailedOperation(SafeErc20FailedOperation { token })); - } - Ok(()) + let call = IErc1363::transferFromAndCallCall { from, to, value, data}; + Self::call_optional_return(&token, &call) } } @@ -493,60 +483,56 @@ impl ISafeErc20 for SafeErc20 { value: U256, data: Vec, ) -> Result<(), Self::Error> { - if Self::account_has_code(spender) == 0 { + if !spender.has_code() { self.force_approve(token, spender, value) } else { - let data_bytes = data.into(); - let call = IErc1363::approveAndCallCall { spender, value, data: data_bytes }; - if !Self::call_optional_return_bool(&token, &call)? { - return Err(Error::SafeErc20FailedOperation(SafeErc20FailedOperation { token })); - } - Ok(()) + let call = IErc1363::approveAndCallCall { spender, value, data }; + Self::call_optional_return(&token, &call) } } } impl SafeErc20 { - #[inline] - fn account_has_code(addr: Address) -> bool { - // returns true if `addr` has contract code - addr.has_code() - } - + /// 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 + /// + /// * [`Error::SafeErc20FailedOperation`] - If the `token` address is not a + /// contract, the contract fails to execute the call or the call returns + /// value that is not `true`. fn call_optional_return( - token: &Address, + token: Address, call: &impl SolCall, ) -> Result<(), Error> { + if !Address::has_code(&token) { + return Err(SafeErc20FailedOperation { token }.into()); + } - let calldata = call.abi_encode(); - let result = unsafe { - RawCall::new() - .gas(u64::MAX) - .call(*token, &calldata) - }; - - let return_data = match result { - Ok(bytes) => bytes, - Err(_) => { - return Err(Error::SafeErc20FailedOperation( - SafeErc20FailedOperation { token: *token }, - )); + unsafe { + match RawCall::new() + .limit_return_data(0, BOOL_TYPE_SIZE) + .flush_storage_cache() + .call(token, &call.abi_encode()) + { + Ok(data) if data.is_empty() || Self::encodes_true(&data) => { + Ok(()) + } + _ => Err(SafeErc20FailedOperation { token }.into()), } - }; - - // Treat no-data (all zeros) as success; only fail if there's non-zero junk that isn't `true` - if return_data.iter().any(|&b| b != 0) && !Self::encodes_true(&return_data) { - return Err(Error::SafeErc20FailedOperation(SafeErc20FailedOperation { - token: *token, - })); } - Ok(()) } fn call_optional_return_bool( token: &Address, call: &impl SolCall, - ) -> Result { + ) -> bool { let calldata = call.abi_encode(); let result = unsafe { RawCall::new() @@ -561,7 +547,16 @@ impl SafeErc20 { Ok(Self::encodes_true(&return_data)) } - + /// + /// * `token` - Address of the ERC-20 token contract. + /// * `spender` - Account that will spend the tokens. + /// + /// # Errors + /// + /// * [`Error::SafeErc20FailedOperation`] - If the `token` address is not a + /// contract. + /// * [`Error::SafeErc20FailedOperation`] - If the contract fails to read + /// `spender`'s allowance. fn allowance(token: &Address, spender: Address) -> Result { let call = IErc20::allowanceCall { owner: address(), @@ -581,11 +576,16 @@ impl SafeErc20 { .copy_from_slice(&data[..data.len().min(32)]); Ok(U256::from_be_bytes(buf)) } - + + /// 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.len() == 32 - && data[31] == 1 - && data[..31].iter().all(|&b| b == 0) + data.split_last().is_some_and(|(last, rest)| { + *last == 1 && rest.iter().all(|&byte| byte == 0) + }) } } diff --git a/examples/safe-erc20/tests/abi/mod.rs b/examples/safe-erc20/tests/abi/mod.rs index 41cd5363b..0ad1c4998 100644 --- a/examples/safe-erc20/tests/abi/mod.rs +++ b/examples/safe-erc20/tests/abi/mod.rs @@ -9,7 +9,9 @@ sol!( 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; - + function transferAndCallRelaxed(address token, address to, uint256 value, bytes data) external; + function transferFromAndCallRelaxed(address token, address from, address to, uint256 value, bytes data) external; + function approveAndCallRelaxed(address token, address spender, uint256 value, bytes data) external; error SafeErc20FailedOperation(address token); error SafeErc20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease); } @@ -31,49 +33,4 @@ impl SafeErc20 { pub fn new(address: Address, wallet: &Wallet) -> Self { Self } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Erc20; - -impl SafeErc20 { - pub fn safeTransfer(&self, token: Address, to: Address, value: U256) -> Result<()> { - Ok(()) - } - - pub fn safeTransferFrom(&self, token: Address, from: Address, to: Address, value: U256) -> Result<()> { - Ok(()) - } - - pub fn trySafeTransfer(&self, token: Address, to: Address, value: U256) -> Result { - Ok(true) - } - - pub fn trySafeTransferFrom(&self, token: Address, from: Address, to: Address, value: U256) -> Result { - Ok(true) - } - - pub fn safeIncreaseAllowance(&self, token: Address, spender: Address, value: U256) -> Result<()> { - Ok(()) - } - - pub fn safeDecreaseAllowance(&self, token: Address, spender: Address, requestedDecrease: U256) -> Result<()> { - Ok(()) - } - - pub fn forceApprove(&self, token: Address, spender: Address, value: U256) -> Result<()> { - Ok(()) - } - - pub fn transferAndCallRelaxed(&self, token: Address, to: Address, value: U256, data: Vec) -> Result<()> { - Ok(()) - } - - pub fn transferFromAndCallRelaxed(&self, token: Address, from: Address, to: Address, value: U256, data: Vec) -> Result<()> { - Ok(()) - } - - pub fn approveAndCallRelaxed(&self, token: Address, to: Address, value: U256, data: Vec) -> Result<()> { - Ok(()) - } -} +} \ No newline at end of file