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 45 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
45 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
bf1915a
Merge branch 'main' into safe-erc20
0xNeshi Sep 30, 2024
2b79af1
Implement safe_increase_allowance
0xNeshi Sep 30, 2024
306ec74
Refactor allowance-related fns
0xNeshi Sep 30, 2024
b88ee72
refactor
0xNeshi Sep 30, 2024
19f3448
IMplement safe_decrease_allowance
0xNeshi Sep 30, 2024
a7dd96d
Refactor arg encoding
0xNeshi Sep 30, 2024
bc7dc15
Fail with SafeErc20FailedDecreaseAllowance on failing to decrease all…
0xNeshi Sep 30, 2024
d3bb6ec
Implement return_false tests
0xNeshi Sep 30, 2024
8dd894e
Check whether return is true in call_optional_return
0xNeshi Sep 30, 2024
8739ec6
Use RawCall to limit the amount of returned data
0xNeshi Sep 30, 2024
5db49ad
Implement tests for USDT approval behavior
0xNeshi Sep 30, 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
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;
203 changes: 203 additions & 0 deletions contracts/src/token/erc20/utils/safe_erc20.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
//! 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::SolidityError;
use stylus_sdk::{
call::RawCall, contract::address, function_selector,
storage::TopLevelStorage, types::AddressVM,
};

use crate::token::{erc20, erc20::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),
}

/// 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 trait SafeErc20 {
/// The error type associated to this Safe ERC-20 trait implementation.
type Error: Into<alloc::vec::Vec<u8>>;

/// Transfer `value` amount of `token` from the calling contract to `to`. If
/// `token` returns no value, non-reverting calls are assumed to be
/// successful.
fn safe_transfer(
&self,
to: Address,
value: U256,
) -> Result<(), Self::Error>;

/// Transfer `value` amount of `token` from `from` to `to`, spending the
/// approval given by `from` to the calling contract. If `token` returns
/// no value, non-reverting calls are assumed to be successful.
fn safe_transfer_from(
&self,
from: Address,
to: Address,
value: U256,
) -> Result<(), Self::Error>;
}

impl SafeErc20 for Erc20 {
type Error = Error;

fn safe_transfer(&self, 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(calldata)
}

fn safe_transfer_from(
&self,
from: Address,
to: Address,
value: U256,
) -> Result<(), Self::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(calldata)
}
}

/// 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 Erc20 {}

impl Erc20 {
/// 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, data: Vec<u8>) -> Result<(), Error> {
match RawCall::new()
0xNeshi marked this conversation as resolved.
Show resolved Hide resolved
.limit_return_data(0, 32)
.call(todo!("get address of token"), data.as_slice())
{
Ok(data) => {
if data.is_empty() && !Address::has_code(&address()) {
return Err(Error::SafeErc20FailedOperation(
SafeErc20FailedOperation { token: address() },
));
}
}
Err(_) => {
return Err(Error::SafeErc20FailedOperation(
SafeErc20FailedOperation { token: address() },
))
}
}
Ok(())
}
}

#[cfg(all(test, feature = "std"))]
mod tests {
use alloy_primitives::{address, uint, Address, U256};
use stylus_sdk::msg;

use super::SafeErc20;
use crate::token::erc20::{Erc20, IErc20};

#[motsu::test]
fn safe_transfer(contract: Erc20) {
let sender = msg::sender();
let alice = address!("A11CEacF9aa32246d767FCCD72e02d6bCbcC375d");
let one = uint!(1_U256);

// Initialize state for the test case:
// Msg sender's & Alice's balance as `one`.
contract
._update(Address::ZERO, sender, one)
.expect("should mint tokens");
contract
._update(Address::ZERO, alice, one)
.expect("should mint tokens");

// Store initial balance & supply.
let initial_sender_balance = contract.balance_of(sender);
let initial_alice_balance = contract.balance_of(alice);
let initial_supply = contract.total_supply();

// Transfer action should work.
let result = contract.safe_transfer(alice, one);
assert!(result.is_ok());

// Check updated balance & supply.
assert_eq!(initial_sender_balance - one, contract.balance_of(sender));
assert_eq!(initial_alice_balance + one, contract.balance_of(alice));
assert_eq!(initial_supply, contract.total_supply());
}

#[motsu::test]
fn transfers_from(contract: Erc20) {
let alice = address!("A11CEacF9aa32246d767FCCD72e02d6bCbcC375d");
let bob = address!("B0B0cB49ec2e96DF5F5fFB081acaE66A2cBBc2e2");
let sender = msg::sender();

// Alice approves `msg::sender`.
let one = uint!(1_U256);
contract._allowances.setter(alice).setter(sender).set(one);

// Mint some tokens for Alice.
let two = uint!(2_U256);
contract._update(Address::ZERO, alice, two).unwrap();
assert_eq!(two, contract.balance_of(alice));

let result = contract.safe_transfer_from(alice, bob, one);
assert!(result.is_ok());

assert_eq!(one, contract.balance_of(alice));
assert_eq!(one, contract.balance_of(bob));
assert_eq!(U256::ZERO, contract.allowance(alice, sender));
}
}