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: Erc20Wrapper extension #498

Draft
wants to merge 29 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1944622
chore: wrapper approch
Ifechukwudaniel Jan 16, 2025
0cca59f
chore: code fmt
Ifechukwudaniel Jan 16, 2025
3bab6ac
chore: comments
Ifechukwudaniel Jan 16, 2025
038020b
chore: erc20 wrapper token
Ifechukwudaniel Jan 16, 2025
7587d34
chore: code fmt
Ifechukwudaniel Jan 16, 2025
961353e
chore: wrapper example
Ifechukwudaniel Jan 16, 2025
a347f69
chore: erc20 wrapper
Ifechukwudaniel Jan 25, 2025
9c66630
chore: removed variable
Ifechukwudaniel Jan 25, 2025
bd69a58
chore: wrapper and example
Ifechukwudaniel Jan 26, 2025
1bedc16
chore: erc20 wrapper example
Ifechukwudaniel Jan 26, 2025
9d5419c
cargo fmt
Ifechukwudaniel Jan 26, 2025
2d6a02f
chore: fixed constructor test
Ifechukwudaniel Jan 27, 2025
398da73
chore: wrapper
Ifechukwudaniel Jan 27, 2025
3405bf2
chore: fomrmat
Ifechukwudaniel Jan 27, 2025
dfc1e00
chore: CHANGELOG
Ifechukwudaniel Jan 27, 2025
4274d2b
chore:docs
Ifechukwudaniel Jan 27, 2025
fd1de52
chore: removed docs
Ifechukwudaniel Jan 27, 2025
fb6a894
chore: erc20 wrappers docs
Ifechukwudaniel Jan 27, 2025
102611b
chore: fix underlying state
Ifechukwudaniel Jan 27, 2025
1f450b2
chore: fixed clippy error
Ifechukwudaniel Jan 27, 2025
fae5882
chore: fixed clippy warnings
Ifechukwudaniel Jan 27, 2025
f8d684a
docs
Ifechukwudaniel Jan 28, 2025
d13ad47
docs: added missing docs
Ifechukwudaniel Jan 28, 2025
ab401e2
chore: format
Ifechukwudaniel Jan 28, 2025
4070411
docs: ivalid to invalid
Ifechukwudaniel Jan 28, 2025
4ac2b5c
test: underlying test works
Ifechukwudaniel Jan 28, 2025
104c45a
test: test fixes
Ifechukwudaniel Jan 28, 2025
b6e1d6e
Merge branch 'main' into ERC20Wrapper
Ifechukwudaniel Jan 30, 2025
1b15a10
Merge branch 'main' into ERC20Wrapper
Ifechukwudaniel Jan 31, 2025
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Implement `mul_div` for `U256`. #465
- Implement `AddAssignChecked` for `StorageUint`. #474
- `Erc20FlashMint` extension. #407
- `Erc20Wrapper` "Token Wrapping contract". #498

### Changed

Expand Down
2 changes: 2 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 @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions contracts/src/token/erc20/extensions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ pub mod erc4626;
pub mod flash_mint;
pub mod metadata;
pub mod permit;
pub mod wrapper;

pub use burnable::IErc20Burnable;
pub use capped::Capped;
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};
272 changes: 272 additions & 0 deletions contracts/src/token/erc20/extensions/wrapper.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
//! 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::{self, ISafeErc20},
SafeErc20,
},
Erc20,
};

mod token {

#![allow(missing_docs)]
#![cfg_attr(coverage_nightly, coverage(off))]

use alloc::vec;

use stylus_sdk::stylus_proc::sol_interface;

sol_interface! {
/// Solidity Interface of the ERC-20 token.
interface IErc20 {
function balanceOf(address account) external view returns (uint256);
function totalSupply() external view returns (uint256);
}
}
}

use token::IErc20 as IErc20Solidity;

sol! {
Ifechukwudaniel marked this conversation as resolved.
Show resolved Hide resolved
/// 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 thata 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 this `ERC20Wrapper` trait implementation.
type Error: Into<alloc::vec::Vec<u8>>;

/// 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_to(
Ifechukwudaniel marked this conversation as resolved.
Show resolved Hide resolved
&mut self,
account: Address,
value: U256,
erc20: &mut Erc20,
) -> Result<bool, Self::Error>;

/// 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(
Ifechukwudaniel marked this conversation as resolved.
Show resolved Hide resolved
&mut self,
account: Address,
value: U256,
erc20: &mut Erc20,
) -> Result<bool, Self::Error>;
}

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

impl IERC20Wrapper for Erc20Wrapper {
type Error = Error;

fn underlying(&self) -> Address {
self.underlying_address.get()
}

fn deposit_to(
&mut self,
account: Address,
value: U256,
erc20: &mut Erc20,
) -> Result<bool, Error> {
let underlined_token = self.underlying_address.get();
let sender = msg::sender();
if account == contract::address() {
return Err(Error::InvalidSender(ERC20InvalidSender {
sender: contract::address(),
}));
}

if sender == contract::address() {
return Err(Error::InvalidReceiver(ERC20InvalidReceiver {
receiver: account,
}));
}
self.safe_erc20.safe_transfer_from(
underlined_token,
sender,
contract::address(),
value,
)?;
erc20._mint(account, value)?;
Ok(true)
}

fn withdraw_to(
&mut self,
account: Address,
value: U256,
erc20: &mut Erc20,
) -> Result<bool, Error> {
let underlined_token = self.underlying_address.get();
if account == contract::address() {
return Err(Error::InvalidReceiver(ERC20InvalidReceiver {
receiver: account,
}));
}
erc20._burn(account, value)?;
self.safe_erc20.safe_transfer(underlined_token, 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(
Ifechukwudaniel marked this conversation as resolved.
Show resolved Hide resolved
&mut self,
account: Address,
erc20: &mut Erc20,
) -> Result<U256, Error> {
let token = IErc20Solidity::new(self.underlying());
let value = 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 super::Erc20Wrapper;

#[motsu::test]
fn underlying_works(contract: Erc20Wrapper) {
let asset = address!("DeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF");
contract.underlying_address.set(asset);
assert_eq!(contract.underlying(), asset);
}
}
57 changes: 57 additions & 0 deletions docs/modules/ROOT/pages/erc20-wrapper.adoc
Original file line number Diff line number Diff line change
@@ -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<bool, Vec<u8>> {
Ok(self.wrapper.deposit_to(account, value, &mut self.erc20)?)
}

fn withdraw_to(
&mut self,
account: Address,
value: U256,
) -> Result<bool, Vec<u8>> {
Ok(self.wrapper.withdraw_to(account, value, &mut self.erc20)?)
}
}
----
2 changes: 2 additions & 0 deletions docs/modules/ROOT/pages/erc20.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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 .
Loading
Loading