diff --git a/contracts/src/lib.rs b/contracts/src/lib.rs index 3a0750b35..26f91814a 100644 --- a/contracts/src/lib.rs +++ b/contracts/src/lib.rs @@ -83,9 +83,9 @@ impl IErc20 for MyContract { */ #![allow( - clippy::module_name_repetitions, - clippy::used_underscore_items, - deprecated + clippy::module_name_repetitions, + clippy::used_underscore_items, + deprecated )] #![cfg_attr(not(any(test, feature = "export-abi")), no_std, no_main)] #![cfg_attr(coverage_nightly, feature(coverage_attribute))] @@ -95,4 +95,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/mod.rs b/contracts/src/token/erc20/mod.rs index 368af8ea1..fe8cc0b7b 100644 --- a/contracts/src/token/erc20/mod.rs +++ b/contracts/src/token/erc20/mod.rs @@ -66,7 +66,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 +109,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 @@ -272,11 +272,6 @@ pub trait IErc20 { ) -> Result; } -#[public] -#[implements(IErc20)] -impl Erc20 {} - -#[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 078b82089..ee54bb856 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::utils::introspection::erc165::IErc165; @@ -36,7 +37,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. @@ -45,7 +45,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, @@ -81,6 +80,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); + } } } @@ -147,9 +153,48 @@ 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. + /// + /// * `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 /// @@ -222,11 +267,78 @@ pub trait ISafeErc20 { spender: Address, value: U256, ) -> Result<(), Self::Error>; -} -#[public] -#[implements(ISafeErc20)] -impl SafeErc20 {} + /// 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: Bytes, + ) -> 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: Bytes, + ) -> 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, + spender: Address, + value: U256, + data: Bytes, + ) -> Result<(), Self::Error>; +} #[public] impl ISafeErc20 for SafeErc20 { @@ -239,8 +351,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( @@ -251,8 +362,28 @@ impl ISafeErc20 for SafeErc20 { value: U256, ) -> Result<(), Self::Error> { let call = IErc20::transferFromCall { from, to, value }; + 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) + } - Self::call_optional_return(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( @@ -261,10 +392,9 @@ 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"); + let old_allowance = Self::allowance(&token, spender)?; + let new_allowance = old_allowance.checked_add(value) + .ok_or_else(|| Error::SafeErc20FailedOperation(SafeErc20FailedOperation { token }))?; self.force_approve(token, spender, new_allowance) } @@ -274,22 +404,18 @@ 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( @@ -299,23 +425,70 @@ 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(token, &approve_call).is_ok() { + + // 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 that fails, reset the allowance to zero, then retry the approval. + 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( + &mut self, + token: Address, + to: Address, + value: U256, + data: Vec, + ) -> Result<(), Self::Error> { + if !to.has_code() { + self.safe_transfer(token, to, value) + } else { + let call = IErc1363::transferAndCallCall { to, value, data }; + Self::call_optional_return(&token, &call) + } + } + + fn transfer_from_and_call_relaxed( + &mut self, + token: Address, + from: Address, + to: Address, + value: U256, + data: Vec, + ) -> Result<(), Self::Error> { + if !to.has_code() { + self.safe_transfer_from(token, from, to, value) + } else { + let call = IErc1363::transferFromAndCallCall { from, to, value, data}; + Self::call_optional_return(&token, &call) + } + } + + fn approve_and_call_relaxed( + &mut self, + token: Address, + spender: Address, + value: U256, + data: Vec, + ) -> Result<(), Self::Error> { + if !spender.has_code() { + self.force_approve(token, spender, value) + } else { + let call = IErc1363::approveAndCallCall { spender, value, data }; + Self::call_optional_return(&token, &call) + } } } impl SafeErc20 { - /// Imitates a Stylus high-level call, relaxing the requirement on the + /// 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. /// @@ -351,10 +524,24 @@ impl SafeErc20 { } } - /// Returns the remaining number of ERC-20 tokens that `spender` - /// will be allowed to spend on behalf of an owner. - /// - /// # Arguments + fn call_optional_return_bool( + token: &Address, + call: &impl SolCall, + ) -> bool { + 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 Ok(false), // keep "soft" failure but make it explicit + }; + + Ok(Self::encodes_true(&return_data)) + } /// /// * `token` - Address of the ERC-20 token contract. /// * `spender` - Account that will spend the tokens. @@ -365,27 +552,26 @@ impl SafeErc20 { /// 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()); - } - - let call = IErc20::allowanceCall { owner: address(), spender }; + fn allowance(token: &Address, spender: Address) -> Result { + let call = IErc20::allowanceCall { + owner: address(), + spender, + }; + let calldata = call.abi_encode(); let result = 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, &calldata) }; - - Ok(U256::from_be_slice(&result)) + 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)) } - + /// Returns true if a slice of bytes is an ABI encoded `true` value. /// /// # Arguments diff --git a/examples/safe-erc20/src/lib.rs b/examples/safe-erc20/src/lib.rs index 891ddaf1c..0301a016e 100644 --- a/examples/safe-erc20/src/lib.rs +++ b/examples/safe-erc20/src/lib.rs @@ -1,75 +1,70 @@ #![cfg_attr(not(any(test, feature = "export-abi")), no_main)] extern crate alloc; -use openzeppelin_stylus::token::erc20::utils::safe_erc20::{ - self, ISafeErc20, SafeErc20, -}; -use stylus_sdk::{ - alloy_primitives::{Address, U256}, - prelude::*, -}; -#[entrypoint] -#[storage] -struct SafeErc20Example { +use alloy_primitives::{Address, U256}; +use openzeppelin_stylus::token::erc20::utils::safe_erc20::SafeErc20; +use stylus_sdk::prelude::*; + +#[derive(Clone)] +pub struct SafeErc20Example { safe_erc20: SafeErc20, } -#[public] -#[implements(ISafeErc20)] +#[inherit(SafeErc20)] impl SafeErc20Example {} -#[public] -impl ISafeErc20 for SafeErc20Example { - type Error = safe_erc20::Error; +#[external] +impl SafeErc20Example { + pub fn transfer_and_call( - fn safe_transfer( &mut self, token: Address, to: Address, value: U256, - ) -> Result<(), Self::Error> { - self.safe_erc20.safe_transfer(token, to, value) + data: Vec, + ) -> Result<(), Vec> { + self.transfer_and_call_relaxed(token, to, value, data) } - fn safe_transfer_from( + pub fn transfer_from_and_call( + &mut self, token: Address, from: Address, to: Address, value: U256, - ) -> Result<(), Self::Error> { - self.safe_erc20.safe_transfer_from(token, from, to, value) + data: Vec, + ) -> Result<(), Vec> { + self.transfer_from_and_call_relaxed(token, from, to, value, data) } - fn safe_increase_allowance( + pub fn approve_and_call( &mut self, token: Address, - spender: Address, + to: Address, value: U256, - ) -> Result<(), Self::Error> { - self.safe_erc20.safe_increase_allowance(token, spender, value) + data: Vec, + ) -> Result<(), Vec> { + self.approve_and_call_relaxed(token, to, value, data) } - fn safe_decrease_allowance( + pub fn try_transfer( &mut self, token: Address, - spender: Address, - requested_decrease: U256, - ) -> Result<(), Self::Error> { - self.safe_erc20.safe_decrease_allowance( - token, - spender, - requested_decrease, - ) + to: Address, + value: U256, + ) -> Result> { + self.try_safe_transfer(token, to, value) } - fn force_approve( + pub fn try_transfer_from( &mut self, token: Address, - spender: Address, + from: Address, + to: Address, value: U256, - ) -> Result<(), Self::Error> { - self.safe_erc20.force_approve(token, spender, value) + ) -> 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..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); } @@ -23,3 +25,12 @@ 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 + } +} \ No newline at end of file