Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: SafeErc20 utility #289

Draft
wants to merge 34 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
98e0660
Add SafeErc20 + safe_transfer
0xNeshi Sep 17, 2024
025dc51
Add transfer param types when calc. selector
0xNeshi Sep 17, 2024
3db1217
Merge branch 'main' into safe-erc20
0xNeshi Sep 17, 2024
2287251
Add gas_left + fix has code check
0xNeshi Sep 18, 2024
eb779f1
Use RawCall with no value instead of Call
0xNeshi Sep 18, 2024
3182cc6
Remove unused imports
0xNeshi Sep 18, 2024
a3a4673
Add safe_transfer happy path test
0xNeshi Sep 19, 2024
05b235f
Fix test balance assertions
0xNeshi Sep 19, 2024
9e449f4
Mock erc20 contract address in raw call
0xNeshi Sep 19, 2024
025564a
Use function_selector to get the appropriate value
0xNeshi Sep 19, 2024
019ef51
Use Call instead of RawCall
0xNeshi Sep 19, 2024
33ef805
Use Call:new instead of new_in
0xNeshi Sep 19, 2024
ef2b0c8
Revert to RawCall
0xNeshi Sep 19, 2024
8041c47
Add safe_transfer_from
0xNeshi Sep 19, 2024
e4ba4e5
Format
0xNeshi Sep 19, 2024
1a47709
Update logic (removes safe_transfer_from)
0xNeshi Sep 19, 2024
e978a6b
Initialize tests
0xNeshi Sep 19, 2024
6e10c60
Fix token address in SafeErc20FailedOperation error
0xNeshi Sep 19, 2024
970e3cf
Use receipt instead of send
0xNeshi Sep 19, 2024
22092da
SafeErc20Example.safe_transfer_token->safe_transfer
0xNeshi Sep 19, 2024
1ed10b1
Simplify ERC20Mock
0xNeshi Sep 19, 2024
429b42d
Revert e2e-tests.sh
0xNeshi Sep 20, 2024
1348579
Add additional failure tests
0xNeshi Sep 20, 2024
3542d32
Use inherited ERC20 functions + remove redundant src/ERC20Mock.sol
0xNeshi Sep 23, 2024
b6ccccf
Rename reject-on-error test
0xNeshi Sep 23, 2024
c42aa9d
Add safe_transfer_from_+ tests + fix eoa rejection tests (fix alice a…
0xNeshi Sep 23, 2024
89240e6
Merge branch 'main' into safe-erc20
0xNeshi Sep 24, 2024
282e9f9
Fixed tests
0xNeshi Sep 24, 2024
23ca2af
Add stubs for the rest of ERC20-related safe-functions
0xNeshi Sep 24, 2024
53ab7f0
Add all required erc20 mocks
0xNeshi Sep 24, 2024
bcf1374
Simplify SafeErc20Example
0xNeshi Sep 24, 2024
29ea3b4
Extract has_no_code tests + Create the rest of has_no_code tests
0xNeshi Sep 25, 2024
423743b
Revert to using low level call
0xNeshi Sep 25, 2024
d47db5a
Implement forceApprove
0xNeshi Sep 25, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ members = [
"examples/basic/token",
"examples/basic/script",
"examples/ecdsa",
"examples/safe-erc20",
"benches",
]
default-members = [
Expand All @@ -30,6 +31,7 @@ default-members = [
"examples/erc721",
"examples/erc721-consecutive",
"examples/erc721-metadata",
"examples/safe-erc20",
"examples/merkle-proofs",
"examples/ownable",
"examples/access-control",
Expand Down
1 change: 1 addition & 0 deletions contracts/src/token/erc20/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use stylus_sdk::{
};

pub mod extensions;
pub mod utils;

sol! {
/// Emitted when `value` tokens are moved from one account (`from`) to
Expand Down
4 changes: 4 additions & 0 deletions contracts/src/token/erc20/utils/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
//! Utilities for the ERC-20 standard.
pub mod safe_erc20;

pub use safe_erc20::SafeErc20;
185 changes: 185 additions & 0 deletions contracts/src/token/erc20/utils/safe_erc20.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
//! Wrappers around ERC-20 operations that throw on failure.

use alloc::vec::Vec;
use alloy_primitives::{Address, U256};
use alloy_sol_types::{
sol,
sol_data::{Address as SOLAddress, Uint},
SolType,
};
use stylus_proc::{public, sol_interface, sol_storage, SolidityError};

Check warning on line 10 in contracts/src/token/erc20/utils/safe_erc20.rs

View workflow job for this annotation

GitHub Actions / stable / clippy

[clippy] reported by reviewdog 🐶 warning: unused import: `sol_interface` --> contracts/src/token/erc20/utils/safe_erc20.rs:10:27 | 10 | use stylus_proc::{public, sol_interface, sol_storage, SolidityError}; | ^^^^^^^^^^^^^ | = note: `#[warn(unused_imports)]` on by default Raw Output: contracts/src/token/erc20/utils/safe_erc20.rs:10:27:w:warning: unused import: `sol_interface` --> contracts/src/token/erc20/utils/safe_erc20.rs:10:27 | 10 | use stylus_proc::{public, sol_interface, sol_storage, SolidityError}; | ^^^^^^^^^^^^^ | = note: `#[warn(unused_imports)]` on by default __END__

Check warning on line 10 in contracts/src/token/erc20/utils/safe_erc20.rs

View workflow job for this annotation

GitHub Actions / beta / clippy

[clippy] reported by reviewdog 🐶 warning: unused import: `sol_interface` --> contracts/src/token/erc20/utils/safe_erc20.rs:10:27 | 10 | use stylus_proc::{public, sol_interface, sol_storage, SolidityError}; | ^^^^^^^^^^^^^ | = note: `#[warn(unused_imports)]` on by default Raw Output: contracts/src/token/erc20/utils/safe_erc20.rs:10:27:w:warning: unused import: `sol_interface` --> contracts/src/token/erc20/utils/safe_erc20.rs:10:27 | 10 | use stylus_proc::{public, sol_interface, sol_storage, SolidityError}; | ^^^^^^^^^^^^^ | = note: `#[warn(unused_imports)]` on by default __END__
use stylus_sdk::{
call::{call, Call},
function_selector,
storage::TopLevelStorage,
types::AddressVM,
};

use crate::token::erc20;

sol! {
/// An operation with an ERC-20 token failed.
#[derive(Debug)]
#[allow(missing_docs)]
error SafeErc20FailedOperation(address token);

/// Indicates a failed `decreaseAllowance` request.
#[derive(Debug)]
#[allow(missing_docs)]
error SafeErc20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease);
}

/// A SafeErc20 error
#[derive(SolidityError, Debug)]
pub enum Error {
/// Error type from [`Erc20`] contract [`erc20::Error`].
Erc20(erc20::Error),
/// An operation with an ERC-20 token failed.
SafeErc20FailedOperation(SafeErc20FailedOperation),
/// Indicates a failed `decreaseAllowance` request.
SafeErc20FailedDecreaseAllowance(SafeErc20FailedDecreaseAllowance),
}

sol_storage! {
/// 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 `using SafeERC20 for IERC20;` statement to
/// your contract, which allows you to call the safe operations as
/// `token.safeTransfer(...)`, etc.
pub struct SafeErc20 {}
}

/// NOTE: Implementation of [`TopLevelStorage`] to be able use `&mut self` when
/// calling other contracts and not `&mut (impl TopLevelStorage +
/// BorrowMut<Self>)`. Should be fixed in the future by the Stylus team.
unsafe impl TopLevelStorage for SafeErc20 {}

#[public]
impl SafeErc20 {
/// Transfer `value` amount of `token` from the calling contract to `to`. If
/// `token` returns no value, non-reverting calls are assumed to be
/// successful.
pub fn safe_transfer(
&mut self,
token: Address,
to: Address,
value: U256,
) -> Result<(), Error> {
type TransferType = (SOLAddress, Uint<256>);
let tx_data = (to, value);
let data = TransferType::abi_encode_params(&tx_data);
let hashed_function_selector =
function_selector!("transfer", Address, U256);
// Combine function selector and input data (use abi_packed way)
let calldata = [&hashed_function_selector[..4], &data].concat();

self.call_optional_return(token, calldata)
}

/// Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the
/// calling contract. If `token` returns no value, non-reverting calls are assumed to be successful.
pub fn safe_transfer_from(
&mut self,
token: Address,
from: Address,
to: Address,
value: U256,
) -> Result<(), Error> {
type TransferType = (SOLAddress, SOLAddress, Uint<256>);
let tx_data = (from, to, value);
let data = TransferType::abi_encode_params(&tx_data);
let hashed_function_selector =
function_selector!("transferFrom", Address, Address, U256);
// Combine function selector and input data (use abi_packed way)
let calldata = [&hashed_function_selector[..4], &data].concat();

self.call_optional_return(token, calldata)
}

/// Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value,
/// non-reverting calls are assumed to be successful.
pub fn safe_increase_allowance(
&mut self,
token: Address,

Check warning on line 105 in contracts/src/token/erc20/utils/safe_erc20.rs

View workflow job for this annotation

GitHub Actions / stable / clippy

[clippy] reported by reviewdog 🐶 warning: unused variable: `token` --> contracts/src/token/erc20/utils/safe_erc20.rs:105:9 | 105 | token: Address, | ^^^^^ help: if this is intentional, prefix it with an underscore: `_token` | = note: `#[warn(unused_variables)]` on by default Raw Output: contracts/src/token/erc20/utils/safe_erc20.rs:105:9:w:warning: unused variable: `token` --> contracts/src/token/erc20/utils/safe_erc20.rs:105:9 | 105 | token: Address, | ^^^^^ help: if this is intentional, prefix it with an underscore: `_token` | = note: `#[warn(unused_variables)]` on by default __END__

Check warning on line 105 in contracts/src/token/erc20/utils/safe_erc20.rs

View workflow job for this annotation

GitHub Actions / beta / clippy

[clippy] reported by reviewdog 🐶 warning: unused variable: `token` --> contracts/src/token/erc20/utils/safe_erc20.rs:105:9 | 105 | token: Address, | ^^^^^ help: if this is intentional, prefix it with an underscore: `_token` | = note: `#[warn(unused_variables)]` on by default Raw Output: contracts/src/token/erc20/utils/safe_erc20.rs:105:9:w:warning: unused variable: `token` --> contracts/src/token/erc20/utils/safe_erc20.rs:105:9 | 105 | token: Address, | ^^^^^ help: if this is intentional, prefix it with an underscore: `_token` | = note: `#[warn(unused_variables)]` on by default __END__
spender: Address,

Check warning on line 106 in contracts/src/token/erc20/utils/safe_erc20.rs

View workflow job for this annotation

GitHub Actions / stable / clippy

[clippy] reported by reviewdog 🐶 warning: unused variable: `spender` --> contracts/src/token/erc20/utils/safe_erc20.rs:106:9 | 106 | spender: Address, | ^^^^^^^ help: if this is intentional, prefix it with an underscore: `_spender` Raw Output: contracts/src/token/erc20/utils/safe_erc20.rs:106:9:w:warning: unused variable: `spender` --> contracts/src/token/erc20/utils/safe_erc20.rs:106:9 | 106 | spender: Address, | ^^^^^^^ help: if this is intentional, prefix it with an underscore: `_spender` __END__

Check warning on line 106 in contracts/src/token/erc20/utils/safe_erc20.rs

View workflow job for this annotation

GitHub Actions / beta / clippy

[clippy] reported by reviewdog 🐶 warning: unused variable: `spender` --> contracts/src/token/erc20/utils/safe_erc20.rs:106:9 | 106 | spender: Address, | ^^^^^^^ help: if this is intentional, prefix it with an underscore: `_spender` Raw Output: contracts/src/token/erc20/utils/safe_erc20.rs:106:9:w:warning: unused variable: `spender` --> contracts/src/token/erc20/utils/safe_erc20.rs:106:9 | 106 | spender: Address, | ^^^^^^^ help: if this is intentional, prefix it with an underscore: `_spender` __END__
value: U256,

Check warning on line 107 in contracts/src/token/erc20/utils/safe_erc20.rs

View workflow job for this annotation

GitHub Actions / stable / clippy

[clippy] reported by reviewdog 🐶 warning: unused variable: `value` --> contracts/src/token/erc20/utils/safe_erc20.rs:107:9 | 107 | value: U256, | ^^^^^ help: if this is intentional, prefix it with an underscore: `_value` Raw Output: contracts/src/token/erc20/utils/safe_erc20.rs:107:9:w:warning: unused variable: `value` --> contracts/src/token/erc20/utils/safe_erc20.rs:107:9 | 107 | value: U256, | ^^^^^ help: if this is intentional, prefix it with an underscore: `_value` __END__

Check warning on line 107 in contracts/src/token/erc20/utils/safe_erc20.rs

View workflow job for this annotation

GitHub Actions / beta / clippy

[clippy] reported by reviewdog 🐶 warning: unused variable: `value` --> contracts/src/token/erc20/utils/safe_erc20.rs:107:9 | 107 | value: U256, | ^^^^^ help: if this is intentional, prefix it with an underscore: `_value` Raw Output: contracts/src/token/erc20/utils/safe_erc20.rs:107:9:w:warning: unused variable: `value` --> contracts/src/token/erc20/utils/safe_erc20.rs:107:9 | 107 | value: U256, | ^^^^^ help: if this is intentional, prefix it with an underscore: `_value` __END__
) -> Result<(), Error> {
todo!()
}

/// Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no
/// value, non-reverting calls are assumed to be successful.
pub fn safe_decrease_allowance(
&mut self,
token: Address,

Check warning on line 116 in contracts/src/token/erc20/utils/safe_erc20.rs

View workflow job for this annotation

GitHub Actions / stable / clippy

[clippy] reported by reviewdog 🐶 warning: unused variable: `token` --> contracts/src/token/erc20/utils/safe_erc20.rs:116:9 | 116 | token: Address, | ^^^^^ help: if this is intentional, prefix it with an underscore: `_token` Raw Output: contracts/src/token/erc20/utils/safe_erc20.rs:116:9:w:warning: unused variable: `token` --> contracts/src/token/erc20/utils/safe_erc20.rs:116:9 | 116 | token: Address, | ^^^^^ help: if this is intentional, prefix it with an underscore: `_token` __END__

Check warning on line 116 in contracts/src/token/erc20/utils/safe_erc20.rs

View workflow job for this annotation

GitHub Actions / beta / clippy

[clippy] reported by reviewdog 🐶 warning: unused variable: `token` --> contracts/src/token/erc20/utils/safe_erc20.rs:116:9 | 116 | token: Address, | ^^^^^ help: if this is intentional, prefix it with an underscore: `_token` Raw Output: contracts/src/token/erc20/utils/safe_erc20.rs:116:9:w:warning: unused variable: `token` --> contracts/src/token/erc20/utils/safe_erc20.rs:116:9 | 116 | token: Address, | ^^^^^ help: if this is intentional, prefix it with an underscore: `_token` __END__
spender: Address,

Check warning on line 117 in contracts/src/token/erc20/utils/safe_erc20.rs

View workflow job for this annotation

GitHub Actions / stable / clippy

[clippy] reported by reviewdog 🐶 warning: unused variable: `spender` --> contracts/src/token/erc20/utils/safe_erc20.rs:117:9 | 117 | spender: Address, | ^^^^^^^ help: if this is intentional, prefix it with an underscore: `_spender` Raw Output: contracts/src/token/erc20/utils/safe_erc20.rs:117:9:w:warning: unused variable: `spender` --> contracts/src/token/erc20/utils/safe_erc20.rs:117:9 | 117 | spender: Address, | ^^^^^^^ help: if this is intentional, prefix it with an underscore: `_spender` __END__

Check warning on line 117 in contracts/src/token/erc20/utils/safe_erc20.rs

View workflow job for this annotation

GitHub Actions / beta / clippy

[clippy] reported by reviewdog 🐶 warning: unused variable: `spender` --> contracts/src/token/erc20/utils/safe_erc20.rs:117:9 | 117 | spender: Address, | ^^^^^^^ help: if this is intentional, prefix it with an underscore: `_spender` Raw Output: contracts/src/token/erc20/utils/safe_erc20.rs:117:9:w:warning: unused variable: `spender` --> contracts/src/token/erc20/utils/safe_erc20.rs:117:9 | 117 | spender: Address, | ^^^^^^^ help: if this is intentional, prefix it with an underscore: `_spender` __END__
requested_decrease: U256,

Check warning on line 118 in contracts/src/token/erc20/utils/safe_erc20.rs

View workflow job for this annotation

GitHub Actions / stable / clippy

[clippy] reported by reviewdog 🐶 warning: unused variable: `requested_decrease` --> contracts/src/token/erc20/utils/safe_erc20.rs:118:9 | 118 | requested_decrease: U256, | ^^^^^^^^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_requested_decrease` Raw Output: contracts/src/token/erc20/utils/safe_erc20.rs:118:9:w:warning: unused variable: `requested_decrease` --> contracts/src/token/erc20/utils/safe_erc20.rs:118:9 | 118 | requested_decrease: U256, | ^^^^^^^^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_requested_decrease` __END__

Check warning on line 118 in contracts/src/token/erc20/utils/safe_erc20.rs

View workflow job for this annotation

GitHub Actions / beta / clippy

[clippy] reported by reviewdog 🐶 warning: unused variable: `requested_decrease` --> contracts/src/token/erc20/utils/safe_erc20.rs:118:9 | 118 | requested_decrease: U256, | ^^^^^^^^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_requested_decrease` Raw Output: contracts/src/token/erc20/utils/safe_erc20.rs:118:9:w:warning: unused variable: `requested_decrease` --> contracts/src/token/erc20/utils/safe_erc20.rs:118:9 | 118 | requested_decrease: U256, | ^^^^^^^^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_requested_decrease` __END__
) -> Result<(), Error> {
todo!()
}

/// Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value,
/// non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval
/// to be set to zero before setting it to a non-zero value, such as USDT.
pub fn force_approve(
&mut self,
token: Address,
spender: Address,
value: U256,
) -> Result<(), Error> {
type TransferType = (SOLAddress, Uint<256>);
let tx_data = (spender, value);
let data = TransferType::abi_encode_params(&tx_data);
let hashed_function_selector =
function_selector!("approve", Address, U256);
// Combine function selector and input data (use abi_packed way)
let approve_calldata = [&hashed_function_selector[..4], &data].concat();

if self.call_optional_return(token, approve_calldata.clone()).is_err() {
let tx_data = (spender, U256::ZERO);
let data = TransferType::abi_encode_params(&tx_data);
self.call_optional_return(
token,
[&hashed_function_selector[..4], &data].concat(),
)?;
self.call_optional_return(token, approve_calldata)?;
}

Ok(())
}
}

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).
/// @param token The token targeted by the call.
/// @param data The call data (encoded using abi.encode or one of its
/// variants).
///
/// This is a variant of {_callOptionalReturnBool} that reverts if call
/// fails to meet the requirements.
fn call_optional_return(
&self,
token: Address,
data: Vec<u8>,
) -> Result<(), Error> {
match call(Call::new(), token, data.as_slice()) {
Ok(data) => {
if data.is_empty() && !Address::has_code(&token) {
return Err(Error::SafeErc20FailedOperation(
SafeErc20FailedOperation { token },
));
}
}
Err(_) => {
return Err(Error::SafeErc20FailedOperation(
SafeErc20FailedOperation { token },
))
}
}
Ok(())
}
}
26 changes: 26 additions & 0 deletions examples/safe-erc20/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[package]
name = "safe-erc20-example"
edition.workspace = true
license.workspace = true
repository.workspace = true
publish = false
version = "0.0.0"

[dependencies]
openzeppelin-stylus = { path = "../../contracts" }
alloy-primitives = { workspace = true, features = ["tiny-keccak"] }
stylus-sdk.workspace = true
stylus-proc.workspace = true
mini-alloc.workspace = true

[dev-dependencies]
alloy.workspace = true
eyre.workspace = true
tokio.workspace = true
e2e = { path = "../../lib/e2e" }

[features]
e2e = []

[lib]
crate-type = ["lib", "cdylib"]
5 changes: 5 additions & 0 deletions examples/safe-erc20/src/constructor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;

contract SafeErc20Example {
}
17 changes: 17 additions & 0 deletions examples/safe-erc20/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#![cfg_attr(not(test), no_main, no_std)]
extern crate alloc;

use openzeppelin_stylus::token::erc20::utils::safe_erc20::SafeErc20;
use stylus_sdk::prelude::{entrypoint, public, sol_storage};

sol_storage! {
#[entrypoint]
struct SafeErc20Example {
#[borrow]
SafeErc20 safe_erc20;
}
}

#[public]
#[inherit(SafeErc20)]
impl SafeErc20Example {}
16 changes: 16 additions & 0 deletions examples/safe-erc20/tests/abi/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#![allow(dead_code)]
use alloy::sol;

sol!(
#[sol(rpc)]
contract SafeErc20 {
function safeTransfer(address token, address to, uint256 value) external;
function safeTransferFrom(address token, address from, address to, uint256 value) external;
function safeIncreaseAllowance(address token, address spender, uint256 value) external;
function safeDecreaseAllowance(address token, address spender, uint256 requestedDecrease) external;
function forceApprove(address token, address spender, uint256 value) external;

error SafeErc20FailedOperation(address token);
error SafeErc20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease);
}
);
Loading
Loading