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

Merged
merged 87 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
87 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
8ed5249
Add approvals test with zero init allowance
0xNeshi Oct 2, 2024
c90bee0
Add approvals test with non-zero init allowance
0xNeshi Oct 2, 2024
03f1777
Nest transfer-related tests into separate module
0xNeshi Oct 2, 2024
3168ec2
Fix call_optional_return Ok logic
0xNeshi Oct 2, 2024
e5736ff
Add erc20_that_doesnt_return tests
0xNeshi Oct 2, 2024
05f3e1a
Give more descriptive test file names
0xNeshi Oct 2, 2024
a5e803c
Merge branch 'main' into safe-erc20
0xNeshi Oct 2, 2024
e3b2782
Revert changes to e2e-tests.sh
0xNeshi Oct 2, 2024
ef3dba3
Fix comments
0xNeshi Oct 2, 2024
86f352c
Format constructor.sol
0xNeshi Oct 2, 2024
7034914
Merge branch 'main' into safe-erc20
0xNeshi Oct 3, 2024
a4c21fb
Remove empty constructor.sol
0xNeshi Oct 3, 2024
34eda9f
Align Cargo.toml deps
0xNeshi Oct 3, 2024
fde3b18
Fix typos
0xNeshi Oct 3, 2024
06d8112
Format with nightly
0xNeshi Oct 3, 2024
08d5ac6
Fix docs link to Erc20
0xNeshi Oct 3, 2024
ee6beb4
Add underscore prefix to call_optional_return->_call_optional_return
0xNeshi Oct 8, 2024
e325745
Move contract docs to the top of file
0xNeshi Oct 9, 2024
839517b
Refactor er20_that_doesnt_return tests
0xNeshi Oct 15, 2024
f006d42
Refactor all tests
0xNeshi Oct 15, 2024
6aa5d0d
Merge branch 'main' into safe-erc20
bidzyyys Oct 29, 2024
b4f78b1
Run format
0xNeshi Oct 29, 2024
e8b19a2
Merge branch 'OpenZeppelin:main' into safe-erc20
0xNeshi Oct 30, 2024
3b2bf2b
Merge branch 'main' into safe-erc20
bidzyyys Nov 5, 2024
b40b097
fix: compilation error
bidzyyys Nov 6, 2024
2e9169d
ref: add ISafeErc20 trait
bidzyyys Nov 6, 2024
b8f04ec
ref: improve code style
bidzyyys Nov 6, 2024
6f3f69e
ref: abi encode based on solidity interface
bidzyyys Nov 6, 2024
56e8c77
Merge branch 'main' into safe-erc20
bidzyyys Nov 6, 2024
caf1abb
Apply comment update suggestions from code review
0xNeshi Nov 6, 2024
4986938
ref: optimize the code
bidzyyys Nov 6, 2024
a52c2c0
docs: add Rust docs
bidzyyys Nov 6, 2024
d28435b
docs: update docs
bidzyyys Nov 6, 2024
95f4d08
test: add unit tests
bidzyyys Nov 6, 2024
7aa0539
test: check events in E2E tests
bidzyyys Nov 6, 2024
925e01a
test: E2E tests for failed internal Erc20 operations
bidzyyys Nov 6, 2024
b48ea0e
test: E2E check for math overflow
bidzyyys Nov 7, 2024
b765521
docs: update CHANGELOG
bidzyyys Nov 7, 2024
893a5b6
fix: apply clippy comments
bidzyyys Nov 7, 2024
9a26dbf
grammar fixes
qalisander Nov 7, 2024
1fe32cb
add dots on comments
qalisander Nov 7, 2024
6c447e1
Merge branch 'main' into safe-erc20
bidzyyys Nov 7, 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.
bidzyyys marked this conversation as resolved.
Show resolved Hide resolved
#[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);
bidzyyys marked this conversation as resolved.
Show resolved Hide resolved
}

/// A SafeErc20 error
bidzyyys marked this conversation as resolved.
Show resolved Hide resolved
#[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
bidzyyys marked this conversation as resolved.
Show resolved Hide resolved
bidzyyys 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));
}
}