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 27 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
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;
131 changes: 131 additions & 0 deletions contracts/src/token/erc20/utils/safe_erc20.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
//! Wrappers around ERC-20 operations that throw on failure.

use alloy_primitives::{Address, U256};
use alloy_sol_types::sol;
use stylus_proc::{public, sol_interface, sol_storage, SolidityError};
use stylus_sdk::{call::Call, 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_interface! {
/// Interface of the ERC-20 standard as defined in the ERC.
interface IERC20 {
/// Moves a `value` amount of tokens from the caller's account to `to`.
///
/// Returns a boolean value indicating whether the operation succeeded.
///
/// Emits a {Transfer} event.
function transfer(address to, uint256 amount) external returns (bool);

/// Moves a `value` amount of tokens from `from` to `to` using the
/// allowance mechanism. `value` is then deducted from the caller's
/// allowance.
///
/// Returns a boolean value indicating whether the operation succeeded.
///
/// Emits a {Transfer} event.
function transferFrom(address from, address to, uint256 amount) external returns (bool);
}
}

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> {
let erc20 = IERC20::new(token);
let call = Call::new_in(self);

match erc20.transfer(call, to, value) {
Ok(data) => {
if data && !Address::has_code(&token) {
return Err(Error::SafeErc20FailedOperation(
SafeErc20FailedOperation { token },
));
}
}
Err(_) => {
return Err(Error::SafeErc20FailedOperation(
SafeErc20FailedOperation { token },
))
}
}

Ok(())
}

/// 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> {
let erc20 = IERC20::new(token);
let call = Call::new_in(self);

match erc20.transfer_from(call, from, to, value) {
Ok(data) => {
if data && !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 {
}
29 changes: 29 additions & 0 deletions examples/safe-erc20/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#![cfg_attr(not(test), no_main, no_std)]
extern crate alloc;

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

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

#[public]
#[inherit(SafeErc20)]
impl SafeErc20Example {
// Add token minting feature.
pub fn safe_transfer(
&mut self,
token: Address,
to: Address,
value: U256,
) -> Result<(), Error> {
self.safe_erc20.safe_transfer(token, to, value)?;
Ok(())
}
}
13 changes: 13 additions & 0 deletions examples/safe-erc20/tests/abi/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#![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;

error SafeErc20FailedOperation(address token);
error SafeErc20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease);
}
);
40 changes: 40 additions & 0 deletions examples/safe-erc20/tests/mock/erc20.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#![allow(dead_code)]
#![cfg(feature = "e2e")]
use alloy::{primitives::Address, sol};
use e2e::Wallet;

sol! {
#[allow(missing_docs)]
// Built with Remix IDE; solc v0.8.21+commit.d9974bed
#[sol(rpc, bytecode="608060405234801562000010575f80fd5b506040518060400160405280600781526020017f4d79546f6b656e000000000000000000000000000000000000000000000000008152506040518060400160405280600381526020017f4d544b000000000000000000000000000000000000000000000000000000000081525081600390816200008e91906200030d565b508060049081620000a091906200030d565b505050620003f1565b5f81519050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b5f60028204905060018216806200012557607f821691505b6020821081036200013b576200013a620000e0565b5b50919050565b5f819050815f5260205f209050919050565b5f6020601f8301049050919050565b5f82821b905092915050565b5f600883026200019f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8262000162565b620001ab868362000162565b95508019841693508086168417925050509392505050565b5f819050919050565b5f819050919050565b5f620001f5620001ef620001e984620001c3565b620001cc565b620001c3565b9050919050565b5f819050919050565b6200021083620001d5565b620002286200021f82620001fc565b8484546200016e565b825550505050565b5f90565b6200023e62000230565b6200024b81848462000205565b505050565b5b818110156200027257620002665f8262000234565b60018101905062000251565b5050565b601f821115620002c1576200028b8162000141565b620002968462000153565b81016020851015620002a6578190505b620002be620002b58562000153565b83018262000250565b50505b505050565b5f82821c905092915050565b5f620002e35f1984600802620002c6565b1980831691505092915050565b5f620002fd8383620002d2565b9150826002028217905092915050565b6200031882620000a9565b67ffffffffffffffff811115620003345762000333620000b3565b5b6200034082546200010d565b6200034d82828562000276565b5f60209050601f83116001811462000383575f84156200036e578287015190505b6200037a8582620002f0565b865550620003e9565b601f198416620003938662000141565b5f5b82811015620003bc5784890151825560018201915060208501945060208101905062000395565b86831015620003dc5784890151620003d8601f891682620002d2565b8355505b6001600288020188555050505b505050505050565b610efb80620003ff5f395ff3fe608060405234801561000f575f80fd5b506004361061009c575f3560e01c806340c10f191161006457806340c10f191461015a57806370a082311461017657806395d89b41146101a6578063a9059cbb146101c4578063dd62ed3e146101f45761009c565b806306fdde03146100a0578063095ea7b3146100be57806318160ddd146100ee57806323b872dd1461010c578063313ce5671461013c575b5f80fd5b6100a8610224565b6040516100b59190610b74565b60405180910390f35b6100d860048036038101906100d39190610c25565b6102b4565b6040516100e59190610c7d565b60405180910390f35b6100f66102c7565b6040516101039190610ca5565b60405180910390f35b61012660048036038101906101219190610cbe565b6102d0565b6040516101339190610c7d565b60405180910390f35b6101446102e5565b6040516101519190610d29565b60405180910390f35b610174600480360381019061016f9190610c25565b6102ed565b005b610190600480360381019061018b9190610d42565b6102fb565b60405161019d9190610ca5565b60405180910390f35b6101ae61030c565b6040516101bb9190610b74565b60405180910390f35b6101de60048036038101906101d99190610c25565b61039c565b6040516101eb9190610c7d565b60405180910390f35b61020e60048036038101906102099190610d6d565b6103af565b60405161021b9190610ca5565b60405180910390f35b60606003805461023390610dd8565b80601f016020809104026020016040519081016040528092919081815260200182805461025f90610dd8565b80156102aa5780601f10610281576101008083540402835291602001916102aa565b820191905f5260205f20905b81548152906001019060200180831161028d57829003601f168201915b5050505050905090565b5f6102bf8383610431565b905092915050565b5f600254905090565b5f6102dc848484610453565b90509392505050565b5f6012905090565b6102f78282610481565b5050565b5f61030582610500565b9050919050565b60606004805461031b90610dd8565b80601f016020809104026020016040519081016040528092919081815260200182805461034790610dd8565b80156103925780601f1061036957610100808354040283529160200191610392565b820191905f5260205f20905b81548152906001019060200180831161037557829003601f168201915b5050505050905090565b5f6103a78383610545565b905092915050565b5f60015f8473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f2054905092915050565b5f8061043b610567565b905061044881858561056e565b600191505092915050565b5f8061045d610567565b905061046a858285610580565b610475858585610612565b60019150509392505050565b5f73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036104f1575f6040517fec442f050000000000000000000000000000000000000000000000000000000081526004016104e89190610e17565b60405180910390fd5b6104fc5f8383610702565b5050565b5f805f8373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f20549050919050565b5f8061054f610567565b905061055c818585610612565b600191505092915050565b5f33905090565b61057b838383600161091b565b505050565b5f61058b84846103af565b90507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff811461060c57818110156105fd578281836040517ffb8f41b20000000000000000000000000000000000000000000000000000000081526004016105f493929190610e30565b60405180910390fd5b61060b84848484035f61091b565b5b50505050565b5f73ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1603610682575f6040517f96c6fd1e0000000000000000000000000000000000000000000000000000000081526004016106799190610e17565b60405180910390fd5b5f73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036106f2575f6040517fec442f050000000000000000000000000000000000000000000000000000000081526004016106e99190610e17565b60405180910390fd5b6106fd838383610702565b505050565b5f73ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1603610752578060025f8282546107469190610e92565b92505081905550610820565b5f805f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f20549050818110156107db578381836040517fe450d38c0000000000000000000000000000000000000000000000000000000081526004016107d293929190610e30565b60405180910390fd5b8181035f808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f2081905550505b5f73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603610867578060025f82825403925050819055506108b1565b805f808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f82825401925050819055505b8173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8360405161090e9190610ca5565b60405180910390a3505050565b5f73ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff160361098b575f6040517fe602df050000000000000000000000000000000000000000000000000000000081526004016109829190610e17565b60405180910390fd5b5f73ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16036109fb575f6040517f94280d620000000000000000000000000000000000000000000000000000000081526004016109f29190610e17565b60405180910390fd5b8160015f8673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f20819055508015610ae4578273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92584604051610adb9190610ca5565b60405180910390a35b50505050565b5f81519050919050565b5f82825260208201905092915050565b5f5b83811015610b21578082015181840152602081019050610b06565b5f8484015250505050565b5f601f19601f8301169050919050565b5f610b4682610aea565b610b508185610af4565b9350610b60818560208601610b04565b610b6981610b2c565b840191505092915050565b5f6020820190508181035f830152610b8c8184610b3c565b905092915050565b5f80fd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f610bc182610b98565b9050919050565b610bd181610bb7565b8114610bdb575f80fd5b50565b5f81359050610bec81610bc8565b92915050565b5f819050919050565b610c0481610bf2565b8114610c0e575f80fd5b50565b5f81359050610c1f81610bfb565b92915050565b5f8060408385031215610c3b57610c3a610b94565b5b5f610c4885828601610bde565b9250506020610c5985828601610c11565b9150509250929050565b5f8115159050919050565b610c7781610c63565b82525050565b5f602082019050610c905f830184610c6e565b92915050565b610c9f81610bf2565b82525050565b5f602082019050610cb85f830184610c96565b92915050565b5f805f60608486031215610cd557610cd4610b94565b5b5f610ce286828701610bde565b9350506020610cf386828701610bde565b9250506040610d0486828701610c11565b9150509250925092565b5f60ff82169050919050565b610d2381610d0e565b82525050565b5f602082019050610d3c5f830184610d1a565b92915050565b5f60208284031215610d5757610d56610b94565b5b5f610d6484828501610bde565b91505092915050565b5f8060408385031215610d8357610d82610b94565b5b5f610d9085828601610bde565b9250506020610da185828601610bde565b9150509250929050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b5f6002820490506001821680610def57607f821691505b602082108103610e0257610e01610dab565b5b50919050565b610e1181610bb7565b82525050565b5f602082019050610e2a5f830184610e08565b92915050565b5f606082019050610e435f830186610e08565b610e506020830185610c96565b610e5d6040830184610c96565b949350505050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f610e9c82610bf2565b9150610ea783610bf2565b9250828201905080821115610ebf57610ebe610e65565b5b9291505056fea26469706673582212204b0d5b8c644aa4b312a9bee1615964e800240731dcc271c19c1615f2940a1ed564736f6c63430008150033")]
// SPDX-License-Identifier: MIT
contract ERC20Mock is ERC20 {
constructor() ERC20("MyToken", "MTK") {}

function approve(address spender, uint256 value) public override returns (bool) {
return super.approve(spender, value);
}

function balanceOf(address account) public override view returns (uint256) {
return super.balanceOf(account);
}

function mint(address account, uint256 value) public {
super._mint(account, value);
}

function transfer(address to, uint256 amount) public override returns (bool) {
return super.transfer(to, amount);
0xNeshi marked this conversation as resolved.
Show resolved Hide resolved
}

function transferFrom(address from, address to, uint256 value) public override returns (bool) {
return super.transferFrom(from, to, value);
}
}
}

pub async fn deploy(wallet: &Wallet) -> eyre::Result<Address> {
// Deploy the contract.
let contract = ERC20Mock::deploy(wallet).await?;
Ok(*contract.address())
}
1 change: 1 addition & 0 deletions examples/safe-erc20/tests/mock/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod erc20;
Loading