diff --git a/CHANGELOG.md b/CHANGELOG.md index a60154ade..ae8f7e07b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added - -- +- `Erc20Wrapper` "Token Wrapping contract". #498 ### Changed @@ -33,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Implement `AddAssignChecked` for `StorageUint`. #474 - `Erc20FlashMint` extension. #407 + ### Changed - Keccak constants `PERMIT_TYPEHASH` in `Erc20Permit`, and `TYPE_HASH` in `Erc712` are now statically computed. #478 diff --git a/Cargo.lock b/Cargo.lock index d8f171cc1..99dc8b3a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1741,6 +1741,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "erc20-wrapper" +version = "0.2.0-alpha.3" +dependencies = [ + "alloy", + "alloy-primitives", + "e2e", + "eyre", + "openzeppelin-stylus", + "stylus-sdk", + "tokio", +] + [[package]] name = "erc4626-example" version = "0.2.0-alpha.3" diff --git a/Cargo.toml b/Cargo.toml index bf6d7cdc4..67fed433e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "examples/erc20", "examples/erc20-permit", "examples/erc20-flash-mint", + "examples/erc20-wrapper", "examples/erc721", "examples/erc721-consecutive", "examples/erc721-metadata", @@ -35,6 +36,7 @@ default-members = [ "examples/erc20", "examples/erc20-permit", "examples/erc20-flash-mint", + "examples/erc20-wrapper", "examples/erc721", "examples/erc721-consecutive", "examples/erc721-metadata", diff --git a/contracts/src/token/erc20/extensions/mod.rs b/contracts/src/token/erc20/extensions/mod.rs index 083f024af..b72b76fca 100644 --- a/contracts/src/token/erc20/extensions/mod.rs +++ b/contracts/src/token/erc20/extensions/mod.rs @@ -5,6 +5,7 @@ pub mod erc4626; pub mod flash_mint; pub mod metadata; pub mod permit; +pub mod wrapper; pub use burnable::IErc20Burnable; pub use capped::Capped; @@ -12,3 +13,4 @@ pub use erc4626::{Erc4626, IErc4626}; pub use flash_mint::{Erc20FlashMint, IErc3156FlashLender}; pub use metadata::{Erc20Metadata, IErc20Metadata}; pub use permit::Erc20Permit; +pub use wrapper::{Erc20Wrapper, IErc20Wrapper}; diff --git a/contracts/src/token/erc20/extensions/wrapper.rs b/contracts/src/token/erc20/extensions/wrapper.rs new file mode 100644 index 000000000..9531facb9 --- /dev/null +++ b/contracts/src/token/erc20/extensions/wrapper.rs @@ -0,0 +1,255 @@ +//! Extension of the ERC-20 token contract to support token wrapping. +//! +//! Users can deposit and withdraw "underlying tokens" and receive a matching +//! number of "wrapped tokens". This is useful in conjunction with other +//! modules. +//! +//! WARNING: Any mechanism in which the underlying token changes the {balanceOf} +//! of an account without an explicit transfer may desynchronize this contract's +//! supply and its underlying balance. Please exercise caution when wrapping +//! tokens that may undercollateralize the wrapper (i.e. wrapper's total supply +//! is higher than its underlying balance). See {_recover} for recovering value +//! accrued to the wrapper. + +use alloy_primitives::{Address, U256}; +use alloy_sol_macro::sol; +use stylus_sdk::{ + call::Call, + contract, msg, + prelude::storage, + storage::{StorageAddress, TopLevelStorage}, + stylus_proc::SolidityError, +}; + +use crate::token::erc20::{ + self, + utils::{safe_erc20, IErc20 as IErc20Solidity, ISafeErc20, SafeErc20}, + Erc20, +}; + +sol! { + /// Indicates that he address is not a valid ERC-20 token. + /// + /// * `address` - Address of the invalid underling ERC-20 token. + #[derive(Debug)] + #[allow(missing_docs)] + error ERC20InvalidUnderlying(address token); + + /// Indicates that the address is not an Invalid Sender address. + /// + /// * `sender` - Address is an invalid sender. + #[derive(Debug)] + #[allow(missing_docs)] + error ERC20InvalidSender(address sender); + + /// Indicates that The address is not a valid Invalid Asset. + /// + /// * `asset` - Address of the invalid address of the token. + #[derive(Debug)] + #[allow(missing_docs)] + error InvalidAsset(address asset); + + /// Indicates that the address is not an invalid receiver addresss. + /// + /// * `receiver` - Address of the invalid receiver. + #[derive(Debug)] + #[allow(missing_docs)] + error ERC20InvalidReceiver(address receiver); + +} + +/// An [`Erc20Wrapper`] error. +#[derive(SolidityError, Debug)] +pub enum Error { + /// Error type from [`SafeErc20`] contract [`safe_erc20::Error`]. + SafeErc20(safe_erc20::Error), + + /// The Sender Address is not valid. + InvalidSender(ERC20InvalidSender), + + /// The Reciver Address is not valid. + InvalidReceiver(ERC20InvalidReceiver), + + /// The underlying token couldn't be wrapped. + InvalidUnderlying(ERC20InvalidUnderlying), + + /// The address is not a valid ERC-20 token. + InvalidAsset(InvalidAsset), + + /// Error type from [`Erc20`] contract [`erc20::Error`]. + Erc20(erc20::Error), +} +/// State of an [`Erc20Wrapper`] token. +#[storage] +pub struct Erc20Wrapper { + /// Token Address of the underline token + #[allow(clippy::used_underscore_binding)] + pub(crate) underlying_address: StorageAddress, + + /// [`SafeErc20`] contract + safe_erc20: SafeErc20, +} + +/// ERC-20 Wrapper Standard Interface +pub trait IErc20Wrapper { + /// The error type associated to the` trait implementation. + type Error: Into>; + + /// Returns the address of the underlying token that is been wrapped. + fn underlying(&self) -> Address; + + /// Allow a user to deposit underlying tokens and mint the corresponding + /// number of wrapped token + /// + /// Arguments: + /// + /// * `&mut self` - Write access to the contract's state. + /// * `account` - The account to deposit tokens to. + /// * `value` - The amount of tokens to deposit. + /// + /// # Errors + /// + /// * If the sender address is `contract:address()` or invalid, + /// [`Error::InvalidSender`] is returned. + /// * If the receiver address is `contract:address()` or invalid, + /// [`Error::InvalidReceiver`] is returned. + fn deposit_for( + &mut self, + account: Address, + value: U256, + erc20: &mut Erc20, + ) -> Result; + + /// Allow a user to burn a number of wrapped tokens and withdraw the + /// corresponding number of underlying tokens. + /// + /// Arguments: + /// + /// * `&mut self` - Write access to the contract's state. + /// * `account` - The account to withdraw tokens to. + /// * `value` - The amount of tokens to withdraw. + /// * `erc20` - A mutable reference to the Erc20 contract. + /// + /// # Errors + /// + /// * If the receiver address is `contract:address()` or invalid, + /// [`Error::InvalidReceiver`] is returned. + fn withdraw_to( + &mut self, + account: Address, + value: U256, + erc20: &mut Erc20, + ) -> Result; +} + +/// NOTE: Implementation of [`TopLevelStorage`] to be able use `&mut self` when +/// calling other contracts and not `&mut (impl TopLevelStorage + +/// BorrowMut)`. Should be fixed in the future by the Stylus team. +unsafe impl TopLevelStorage for Erc20Wrapper {} + +impl IErc20Wrapper for Erc20Wrapper { + type Error = Error; + + fn underlying(&self) -> Address { + self.underlying_address.get() + } + + fn deposit_for( + &mut self, + account: Address, + value: U256, + erc20: &mut Erc20, + ) -> Result { + let sender = msg::sender(); + + if sender == contract::address() { + return Err(Error::InvalidReceiver(ERC20InvalidReceiver { + receiver: account, + })); + } + + if account == contract::address() { + return Err(Error::InvalidSender(ERC20InvalidSender { + sender: contract::address(), + })); + } + + self.safe_erc20.safe_transfer_from( + self.underlying(), + sender, + contract::address(), + value, + )?; + erc20._mint(account, value)?; + + Ok(true) + } + + fn withdraw_to( + &mut self, + account: Address, + value: U256, + erc20: &mut Erc20, + ) -> Result { + if account == contract::address() { + return Err(Error::InvalidReceiver(ERC20InvalidReceiver { + receiver: account, + })); + } + erc20._burn(account, value)?; + self.safe_erc20.safe_transfer(self.underlying(), account, value)?; + Ok(true) + } +} + +impl Erc20Wrapper { + /// Mints wrapped tokens to cover any underlying tokens that might have been + /// mistakenly transferred or acquired through rebasing mechanisms. + /// + /// This is an internal function that can be exposed with access control if + /// required. + /// + /// Arguments: + /// + /// * `&mut self` - Write access to the contract's state. + /// * `account` - The account to mint tokens to. + /// * `erc20` - A mutable reference to the Erc20 contract. + /// + /// # Errors + /// + /// If the external call for balance of fails , then the error + /// [`Error::InvalidAsset`] is returned. + pub fn _recover( + &mut self, + account: Address, + erc20: &mut Erc20, + ) -> Result { + let underline_token = IErc20Solidity::new(self.underlying()); + let value = underline_token + .balance_of(Call::new_in(self), contract::address()) + .map_err(|_| InvalidAsset { asset: contract::address() })?; + erc20._mint(account, value)?; + Ok(U256::from(value)) + } +} + +// TODO: Add missing tests once `motsu` supports calling external contracts. +#[cfg(all(test, feature = "std"))] +mod tests { + use alloy_primitives::address; + use stylus_sdk::prelude::storage; + + use super::{Erc20Wrapper, IErc20Wrapper}; + + #[storage] + struct Erc20WrapperTestExample { + wrapper: Erc20Wrapper, + } + + #[motsu::test] + fn underlying_works(contract: Erc20WrapperTestExample) { + let asset = address!("DeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF"); + contract.wrapper.underlying_address.set(asset); + assert_eq!(contract.wrapper.underlying(), asset); + } +} diff --git a/docs/modules/ROOT/pages/erc20-wrapper.adoc b/docs/modules/ROOT/pages/erc20-wrapper.adoc new file mode 100644 index 000000000..d872cd18d --- /dev/null +++ b/docs/modules/ROOT/pages/erc20-wrapper.adoc @@ -0,0 +1,57 @@ += ERC-20 Wrapper + +Extension of the ERC-20 token contract to support token wrapping. + +Users can deposit and withdraw "underlying tokens" and receive a matching number of "wrapped tokens". +This is useful in conjunction with other modules. + + +[[usage]] +== Usage + +In order to make your ERC20 `wrapped token`: + +[source,rust] +---- +use alloy_primitives::{Address, U256}; +use openzeppelin_stylus::token::erc20::{ + extensions::{Erc20Metadata, Erc20Wrapper, IERC20Wrapper}, + Erc20, +}; +use stylus_sdk::prelude::{entrypoint, public, storage}; + +#[entrypoint] +#[storage] +struct Erc20WrapperExample { + #[borrow] + pub erc20: Erc20, + #[borrow] + pub metadata: Erc20Metadata, + #[borrow] + pub wrapper: Erc20Wrapper, +} + +#[public] +#[inherit(Erc20, Erc20Metadata)] +impl Erc20WrapperExample { + fn underlying(&self) -> Address { + self.wrapper.underlying() + } + + fn deposit_to( + &mut self, + account: Address, + value: U256, + ) -> Result> { + Ok(self.wrapper.deposit_to(account, value, &mut self.erc20)?) + } + + fn withdraw_to( + &mut self, + account: Address, + value: U256, + ) -> Result> { + Ok(self.wrapper.withdraw_to(account, value, &mut self.erc20)?) + } +} +---- diff --git a/docs/modules/ROOT/pages/erc20.adoc b/docs/modules/ROOT/pages/erc20.adoc index 20e130860..da2cd12fc 100644 --- a/docs/modules/ROOT/pages/erc20.adoc +++ b/docs/modules/ROOT/pages/erc20.adoc @@ -86,3 +86,5 @@ Additionally, there are multiple custom extensions, including: * xref:erc4626.adoc[ERC-4626]: tokenized vault that manages shares (represented as ERC-20) that are backed by assets (another ERC-20). * xref:erc20-flash-mint.adoc[ERC-20 Flash-Mint]: token level support for flash loans through the minting and burning of ephemeral tokens (standardized as https://eips.ethereum.org/EIPS/eip-3156[`EIP-3156`]). + + * xref:erc20-wrapper.adoc[ERC-20 Wrapper]: Extension of the ERC-20 token contract to support token wrapping . diff --git a/examples/erc20-wrapper/Cargo.toml b/examples/erc20-wrapper/Cargo.toml new file mode 100644 index 000000000..19cb2feca --- /dev/null +++ b/examples/erc20-wrapper/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "erc20-wrapper" +edition.workspace = true +license.workspace = true +repository.workspace = true +publish = false +version.workspace = true + +[dependencies] +openzeppelin-stylus.workspace = true +alloy-primitives.workspace = true +stylus-sdk.workspace = true + +[dev-dependencies] +alloy.workspace = true +eyre.workspace = true +tokio.workspace = true +e2e.workspace = true + +[features] +e2e = [] + +[lib] +crate-type = ["lib", "cdylib"] diff --git a/examples/erc20-wrapper/src/constructor.sol b/examples/erc20-wrapper/src/constructor.sol new file mode 100644 index 000000000..8f642b3da --- /dev/null +++ b/examples/erc20-wrapper/src/constructor.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +contract Erc20WrapperExample { + + // Erc20 Token Storage + mapping(address account => uint256) private _balances; + mapping(address account => mapping(address spender => uint256)) + private _allowances; + uint256 private _totalSupply; + + // Erc20 Metadata Storage + string private _name; + string private _symbol; + + // Erc20 Wrapper Storage + address private _underlyingToken; + + constructor(string memory name_, string memory symbol_, address underlyingToken_) { + _name = name_; + _symbol = symbol_; + _underlyingToken = underlyingToken_; + } +} diff --git a/examples/erc20-wrapper/src/lib.rs b/examples/erc20-wrapper/src/lib.rs new file mode 100644 index 000000000..10610ffae --- /dev/null +++ b/examples/erc20-wrapper/src/lib.rs @@ -0,0 +1,46 @@ +#![cfg_attr(not(test), no_main)] +extern crate alloc; + +use alloc::vec::Vec; + +use alloy_primitives::{Address, U256}; +use openzeppelin_stylus::token::erc20::{ + extensions::{Erc20Metadata, Erc20Wrapper, IErc20Wrapper}, + Erc20, +}; +use stylus_sdk::prelude::{entrypoint, public, storage}; + +#[entrypoint] +#[storage] +struct Erc20WrapperExample { + #[borrow] + pub erc20: Erc20, + #[borrow] + pub metadata: Erc20Metadata, + #[borrow] + pub wrapper: Erc20Wrapper, +} + +#[public] +#[inherit(Erc20, Erc20Metadata)] +impl Erc20WrapperExample { + fn underlying(&self) -> Address { + self.wrapper.underlying() + } + + fn deposit_for( + &mut self, + account: Address, + value: U256, + ) -> Result> { + Ok(self.wrapper.deposit_for(account, value, &mut self.erc20)?) + } + + fn withdraw_to( + &mut self, + account: Address, + value: U256, + ) -> Result> { + Ok(self.wrapper.withdraw_to(account, value, &mut self.erc20)?) + } +} diff --git a/examples/erc20-wrapper/tests/abi/mod.rs b/examples/erc20-wrapper/tests/abi/mod.rs new file mode 100644 index 000000000..f6967231d --- /dev/null +++ b/examples/erc20-wrapper/tests/abi/mod.rs @@ -0,0 +1,43 @@ +#![allow(dead_code)] +#![allow(clippy::too_many_arguments)] +use alloy::sol; + +sol!( + #[sol(rpc)] + contract Erc20Wrapper { + function name() external view returns (string name); + function symbol() external view returns (string symbol); + function totalSupply() external view returns (uint256 totalSupply); + function balanceOf(address account) external view returns (uint256 balance); + function transfer(address recipient, uint256 amount) external returns (bool); + function allowance(address owner, address spender) external view returns (uint256 allowance); + function approve(address spender, uint256 amount) external returns (bool); + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); + + #[derive(Debug)] + function decimals() external view returns (uint8 decimals); + #[derive(Debug)] + function underlying() external view returns (address underlying); + #[derive(Debug)] + function depositFor(address account, uint256 value) external returns (bool); + #[derive(Debug)] + function withdrawTo(address account, uint256 value) external returns (bool); + + error SafeErc20FailedOperation(address token); + + error ERC20InvalidUnderlying(address token); + + error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed); + error ERC20InvalidSender(address sender); + error ERC20InvalidReceiver(address receiver); + error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed); + error ERC20InvalidSpender(address spender); + error InvalidAsset(address asset); + + #[derive(Debug, PartialEq)] + event Transfer(address indexed from, address indexed to, uint256 value); + #[derive(Debug, PartialEq)] + event Approval(address indexed owner, address indexed spender, uint256 value); + + } +); diff --git a/examples/erc20-wrapper/tests/erc20wrapper.rs b/examples/erc20-wrapper/tests/erc20wrapper.rs new file mode 100644 index 000000000..9ba7ee8ca --- /dev/null +++ b/examples/erc20-wrapper/tests/erc20wrapper.rs @@ -0,0 +1,238 @@ +#![cfg(feature = "e2e")] + +use abi::Erc20Wrapper; +use alloy::{ + primitives::{uint, Address, U256}, + sol, +}; +use e2e::{ + receipt, send, watch, Account, EventExt, Panic, PanicCode, ReceiptExt, + Revert, +}; +use eyre::Result; + +use crate::Erc20WrapperExample::constructorCall; + +mod abi; +mod mock; + +use mock::{erc20, erc20::ERC20Mock}; + +sol!("src/constructor.sol"); + +const WRAPPED_TOKEN_NAME: &str = "WRAPPED Test Token"; +const WRAPPED_TOKEN_SYMBOL: &str = "WTTK"; + +fn ctr(asset_addr: Address) -> constructorCall { + Erc20WrapperExample::constructorCall { + underlyingToken_: asset_addr, + name_: WRAPPED_TOKEN_NAME.to_owned(), + symbol_: WRAPPED_TOKEN_SYMBOL.to_owned(), + } +} + +async fn deploy( + account: &Account, + initial_tokens: U256, +) -> Result<(Address, Address)> { + let asset_addr = erc20::deploy(&account.wallet).await?; + + let contract_addr = account + .as_deployer() + .with_constructor(ctr(asset_addr)) + .deploy() + .await? + .address()?; + + // Mint initial tokens to the vault + if initial_tokens > U256::ZERO { + let asset = ERC20Mock::new(asset_addr, &account.wallet); + _ = watch!(asset.mint(contract_addr, initial_tokens))?; + } + + Ok((contract_addr, asset_addr)) +} + +// ============================================================================ +// Integration Tests: ERC-20 Token + Metadata Extension + ERC-20 Wrapper +// ============================================================================ + +mod constructor { + + use super::*; + + #[e2e::test] + async fn success(alice: Account) -> Result<()> { + let asset_address = erc20::deploy(&alice.wallet).await?; + let contract_addr = alice + .as_deployer() + .with_constructor(ctr(asset_address)) + .deploy() + .await? + .address()?; + let contract = Erc20Wrapper::new(contract_addr, &alice.wallet); + let name = contract.name().call().await?.name; + assert_eq!(name, WRAPPED_TOKEN_NAME.to_owned()); + + let symbol = contract.symbol().call().await?.symbol; + assert_eq!(symbol, WRAPPED_TOKEN_SYMBOL.to_owned()); + + let decimals = contract.decimals().call().await?.decimals; + assert_eq!(decimals, 18); + + Ok(()) + } +} + +mod deposit_to { + + use super::*; + + #[e2e::test] + async fn executes_with_approval( + alice: Account, + bob: Account, + ) -> Result<()> { + let initial_supply = uint!(1000_U256); + let (contract_addr, asset_addr) = deploy(&alice, U256::ZERO).await?; + let alice_address = alice.address(); + let asset = ERC20Mock::new(asset_addr, &alice.wallet); + let contract = Erc20Wrapper::new(contract_addr, &alice.wallet); + + _ = watch!(asset.mint(alice_address, initial_supply))?; + _ = watch!(asset.approve(alice_address, initial_supply))?; + let receipt = + send!(contract.depositFor(alice_address, initial_supply))?; + println!("receipt: {:#?}", receipt); + Ok(()) + } + + #[e2e::test] + async fn reverts_when_invalid_asset(alice: Account) -> Result<()> { + let invalid_asset = alice.address(); + let contract_addr = alice + .as_deployer() + .with_constructor(ctr(invalid_asset)) + .deploy() + .await? + .address()?; + let contract = Erc20Wrapper::new(contract_addr, &alice.wallet); + let err = send!(contract.depositFor(invalid_asset, uint!(10_U256))) + .expect_err("should return `InvalidAsset`"); + // assert!( + // err.reverted_with(Erc20Wrapper::InvalidAsset { asset: + // invalid_asset }) ); + + Ok(()) + } + + #[e2e::test] + async fn reverts_for_invalid_sender(alice: Account) -> Result<()> { + let (contract_addr, _) = deploy(&alice, U256::ZERO).await?; + let contract = Erc20Wrapper::new(contract_addr, &alice.wallet); + let err = contract + .depositFor(contract_addr, uint!(1000_U256)) + .call() + .await + .expect_err("should return `InvalidSender`"); + assert!(err.reverted_with(Erc20Wrapper::ERC20InvalidSender { + sender: contract_addr + })); + Ok(()) + } + + #[e2e::test] + async fn reverts_minting_to_wrapper_contract(alice: Account) -> Result<()> { + let alice_addr: Address = alice.address(); + let (contract_addr, _) = deploy(&alice, U256::ZERO).await?; + let contract = Erc20Wrapper::new(contract_addr, &alice.wallet); + let err = contract + .depositFor(alice.address(), uint!(1000_U256)) + .call() + .await + .expect_err("should return `InvalidReceiver`"); + assert!(err.reverted_with(Erc20Wrapper::ERC20InvalidSender { + sender: alice_addr + })); + Ok(()) + } + + #[e2e::test] + async fn reverts_when_missing_approval(alice: Account) -> Result<()> { + let (contract_addr, asset_addr) = deploy(&alice, U256::ZERO).await?; + let alice_address = alice.address(); + let contract = Erc20Wrapper::new(contract_addr, &alice.wallet); + let err = contract + .depositFor(alice_address, uint!(1000_U256)) + .call() + .await + .expect_err("should return `SafeErc20FailedOperation`"); + assert!(err.reverted_with(Erc20Wrapper::SafeErc20FailedOperation { + token: asset_addr + })); + Ok(()) + } + + #[e2e::test] + async fn reverts_when_insuficient_balance(alice: Account) -> Result<()> { + let (contract_addr, asset_addr) = deploy(&alice, U256::ZERO).await?; + let alice_address = alice.address(); + let contract = Erc20Wrapper::new(contract_addr, &alice.wallet); + let err = contract + .depositFor(alice_address, uint!(1000_U256)) + .call() + .await + .expect_err("should return `ERC20InsufficientBalance`"); + assert!(err.reverted_with(Erc20Wrapper::ERC20InsufficientBalance { + sender: alice_address, + balance: uint!(0_U256), + needed: uint!(1000_U256) + })); + Ok(()) + } + + #[e2e::test] + async fn reflects_balance_after_deposit_for(alice: Account) -> Result<()> { + let (contract_addr, asset_addr) = deploy(&alice, U256::ZERO).await?; + let alice_address = alice.address(); + let contract = Erc20Wrapper::new(contract_addr, &alice.wallet); + Ok(()) + } +} + +mod withdraw_to { + use super::*; + + #[e2e::test] + async fn success(alice: Account) -> Result<()> { + let (contract_addr, _) = deploy(&alice, U256::ZERO).await?; + let contract = Erc20Wrapper::new(contract_addr, &alice.wallet); + Ok(()) + } + + #[e2e::test] + async fn reverts_for_invalid_sender(alice: Account) -> Result<()> { + let (contract_addr, _) = deploy(&alice, U256::ZERO).await?; + let contract = Erc20Wrapper::new(contract_addr, &alice.wallet); + let err = contract + .withdrawTo(contract_addr, uint!(1000_U256)) + .call() + .await + .expect_err("should return `InvalidReciver`"); + assert!(err.reverted_with(Erc20Wrapper::ERC20InvalidReceiver { + receiver: contract_addr + })); + Ok(()) + } + + #[e2e::test] + async fn reflects_balance_after_withdraw_to( + alice: Account, + bob: Account, + ) -> Result<()> { + let (contract_addr, asset_addr) = deploy(&alice, U256::ZERO).await?; + let alice_address = alice.address(); + let contract = Erc20Wrapper::new(contract_addr, &alice.wallet); + Ok(()) + } +} diff --git a/examples/erc20-wrapper/tests/mock/erc20.rs b/examples/erc20-wrapper/tests/mock/erc20.rs new file mode 100644 index 000000000..68514daea --- /dev/null +++ b/examples/erc20-wrapper/tests/mock/erc20.rs @@ -0,0 +1,48 @@ +#![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="608060405234801562000010575f80fd5b506040518060400160405280600981526020017f45524332304d6f636b00000000000000000000000000000000000000000000008152506040518060400160405280600381526020017f4d544b000000000000000000000000000000000000000000000000000000000081525081600390816200008e91906200030d565b508060049081620000a091906200030d565b505050620003f1565b5f81519050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b5f60028204905060018216806200012557607f821691505b6020821081036200013b576200013a620000e0565b5b50919050565b5f819050815f5260205f209050919050565b5f6020601f8301049050919050565b5f82821b905092915050565b5f600883026200019f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8262000162565b620001ab868362000162565b95508019841693508086168417925050509392505050565b5f819050919050565b5f819050919050565b5f620001f5620001ef620001e984620001c3565b620001cc565b620001c3565b9050919050565b5f819050919050565b6200021083620001d5565b620002286200021f82620001fc565b8484546200016e565b825550505050565b5f90565b6200023e62000230565b6200024b81848462000205565b505050565b5b818110156200027257620002665f8262000234565b60018101905062000251565b5050565b601f821115620002c1576200028b8162000141565b620002968462000153565b81016020851015620002a6578190505b620002be620002b58562000153565b83018262000250565b50505b505050565b5f82821c905092915050565b5f620002e35f1984600802620002c6565b1980831691505092915050565b5f620002fd8383620002d2565b9150826002028217905092915050565b6200031882620000a9565b67ffffffffffffffff811115620003345762000333620000b3565b5b6200034082546200010d565b6200034d82828562000276565b5f60209050601f83116001811462000383575f84156200036e578287015190505b6200037a8582620002f0565b865550620003e9565b601f198416620003938662000141565b5f5b82811015620003bc5784890151825560018201915060208501945060208101905062000395565b86831015620003dc5784890151620003d8601f891682620002d2565b8355505b6001600288020188555050505b505050505050565b610f4580620003ff5f395ff3fe608060405234801561000f575f80fd5b50600436106100a7575f3560e01c806340c10f191161006f57806340c10f191461016557806370a08231146101815780638483acfe146101b157806395d89b41146101cd578063a9059cbb146101eb578063dd62ed3e1461021b576100a7565b806306fdde03146100ab578063095ea7b3146100c957806318160ddd146100f957806323b872dd14610117578063313ce56714610147575b5f80fd5b6100b361024b565b6040516100c09190610bbe565b60405180910390f35b6100e360048036038101906100de9190610c6f565b6102db565b6040516100f09190610cc7565b60405180910390f35b6101016102ee565b60405161010e9190610cef565b60405180910390f35b610131600480360381019061012c9190610d08565b6102f7565b60405161013e9190610cc7565b60405180910390f35b61014f61030c565b60405161015c9190610d73565b60405180910390f35b61017f600480360381019061017a9190610c6f565b610314565b005b61019b60048036038101906101969190610d8c565b610322565b6040516101a89190610cef565b60405180910390f35b6101cb60048036038101906101c69190610d08565b610333565b005b6101d5610343565b6040516101e29190610bbe565b60405180910390f35b61020560048036038101906102009190610c6f565b6103d3565b6040516102129190610cc7565b60405180910390f35b61023560048036038101906102309190610db7565b6103e6565b6040516102429190610cef565b60405180910390f35b60606003805461025a90610e22565b80601f016020809104026020016040519081016040528092919081815260200182805461028690610e22565b80156102d15780601f106102a8576101008083540402835291602001916102d1565b820191905f5260205f20905b8154815290600101906020018083116102b457829003601f168201915b5050505050905090565b5f6102e683836103f9565b905092915050565b5f600254905090565b5f61030384848461041b565b90509392505050565b5f6012905090565b61031e8282610449565b5050565b5f61032c826104c8565b9050919050565b61033e83838361050d565b505050565b60606004805461035290610e22565b80601f016020809104026020016040519081016040528092919081815260200182805461037e90610e22565b80156103c95780601f106103a0576101008083540402835291602001916103c9565b820191905f5260205f20905b8154815290600101906020018083116103ac57829003601f168201915b5050505050905090565b5f6103de838361051f565b905092915050565b5f6103f18383610541565b905092915050565b5f806104036105c3565b905061041081858561050d565b600191505092915050565b5f806104256105c3565b90506104328582856105ca565b61043d85858561065c565b60019150509392505050565b5f73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036104b9575f6040517fec442f050000000000000000000000000000000000000000000000000000000081526004016104b09190610e61565b60405180910390fd5b6104c45f838361074c565b5050565b5f805f8373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f20549050919050565b61051a8383836001610965565b505050565b5f806105296105c3565b905061053681858561065c565b600191505092915050565b5f60015f8473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f2054905092915050565b5f33905090565b5f6105d584846103e6565b90507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81146106565781811015610647578281836040517ffb8f41b200000000000000000000000000000000000000000000000000000000815260040161063e93929190610e7a565b60405180910390fd5b61065584848484035f610965565b5b50505050565b5f73ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16036106cc575f6040517f96c6fd1e0000000000000000000000000000000000000000000000000000000081526004016106c39190610e61565b60405180910390fd5b5f73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff160361073c575f6040517fec442f050000000000000000000000000000000000000000000000000000000081526004016107339190610e61565b60405180910390fd5b61074783838361074c565b505050565b5f73ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff160361079c578060025f8282546107909190610edc565b9250508190555061086a565b5f805f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f2054905081811015610825578381836040517fe450d38c00000000000000000000000000000000000000000000000000000000815260040161081c93929190610e7a565b60405180910390fd5b8181035f808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f2081905550505b5f73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036108b1578060025f82825403925050819055506108fb565b805f808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f82825401925050819055505b8173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040516109589190610cef565b60405180910390a3505050565b5f73ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff16036109d5575f6040517fe602df050000000000000000000000000000000000000000000000000000000081526004016109cc9190610e61565b60405180910390fd5b5f73ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1603610a45575f6040517f94280d62000000000000000000000000000000000000000000000000000000008152600401610a3c9190610e61565b60405180910390fd5b8160015f8673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f20819055508015610b2e578273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92584604051610b259190610cef565b60405180910390a35b50505050565b5f81519050919050565b5f82825260208201905092915050565b5f5b83811015610b6b578082015181840152602081019050610b50565b5f8484015250505050565b5f601f19601f8301169050919050565b5f610b9082610b34565b610b9a8185610b3e565b9350610baa818560208601610b4e565b610bb381610b76565b840191505092915050565b5f6020820190508181035f830152610bd68184610b86565b905092915050565b5f80fd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f610c0b82610be2565b9050919050565b610c1b81610c01565b8114610c25575f80fd5b50565b5f81359050610c3681610c12565b92915050565b5f819050919050565b610c4e81610c3c565b8114610c58575f80fd5b50565b5f81359050610c6981610c45565b92915050565b5f8060408385031215610c8557610c84610bde565b5b5f610c9285828601610c28565b9250506020610ca385828601610c5b565b9150509250929050565b5f8115159050919050565b610cc181610cad565b82525050565b5f602082019050610cda5f830184610cb8565b92915050565b610ce981610c3c565b82525050565b5f602082019050610d025f830184610ce0565b92915050565b5f805f60608486031215610d1f57610d1e610bde565b5b5f610d2c86828701610c28565b9350506020610d3d86828701610c28565b9250506040610d4e86828701610c5b565b9150509250925092565b5f60ff82169050919050565b610d6d81610d58565b82525050565b5f602082019050610d865f830184610d64565b92915050565b5f60208284031215610da157610da0610bde565b5b5f610dae84828501610c28565b91505092915050565b5f8060408385031215610dcd57610dcc610bde565b5b5f610dda85828601610c28565b9250506020610deb85828601610c28565b9150509250929050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b5f6002820490506001821680610e3957607f821691505b602082108103610e4c57610e4b610df5565b5b50919050565b610e5b81610c01565b82525050565b5f602082019050610e745f830184610e52565b92915050565b5f606082019050610e8d5f830186610e52565b610e9a6020830185610ce0565b610ea76040830184610ce0565b949350505050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f610ee682610c3c565b9150610ef183610c3c565b9250828201905080821115610f0957610f08610eaf565b5b9291505056fea2646970667358221220383e898342e74543d1bfb6186eff00b4ae7a39d4ecde6190742c5e9f2a7a2e9364736f6c63430008150033")] + // SPDX-License-Identifier: MIT + contract ERC20Mock is ERC20 { + constructor() ERC20("ERC20Mock", "MTK") {} + + function approve(address spender, uint256 value) public override returns (bool) { + return super.approve(spender, value); + } + + function regular_approve(address owner, address spender, uint256 amount) public { + super._approve(owner, spender, amount); + } + + 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); + } + + function transferFrom(address from, address to, uint256 value) public override returns (bool) { + return super.transferFrom(from, to, value); + } + + function allowance(address owner, address spender) public view override returns (uint256) { + return super.allowance(owner, spender); + } + } +} + +pub async fn deploy(wallet: &Wallet) -> eyre::Result
{ + // Deploy the contract. + let contract = ERC20Mock::deploy(wallet).await?; + Ok(*contract.address()) +} diff --git a/examples/erc20-wrapper/tests/mock/mod.rs b/examples/erc20-wrapper/tests/mock/mod.rs new file mode 100644 index 000000000..8f3777f6b --- /dev/null +++ b/examples/erc20-wrapper/tests/mock/mod.rs @@ -0,0 +1 @@ +pub mod erc20;