diff --git a/Cargo.lock b/Cargo.lock index 953b3db0..a6294102 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1574,6 +1574,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "erc20-flashmint-example" +version = "0.2.0-alpha.1" +dependencies = [ + "alloy", + "alloy-primitives", + "e2e", + "eyre", + "openzeppelin-stylus", + "stylus-sdk", + "tokio", +] + [[package]] name = "erc20-permit-example" version = "0.2.0-alpha.1" diff --git a/Cargo.toml b/Cargo.toml index 46b03707..06c3acfb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "lib/e2e-proc", "examples/erc20", "examples/erc20-permit", + "examples/erc20-flashmint", "examples/erc721", "examples/erc721-consecutive", "examples/erc721-metadata", @@ -32,6 +33,7 @@ default-members = [ "lib/e2e-proc", "examples/erc20", "examples/erc20-permit", + "examples/erc20-flashmint", "examples/erc721", "examples/erc721-consecutive", "examples/erc721-metadata", diff --git a/contracts/src/token/erc20/extensions/flashmint.rs b/contracts/src/token/erc20/extensions/flashmint.rs new file mode 100644 index 00000000..1cac1828 --- /dev/null +++ b/contracts/src/token/erc20/extensions/flashmint.rs @@ -0,0 +1,326 @@ +//! Optional Flashloan extension of the ERC-20 standard. +//! using the IERC3156FlashBorrower interface to borrow tokens. + +use alloy_primitives::{b256, Address, Bytes, B256, U256}; +use alloy_sol_types::sol; +use stylus_sdk::{ + abi::Bytes as AbiBytes, call::Call, contract, msg, prelude::*, +}; + +use crate::token::erc20::{ + self, utils::borrower::IERC3156FlashBorrower, Erc20, IErc20, +}; + +sol! { + /// Indicate an error related to an unsupported loan token. + /// This occurs when the specified token cannot be used for loans. + #[derive(Debug)] + #[allow(missing_docs)] + error ERC3156UnsupportedToken(address token); + + /// Indicate an error related to the loan amount exceeds the maximum. + /// The requested amount is higher than the allowed loan for this token max_loan. + #[derive(Debug)] + #[allow(missing_docs)] + error ERC3156ExceededMaxLoan(uint256 max_loan); + + /// Indicate an error related to an invalid flash loan receiver. + /// The receiver does not implement the required `onFlashLoan` function. + #[derive(Debug)] + #[allow(missing_docs)] + error ERC3156InvalidReceiver(address receiver); + +} + +/// Extension of [`Erc20`] that allows token holders to destroy both +/// their own tokens and those that they have an allowance for, +/// in a way that can be recognized off-chain (via event analysis). +pub trait IERC3156FlashLender { + /// The error type associated to this ERC-20 Burnable trait implementation. + type Error: Into>; + + /// Returns the maximum amount of tokens that can be borrowed + /// from this contract in a flash loan. + /// + /// For tokens that are not supported, this function returns + /// `U256::MIN`. + /// + /// * `token` - The address of the ERC-20 token that will be loaned. + fn max_flash_loan(&self, token: Address) -> U256; + + /// Calculates the fee for a flash loan. + /// + /// The fee is a fixed percentage of the borrowed amount. + /// + /// If the token is not supported, the function returns an + /// `UnsupportedToken` error. + /// + /// * `token` - The address of the ERC-20 token that will be loaned. + /// * `amount` - The amount of tokens that will be loaned. + fn flash_fee( + &self, + token: Address, + amount: U256, + ) -> Result; + + /// Executes a flash loan. + /// + /// This function is part of the ERC-3156 (Flash Loans) standard. + /// + /// * `receiver` - The contract that will receive the flash loan. + /// * `token` - The ERC-20 token that will be loaned. + /// * `amount` - The amount of tokens that will be loaned. + /// * `data` - Arbitrary data that can be passed to the receiver contract. + /// + /// The function must return `true` if the flash loan was successful, + /// and revert otherwise. + fn flash_loan( + &mut self, + receiver: Address, + token: Address, + amount: U256, + data: AbiBytes, + ) -> Result; +} + +/// A Permit error. +#[derive(SolidityError, Debug)] +pub enum Error { + /// Indicate an error related to an unsupported loan token. + /// This occurs when the specified token cannot be used for loans. + UnsupportedToken(ERC3156UnsupportedToken), + + /// Indicate an error related to the loan amount exceeds the maximum. + /// The requested amount is higher than the allowed loan for this token + /// max_loan. + ExceededMaxLoan(ERC3156ExceededMaxLoan), + + /// Indicate an error related to an invalid flash loan receiver. + /// The receiver does not implement the required `onFlashLoan` function. + InvalidReceiver(ERC3156InvalidReceiver), + + /// Error type from [`Erc20`] contract [`erc20::Error`]. + Erc20(erc20::Error), +} + +sol_storage! { + pub struct Erc20Flashmint { + uint256 _flash_fee_amount; + address _flash_fee_receiver_address; + Erc20 erc20; + } +} + +unsafe impl TopLevelStorage for Erc20Flashmint {} + +const RETURN_VALUE: B256 = + b256!("439148f0bbc682ca079e46d6e2c2f0c1e3b820f1a291b069d8882abf8cf18dd9"); + +#[public] +impl IERC3156FlashLender for Erc20Flashmint { + type Error = Error; + + fn max_flash_loan(&self, token: Address) -> U256 { + self.erc20.total_supply() + // if token == contract::address() { + // return U256::MAX - self.erc20.total_supply(); + // } + // U256::MIN + } + + fn flash_fee( + &self, + token: Address, + amount: U256, + ) -> Result { + if token != contract::address() { + return Err(Error::UnsupportedToken(ERC3156UnsupportedToken { + token, + })); + } + Ok(self._flash_fee(token, amount)) + } + + fn flash_loan( + &mut self, + receiver: Address, + token: Address, + value: U256, + data: AbiBytes, + ) -> Result { + let max_loan = self.max_flash_loan(token); + if value > max_loan { + return Err(Error::ExceededMaxLoan(ERC3156ExceededMaxLoan { + max_loan, + })); + } + + let fee = self.flash_fee(token, value)?; + self.erc20._mint(receiver, value)?; + let loan_reciver = IERC3156FlashBorrower::new(receiver); + if Address::has_code(&loan_reciver) { + return Err(Error::InvalidReceiver(ERC3156InvalidReceiver { + receiver, + })); + } + let call = Call::new(); + let loan_return = loan_reciver.on_flash_loan( + call, + msg::sender(), + token, + value, + fee, + Bytes::from(data.0), + ); + if loan_return.is_err() { + return Err(Error::InvalidReceiver(ERC3156InvalidReceiver { + receiver, + })); + } + if loan_return.ok() != Some(RETURN_VALUE) { + return Err(Error::InvalidReceiver(ERC3156InvalidReceiver { + receiver, + })); + } + + let flash_fee_receiver = self._flash_fee_receiver(); + self.erc20._spend_allowance(receiver, msg::sender(), value + fee)?; + if fee.is_zero() || flash_fee_receiver.is_zero() { + self.erc20._burn(receiver, value + fee)?; + } else { + self.erc20._burn(receiver, value)?; + self.erc20._transfer(receiver, flash_fee_receiver, fee)?; + } + + Ok(true) + } +} + +impl Erc20Flashmint { + /// Calculates the fee for a flash loan. + /// + /// The fee is currently fixed at 0. + /// + /// * `token` - The ERC-20 token that will be loaned. + /// * `value` - The amount of tokens that will be loaned. + pub fn _flash_fee(&self, token: Address, value: U256) -> U256 { + let _ = token; + let _ = value; + if self._flash_fee_amount.is_zero() { + return U256::MIN; + } + self._flash_fee_amount.get() + } + + /// Returns the address of the receiver contract that will receive the flash + /// loan. The default implementation returns `Address::ZERO`. + pub fn _flash_fee_receiver(&self) -> Address { + if self._flash_fee_receiver_address.eq(&Address::ZERO) { + return self._flash_fee_receiver_address.get(); + } + Address::ZERO + } +} + +#[cfg(all(test, feature = "std"))] +mod tests { + use alloy_primitives::{address, uint, U256}; + use stylus_sdk::msg; + + use super::Erc20FlashMint; + use crate::token::erc20::{ + extensions::flashmint::IERC3156FlashLender, Erc20, IErc20, + }; + + #[motsu::test] + fn max_flash_loan_token_match(contract: Erc20FlashMint) { + let token = address!("dce82b5f92c98f27f116f70491a487effdb6a2a9"); + let max_flash_loan = contract.max_flash_loan(token); + assert_eq!(max_flash_loan, U256::MAX); + } + + #[motsu::test] + fn max_flash_loan_token_mismatch(contract: Erc20FlashMint) { + let token = address!("dce82b5f92c98f27f116f70491a487effdb6a2a6"); + let max_flash_loan = contract.max_flash_loan(token); + assert_eq!(max_flash_loan, U256::MIN); + } + + #[motsu::test] + fn burns_errors_when_insufficient_balance(contract: Erc20FlashMint) { + let zero = U256::ZERO; + let one = uint!(1_U256); + let sender = msg::sender(); + + assert_eq!(zero, contract.erc20.balance_of(sender)); + + // let result = contract.burn(one); + // assert!(matches!(result, Err(Error::InsufficientBalance(_)))); + } + + // #[motsu::test] + // fn burn_from(contract: Erc20) { + // let alice = address!("A11CEacF9aa32246d767FCCD72e02d6bCbcC375d"); + // 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)); + // assert_eq!(two, contract.total_supply()); + + // contract.burn_from(alice, one).unwrap(); + + // assert_eq!(one, contract.balance_of(alice)); + // assert_eq!(one, contract.total_supply()); + // assert_eq!(U256::ZERO, contract.allowance(alice, sender)); + // } + + // #[motsu::test] + // fn burns_from_errors_when_insufficient_balance(contract: Erc20) { + // let alice = address!("A11CEacF9aa32246d767FCCD72e02d6bCbcC375d"); + + // // Alice approves `msg::sender`. + // let zero = U256::ZERO; + // let one = uint!(1_U256); + + // contract._allowances.setter(alice).setter(msg::sender()).set(one); + // assert_eq!(zero, contract.balance_of(alice)); + + // let one = uint!(1_U256); + + // let result = contract.burn_from(alice, one); + // assert!(matches!(result, Err(Error::InsufficientBalance(_)))); + // } + + // #[motsu::test] + // fn burns_from_errors_when_invalid_approver(contract: Erc20) { + // let one = uint!(1_U256); + + // contract + // ._allowances + // .setter(Address::ZERO) + // .setter(msg::sender()) + // .set(one); + + // let result = contract.burn_from(Address::ZERO, one); + // assert!(matches!(result, Err(Error::InvalidApprover(_)))); + // } + + // #[motsu::test] + // fn burns_from_errors_when_insufficient_allowance(contract: Erc20) { + // let alice = address!("A11CEacF9aa32246d767FCCD72e02d6bCbcC375d"); + + // // Mint some tokens for Alice. + // let one = uint!(1_U256); + // contract._update(Address::ZERO, alice, one).unwrap(); + // assert_eq!(one, contract.balance_of(alice)); + + // let result = contract.burn_from(alice, one); + // assert!(matches!(result, Err(Error::InsufficientAllowance(_)))); + // } +} diff --git a/contracts/src/token/erc20/extensions/mod.rs b/contracts/src/token/erc20/extensions/mod.rs index beaa80ec..85abcd10 100644 --- a/contracts/src/token/erc20/extensions/mod.rs +++ b/contracts/src/token/erc20/extensions/mod.rs @@ -1,10 +1,12 @@ //! Common extensions to the ERC-20 standard. pub mod burnable; pub mod capped; +pub mod flashmint; pub mod metadata; pub mod permit; pub use burnable::IErc20Burnable; pub use capped::Capped; +pub use flashmint::{Erc20Flashmint, IERC3156FlashLender}; pub use metadata::{Erc20Metadata, IErc20Metadata}; pub use permit::Erc20Permit; diff --git a/contracts/src/token/erc20/utils/borrower.rs b/contracts/src/token/erc20/utils/borrower.rs new file mode 100644 index 00000000..43798425 --- /dev/null +++ b/contracts/src/token/erc20/utils/borrower.rs @@ -0,0 +1,31 @@ +#![allow(missing_docs)] +//! Module with an interface required for smart contract +//! in order to borrow ERC-3156 flashlaon. + +use stylus_sdk::stylus_proc::sol_interface; + +sol_interface! { + /// Interface that must be implemented by smart contracts + /// in order to borrow ERC-3156 flashloan . + interface IERC3156FlashBorrower { + /// Handles the receipt of a flash loan. + /// This function is called after the loan amount has been transferred to the borrower. + /// + /// To indicate successful handling of the flash loan, this function should return + /// the `keccak256` hash of "ERC3156FlashBorrower.onFlashLoan". + /// + /// * `initiator` - The address which initiated the flash loan. + /// * `token` - The address of the token being loaned (loan currency). + /// * `amount` - The amount of tokens lent in the flash loan. + /// * `fee` - The additional fee to repay with the flash loan amount. + /// * `data` - Arbitrary data structure, intended to contain user-defined parameters. + #[allow(missing_docs)] + function onFlashLoan( + address initiator, + address token, + uint256 amount, + uint256 fee, + bytes calldata data + ) external returns (bytes32); + } +} diff --git a/contracts/src/token/erc20/utils/mod.rs b/contracts/src/token/erc20/utils/mod.rs index 955a3f63..def8b0d7 100644 --- a/contracts/src/token/erc20/utils/mod.rs +++ b/contracts/src/token/erc20/utils/mod.rs @@ -1,4 +1,5 @@ //! Utilities for the ERC-20 standard. +pub mod borrower; pub mod safe_erc20; pub use safe_erc20::SafeErc20; diff --git a/docs/modules/ROOT/pages/erc20-flashloan.adoc b/docs/modules/ROOT/pages/erc20-flashloan.adoc new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/docs/modules/ROOT/pages/erc20-flashloan.adoc @@ -0,0 +1 @@ + diff --git a/docs/modules/ROOT/pages/erc20.adoc b/docs/modules/ROOT/pages/erc20.adoc index 5a15c372..955c1c99 100644 --- a/docs/modules/ROOT/pages/erc20.adoc +++ b/docs/modules/ROOT/pages/erc20.adoc @@ -83,3 +83,5 @@ Additionally, there are multiple custom extensions, including: * xref:erc20-pausable.adoc[ERC-20 Pausable]: ability to pause token transfers. * xref:erc20-permit.adoc[ERC-20 Permit]: gasless approval of tokens (standardized as https://eips.ethereum.org/EIPS/eip-2612[`EIP-2612`]). + + * xref:erc20-flashloan.adoc[ERC-20 Flashloan]: flash loans through the minting and burning of ephemeral tokens (standardized as https://eips.ethereum.org/EIPS/eip-3156[`EIP-3156`]). diff --git a/examples/erc20-flashmint/Cargo.toml b/examples/erc20-flashmint/Cargo.toml new file mode 100644 index 00000000..61ae6a5d --- /dev/null +++ b/examples/erc20-flashmint/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "erc20-flashmint-example" +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-flashmint/src/ERC3156FlashBorrowerMock.sol b/examples/erc20-flashmint/src/ERC3156FlashBorrowerMock.sol new file mode 100644 index 00000000..770fac6c --- /dev/null +++ b/examples/erc20-flashmint/src/ERC3156FlashBorrowerMock.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.21; + +import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.1.0/contracts/interfaces/IERC3156FlashBorrower.sol"; + + +contract ERC3156FlashBorrowerMock is IERC3156FlashBorrower { + bytes32 internal constant _RETURN_VALUE = keccak256("ERC3156FlashBorrower.onFlashLoan"); + + bool immutable _enableApprove; + bool immutable _enableReturn; + + event BalanceOf(address token, address account, uint256 value); + event TotalSupply(address token, uint256 value); + + constructor(bool enableReturn, bool enableApprove) { + _enableApprove = enableApprove; + _enableReturn = enableReturn; + } + + function onFlashLoan( + address /*initiator*/, + address token, + uint256 amount, + uint256 fee, + bytes calldata data + ) public returns (bytes32) { + require(msg.sender == token); + + emit BalanceOf(token, address(this), IERC20(token).balanceOf(address(this))); + emit TotalSupply(token, IERC20(token).totalSupply()); + + if (data.length > 0) { + // WARNING: This code is for testing purposes only! Do not use. + Address.functionCall(token, data); + } + + if (_enableApprove) { + IERC20(token).approve(token, amount + fee); + } + + return _enableReturn ? _RETURN_VALUE : bytes32(0); + } +} diff --git a/examples/erc20-flashmint/src/constructor.sol b/examples/erc20-flashmint/src/constructor.sol new file mode 100644 index 00000000..38f4a989 --- /dev/null +++ b/examples/erc20-flashmint/src/constructor.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +contract Erc20FlashmintExample { + mapping(address account => uint256) private _balances; + mapping(address account => mapping(address spender => uint256)) + private _allowances; + uint256 private _totalSupply; + uint256 private _flash_fee_amount; + address private _flash_fee_receiver_address; + constructor(address flash_fee_receiver_address_, uint256 flash_fee_amount_) { + _flash_fee_receiver_address = flash_fee_receiver_address_; + _flash_fee_amount = flash_fee_amount_; + } +} diff --git a/examples/erc20-flashmint/src/lib.rs b/examples/erc20-flashmint/src/lib.rs new file mode 100644 index 00000000..30875cf4 --- /dev/null +++ b/examples/erc20-flashmint/src/lib.rs @@ -0,0 +1,50 @@ +#![cfg_attr(not(test), no_main)] +extern crate alloc; + +use alloc::vec::Vec; + +use alloy_primitives::{Address, U256}; +use openzeppelin_stylus::token::erc20::{extensions::Erc20Flashmint, IErc20}; +use stylus_sdk::prelude::{entrypoint, public, sol_storage}; + +sol_storage! { + #[entrypoint] + struct Erc20FlashmintExample { + #[borrow] + Erc20Flashmint erc20_flashmint; + } +} + +#[public] +#[inherit(Erc20Flashmint)] +impl Erc20FlashmintExample { + pub fn transfer( + &mut self, + to: Address, + value: U256, + ) -> Result> { + self.erc20_flashmint.erc20.transfer(to, value).map_err(|e| e.into()) + } + + pub fn transfer_from( + &mut self, + from: Address, + to: Address, + value: U256, + ) -> Result> { + self.erc20_flashmint + .erc20 + .transfer_from(from, to, value) + .map_err(|e| e.into()) + } + + // Add token minting feature. + pub fn mint( + &mut self, + account: Address, + value: U256, + ) -> Result<(), Vec> { + self.erc20_flashmint.erc20._mint(account, value)?; + Ok(()) + } +} diff --git a/examples/erc20-flashmint/tests/abi/mod.rs b/examples/erc20-flashmint/tests/abi/mod.rs new file mode 100644 index 00000000..ea75d7fd --- /dev/null +++ b/examples/erc20-flashmint/tests/abi/mod.rs @@ -0,0 +1,37 @@ +#![allow(dead_code)] +#![allow(clippy::too_many_arguments)] +use alloy::sol; + +sol!( + #[sol(rpc)] + contract Erc20Flashmint { + 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); + + function mint(address account, uint256 amount) external; + + function maxFlashLoan(address token) external view returns (uint256 maxLoan); + function flashFee(address token, uint256 amount) external view returns (uint256 fee); + function flashLoan(address receiver,address token, uint256 amount, bytes calldata data) external returns (bool); + + 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 ERC3156UnsupportedToken(address token); + error ERC3156ExceededMaxLoan(uint256 maxLoan); + error ERC3156InvalidReceiver(address receiver); + + #[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-flashmint/tests/erc20-flashloan.rs b/examples/erc20-flashmint/tests/erc20-flashloan.rs new file mode 100644 index 00000000..dd9eac98 --- /dev/null +++ b/examples/erc20-flashmint/tests/erc20-flashloan.rs @@ -0,0 +1,182 @@ +#![cfg(feature = "e2e")] + +use std::{assert_eq, println}; + +use abi::Erc20Flashmint; +use alloy::{ + primitives::{address, uint, Address, U256}, + sol, +}; +use e2e::{ + receipt, send, Account, ReceiptExt, + Revert, +}; +use eyre::Result; +// use stylus_sdk::contract::address; +use mock::borrower; + +use crate::Erc20FlashmintExample::constructorCall; + +mod abi; +mod mock; + +sol!("src/constructor.sol"); + +const RECIVER_ADDDRESS: Address = + address!("1000000000000000000000000000000000000000"); +const FLASH_FEE_AMOUNT: U256 = uint!(1_000_U256); + +impl Default for constructorCall { + fn default() -> Self { + ctr() + } +} + +fn ctr() -> constructorCall { + Erc20FlashmintExample::constructorCall { + flash_fee_receiver_address_: RECIVER_ADDDRESS, + flash_fee_amount_: FLASH_FEE_AMOUNT, + } +} + +// ============================================================================ +// Integration Tests: ERC-20 Token + Metadata Extension +// ============================================================================ + +#[e2e::test] +async fn constructs(alice: Account) -> Result<()> { + let contract_addr = alice + .as_deployer() + .with_default_constructor::() + .deploy() + .await? + .address()?; + let contract = Erc20Flashmint::new(contract_addr, &alice.wallet); + let Erc20Flashmint::totalSupplyReturn { totalSupply: total_supply } = + contract.totalSupply().call().await?; + assert_eq!(total_supply, U256::ZERO); + Ok(()) +} + +#[e2e::test] +async fn flash_fee(alice: Account) -> Result<()> { + let contract_addr = alice + .as_deployer() + .with_default_constructor::() + .deploy() + .await? + .address()?; + let contract = Erc20Flashmint::new(contract_addr, &alice.wallet); + + let Erc20Flashmint::flashFeeReturn { fee } = + contract.flashFee(contract_addr, uint!(1000_U256)).call().await?; + assert_eq!(fee, FLASH_FEE_AMOUNT); + Ok(()) +} + +#[e2e::test] +async fn flash_fee_rejects_unsupported_token(alice: Account) -> Result<()> { + let contract_addr = alice + .as_deployer() + .with_default_constructor::() + .deploy() + .await? + .address()?; + let contract = Erc20Flashmint::new(contract_addr, &alice.wallet); + let invalid_token_address = + address!("a6CB74633b3F981AB239ed5fe17E714184236b9C"); + + let err = send!(contract.flashFee(invalid_token_address, uint!(1000_U256))) + .expect_err("should fail with ERC3156UnsupportedToken"); + assert!(err.reverted_with(Erc20Flashmint::ERC3156UnsupportedToken { + token: invalid_token_address + })); + Ok(()) +} + +#[e2e::test] +async fn max_flash_loan(alice: Account) -> Result<()> { + let contract_addr = alice + .as_deployer() + .with_default_constructor::() + .deploy() + .await? + .address()?; + let contract = Erc20Flashmint::new(contract_addr, &alice.wallet); + let alice_addr = alice.address(); + + // let Erc20Flashmint::maxFlashLoanReturn { maxLoan } = + // contract.maxFlashLoan(contract_addr).call().await?; + // assert_eq!(maxLoan, U256::MAX); + + let _ = receipt!(contract.mint(alice_addr, uint!(1000_U256)))?; + let Erc20Flashmint::balanceOfReturn { balance } = + contract.balanceOf(alice_addr).call().await?; + println!("balance: {}", balance); + + // let Erc20Flashmint::maxFlashLoanReturn { maxLoan:bobMaxLoan } = + // contract.maxFlashLoan(contract_addr).call().await?; + + let Erc20Flashmint::totalSupplyReturn { totalSupply } = + contract.totalSupply().call().await?; + + // let Erc20Flashmint::maxFlashLoanReturn { maxLoan: testMaxLoan } = + // contract.maxFlashLoan(contract_addr).call().await?; + // let Erc20Flashmint::balanceOfReturn { balance } = + // contract.balanceOf(bob.address()).call().await?; + // let Erc20Flashmint::balanceOfReturn { balance: alice_balance } = + // contract.balanceOf(alice.address()).call().await?; + let Erc20Flashmint::maxFlashLoanReturn { maxLoan } = + contract.maxFlashLoan(contract_addr).call().await?; + println!("totalSupply: {}", totalSupply); + println!("maxLoan: {}", maxLoan); + // println!("bobMaxLoan: {}", bobMaxLoan); + // println!("testMaxLoan: {}", testMaxLoan); + // println!("balance: {}", balance); + // println!("alice_balance: {}", alice_balance); + // assert_eq!(bobMaxLoan, U256::MAX); + Ok(()) +} + +#[e2e::test] +async fn max_flash_loan_invalid_address(alice: Account) -> Result<()> { + let contract_addr = alice + .as_deployer() + .with_default_constructor::() + .deploy() + .await? + .address()?; + let contract = Erc20Flashmint::new(contract_addr, &alice.wallet); + let random_address = address!("a6CB74633b3F981AB239ed5fe17E714184236b9C"); + + let Erc20Flashmint::maxFlashLoanReturn { maxLoan } = + contract.maxFlashLoan(random_address).call().await?; + assert_eq!(maxLoan, U256::MIN); + Ok(()) +} + +#[e2e::test] +async fn can_deploy_mock_borrower(alice: Account) -> Result<()> { + let borrower = borrower::deploy(&alice.wallet).await?; + assert_eq!(borrower.is_zero(), false); + Ok(()) +} + +// #[e2e::test] +// async fn flash_loan(alice: Account) -> Result<()> { +// let contract_addr = alice +// .as_deployer() +// .with_default_constructor::() +// .deploy() +// .await? +// .address()?; +// let contract = Erc20Flashmint::new(contract_addr, &alice.wallet); +// let alice_addr = alice.address(); +// let random_address = +// address!("a6CB74633b3F981AB239ed5fe17E714184236b9C"); + +// let Erc20Flashmint::maxFlashLoanReturn { maxLoan } = +// contract.maxFlashLoan(random_address).call().await?; +// assert_eq!(maxLoan, U256::MIN); +// Ok(()) +// } diff --git a/examples/erc20-flashmint/tests/mock/borrower.rs b/examples/erc20-flashmint/tests/mock/borrower.rs new file mode 100644 index 00000000..670788c9 --- /dev/null +++ b/examples/erc20-flashmint/tests/mock/borrower.rs @@ -0,0 +1,57 @@ +#![allow(dead_code)] +#![cfg(feature = "e2e")] +use alloy::{ + primitives:: Address, + sol, +}; +use e2e::Wallet; + +sol! { + #[allow(missing_docs)] + // Built with Hardhat; solc v0.8.21+commit.d9974bed + #[sol(rpc, bytecode="60c060405234801561000f575f80fd5b5060405161066338038061066383398101604081905261002e91610051565b1515608052151560a052610082565b8051801515811461004c575f80fd5b919050565b5f8060408385031215610062575f80fd5b61006b8361003d565b91506100796020840161003d565b90509250929050565b60805160a0516105c06100a35f395f6102b801525f61020a01526105c05ff3fe608060405234801561000f575f80fd5b5060043610610029575f3560e01c806323e30c8b1461002d575b5f80fd5b61004061003b36600461046b565b610052565b60405190815260200160405180910390f35b5f336001600160a01b03871614610067575f80fd5b6040516370a0823160e01b815230600482018190527f6ff2acfcb07917b1e80e53f0fe390b467b1151d15b38730a6e08397799c05a8b918891906001600160a01b038316906370a0823190602401602060405180830381865afa1580156100d0573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906100f49190610509565b604080516001600160a01b0394851681529390921660208401529082015260600160405180910390a17f7249fd4c03cce09b30a13d77804b198e2647c0ccd59eadf4de4e7c16099badc586876001600160a01b03166318160ddd6040518163ffffffff1660e01b8152600401602060405180830381865afa15801561017b573d5f803e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061019f9190610509565b604080516001600160a01b03909316835260208301919091520160405180910390a18115610208576102068684848080601f0160208091040260200160405190810160405280939291908181526020018383808284375f9201919091525061030e92505050565b505b7f0000000000000000000000000000000000000000000000000000000000000000156102b6576001600160a01b03861663095ea7b3876102488789610520565b6040516001600160e01b031960e085901b1681526001600160a01b03909216600483015260248201526044016020604051808303815f875af1158015610290573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906102b4919061053f565b505b7f00000000000000000000000000000000000000000000000000000000000000006102e1575f610303565b7f439148f0bbc682ca079e46d6e2c2f0c1e3b820f1a291b069d8882abf8cf18dd95b979650505050505050565b606061031b83835f610324565b90505b92915050565b6060814710156103555760405163cf47918160e01b8152476004820152602481018390526044015b60405180910390fd5b5f80856001600160a01b03168486604051610370919061055e565b5f6040518083038185875af1925050503d805f81146103aa576040519150601f19603f3d011682016040523d82523d5f602084013e6103af565b606091505b50915091506103bf8683836103cb565b925050505b9392505050565b6060826103e0576103db82610427565b6103c4565b81511580156103f757506001600160a01b0384163b155b1561042057604051639996b31560e01b81526001600160a01b038516600482015260240161034c565b50806103c4565b8051156104375780518082602001fd5b60405163d6bda27560e01b815260040160405180910390fd5b80356001600160a01b0381168114610466575f80fd5b919050565b5f805f805f8060a08789031215610480575f80fd5b61048987610450565b955061049760208801610450565b94506040870135935060608701359250608087013567ffffffffffffffff808211156104c1575f80fd5b818901915089601f8301126104d4575f80fd5b8135818111156104e2575f80fd5b8a60208285010111156104f3575f80fd5b6020830194508093505050509295509295509295565b5f60208284031215610519575f80fd5b5051919050565b8082018082111561031e57634e487b7160e01b5f52601160045260245ffd5b5f6020828403121561054f575f80fd5b815180151581146103c4575f80fd5b5f82515f5b8181101561057d5760208186018101518583015201610563565b505f92019182525091905056fea264697066735822122031775710bbb1bf7ed2b9e94b901d3bf2cdef44910a00072957ca9b7bc820a7bc64736f6c63430008180033")] + contract ERC3156FlashBorrowerMock is IERC3156FlashBorrower { + bytes32 internal constant _RETURN_VALUE = keccak256("ERC3156FlashBorrower.onFlashLoan"); + + bool immutable _enableApprove; + bool immutable _enableReturn; + + event BalanceOf(address token, address account, uint256 value); + event TotalSupply(address token, uint256 value); + + constructor(bool enableReturn, bool enableApprove) { + _enableApprove = enableApprove; + _enableReturn = enableReturn; + } + + function onFlashLoan( + address /*initiator*/, + address token, + uint256 amount, + uint256 fee, + bytes calldata data + ) public returns (bytes32) { + require(msg.sender == token); + + emit BalanceOf(token, address(this), IERC20(token).balanceOf(address(this))); + emit TotalSupply(token, IERC20(token).totalSupply()); + + if (data.length > 0) { + // WARNING: This code is for testing purposes only! Do not use. + Address.functionCall(token, data); + } + + if (_enableApprove) { + IERC20(token).approve(token, amount + fee); + } + + return _enableReturn ? _RETURN_VALUE : bytes32(0); + } + } +} + +pub async fn deploy(wallet: &Wallet) -> eyre::Result
{ + // Deploy the contract. + let contract = ERC3156FlashBorrowerMock::deploy(wallet, true, true).await?; + Ok(*contract.address()) +} diff --git a/examples/erc20-flashmint/tests/mock/mod.rs b/examples/erc20-flashmint/tests/mock/mod.rs new file mode 100644 index 00000000..332f3c39 --- /dev/null +++ b/examples/erc20-flashmint/tests/mock/mod.rs @@ -0,0 +1 @@ +pub mod borrower; diff --git a/examples/erc20/src/lib.rs b/examples/erc20/src/lib.rs index 28ec60b2..591034a9 100644 --- a/examples/erc20/src/lib.rs +++ b/examples/erc20/src/lib.rs @@ -6,7 +6,10 @@ use alloc::vec::Vec; use alloy_primitives::{Address, FixedBytes, U256}; use openzeppelin_stylus::{ token::erc20::{ - extensions::{capped, Capped, Erc20Metadata, IErc20Burnable}, + extensions::{ + capped, flashmint::IERC3156FlashLender, Capped, Erc20Metadata, + IErc20Burnable, + }, Erc20, IErc20, }, utils::{introspection::erc165::IErc165, Pausable}, diff --git a/scripts/e2e-tests.sh b/scripts/e2e-tests.sh index 87c4fe65..4423f8e1 100755 --- a/scripts/e2e-tests.sh +++ b/scripts/e2e-tests.sh @@ -5,8 +5,9 @@ MYDIR=$(realpath "$(dirname "$0")") cd "$MYDIR" cd .. +echo $MYDIR cargo build --release --target wasm32-unknown-unknown -Z build-std=std,panic_abort -Z build-std-features=panic_immediate_abort export RPC_URL=http://localhost:8547 -cargo test --features std,e2e --test "*" +cargo test --features std,e2e --test "erc20-flashloan" -- --nocapture