diff --git a/Cargo.lock b/Cargo.lock index 3f16a44cc..9de1fd649 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1788,6 +1788,20 @@ dependencies = [ "tokio", ] +[[package]] +name = "erc1967-example" +version = "0.2.0" +dependencies = [ + "alloy", + "alloy-primitives", + "alloy-sol-types", + "e2e", + "eyre", + "openzeppelin-stylus", + "stylus-sdk", + "tokio", +] + [[package]] name = "erc20-example" version = "0.2.0" @@ -3254,6 +3268,20 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "proxy-example" +version = "0.2.0" +dependencies = [ + "alloy", + "alloy-primitives", + "alloy-sol-types", + "e2e", + "eyre", + "openzeppelin-stylus", + "stylus-sdk", + "tokio", +] + [[package]] name = "quick-error" version = "1.2.3" diff --git a/Cargo.toml b/Cargo.toml index 4d6e4bd71..f034164f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "lib/e2e-proc", "examples/access-control", "examples/ecdsa", + "examples/erc1967", "examples/erc20", "examples/erc20-permit", "examples/erc20-flash-mint", @@ -26,6 +27,7 @@ members = [ "examples/vesting-wallet", "examples/ownable-two-step", "examples/poseidon", + "examples/proxy", "examples/pedersen", "examples/basic/token", "examples/basic/script", @@ -37,6 +39,7 @@ default-members = [ "lib/e2e-proc", "examples/access-control", "examples/ecdsa", + "examples/erc1967", "examples/erc20", "examples/erc20-permit", "examples/erc20-flash-mint", @@ -55,6 +58,7 @@ default-members = [ "examples/vesting-wallet", "examples/ownable-two-step", "examples/poseidon", + "examples/proxy", "examples/pedersen", "examples/basic/token", ] diff --git a/GUIDELINES.md b/GUIDELINES.md index 549aff0c4..3458dddb5 100644 --- a/GUIDELINES.md +++ b/GUIDELINES.md @@ -3,8 +3,6 @@ ## Setup 1. Install [Docker]. -1. Install the [Solidity Compiler] version `0.8.24`. -(NOTE: it is important to use this exact version to avoid compatibility issues). 1. Install toolchain providing `cargo` using [rustup]. 1. Install the cargo stylus tool with `cargo install --force cargo-stylus`. @@ -15,8 +13,6 @@ and retry installing the stylus tool. [Docker]: https://docs.docker.com/engine/install/ -[Solidity Compiler]: https://docs.soliditylang.org/en/v0.8.28/installing-solidity.html#linux-packages - [rustup]: https://rustup.rs/ ## Testing diff --git a/contracts/src/proxy/beacon/mod.rs b/contracts/src/proxy/beacon/mod.rs index a518f05a9..420784734 100644 --- a/contracts/src/proxy/beacon/mod.rs +++ b/contracts/src/proxy/beacon/mod.rs @@ -1,5 +1,23 @@ //! Solidity Interface of `BeaconProxy`. -pub use beacon::*; + +use alloy_primitives::Address; + +mod proxy; +mod upgradeable; + +pub use beacon::IBeaconInterface; +pub use proxy::BeaconProxy; +pub use upgradeable::{IUpgradeableBeacon, UpgradeableBeacon}; + +/// This is the interface that [BeaconProxy][BeaconProxy] expects of its beacon. +/// +/// [BeaconProxy]: crate::proxy::beacon::BeaconProxy +pub trait IBeacon { + /// Must return an address that can be used as a delegate call target. + /// + /// [`UpgradeableBeacon`] will check that this address is a contract. + fn implementation(&self) -> Result; +} mod beacon { #![allow(missing_docs)] @@ -9,7 +27,7 @@ mod beacon { use stylus_sdk::prelude::sol_interface; sol_interface! { - interface IBeacon { + interface IBeaconInterface { function implementation() external view returns (address); } } diff --git a/contracts/src/proxy/beacon/proxy.rs b/contracts/src/proxy/beacon/proxy.rs new file mode 100644 index 000000000..9b49d248f --- /dev/null +++ b/contracts/src/proxy/beacon/proxy.rs @@ -0,0 +1,89 @@ +//! This contract implements a proxy that gets the implementation address for +//! each call from an [UpgradeableBeacon][UpgradeableBeacon]. +//! +//! The beacon address can only be set once during construction, and cannot be +//! changed afterwards. It is stored in an immutable variable to avoid +//! unnecessary storage reads, and also in the beacon storage slot specified by +//! [ERC-1967] so that it can be accessed externally. +//! +//! CAUTION: Since the beacon address can never be changed, you must ensure that +//! you either control the beacon, or trust the beacon to not upgrade the +//! implementation maliciously. +//! +//! IMPORTANT: Do not use the implementation logic to modify the beacon storage +//! slot. Doing so would leave the proxy in an inconsistent state where the +//! beacon storage slot does not match the beacon address. +//! +//! [UpgradeableBeacon]: crate::proxy::beacon::UpgradeableBeacon +//! [ERC-1967]: https://eips.ethereum.org/EIPS/eip-1967 + +use alloc::{vec, vec::Vec}; + +use alloy_primitives::Address; +use stylus_sdk::{abi::Bytes, prelude::*, storage::StorageAddress}; + +use crate::proxy::{ + beacon::IBeaconInterface, + erc1967::{Erc1967Utils, Error}, + IProxy, +}; + +/// State of an [`BeaconProxy`] token. +#[storage] +pub struct BeaconProxy { + beacon: StorageAddress, +} + +/// 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 BeaconProxy {} + +impl BeaconProxy { + /// Initializes the proxy with `beacon`. + /// + /// If `data` is nonempty, it's used as data in a delegate call to the + /// implementation returned by the beacon. This will typically be an + /// encoded function call, and allows initializing the storage of the proxy + /// like a Solidity constructor. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `beacon` - The beacon address. + /// * `data` - The data to pass to the beacon. + /// + /// # Errors + /// + /// * [`Error::InvalidBeacon`] - If the beacon is not a contract with the + /// interface [IBeacon][IBeacon]. + /// * [`Error::NonPayable`] - If the data is empty and + /// [msg::value][msg_value] is not [`U256::ZERO`]. + /// + /// [msg_value]: stylus_sdk::msg::value + /// [IBeacon]: crate::proxy::beacon::IBeacon + pub fn constructor( + &mut self, + beacon: Address, + data: Bytes, + ) -> Result<(), Error> { + Erc1967Utils::upgrade_beacon_to_and_call(self, beacon, data)?; + self.beacon.set(beacon); + Ok(()) + } + + /// Returns the beacon. + /// + /// # Arguments + /// + /// * `&self` - Read access to the contract's state. + pub fn get_beacon(&self) -> Address { + self.beacon.get() + } +} + +impl IProxy for BeaconProxy { + fn implementation(&self) -> Result { + IBeaconInterface::new(self.get_beacon()).implementation(self) + } +} diff --git a/contracts/src/proxy/beacon/upgradeable.rs b/contracts/src/proxy/beacon/upgradeable.rs new file mode 100644 index 000000000..549568ffd --- /dev/null +++ b/contracts/src/proxy/beacon/upgradeable.rs @@ -0,0 +1,24 @@ +use alloc::{vec, vec::Vec}; + +use stylus_sdk::{prelude::*, storage::StorageAddress}; + +use crate::{access::ownable::IOwnable, proxy::beacon::IBeacon}; + +/// This contract is used in conjunction with one or more instances of +/// [BeaconProxy][BeaconProxy] to determine their implementation contract, which +/// is where they will delegate all function calls. +/// +/// An owner is able to change the implementation the beacon points to, thus +/// upgrading the proxies that use this beacon. +/// +/// [BeaconProxy]: crate::proxy::beacon::BeaconProxy +pub trait IUpgradeableBeacon: IBeacon + IOwnable {} + +/// State of an [`UpgradeableBeacon`] contract. +#[storage] +pub struct UpgradeableBeacon { + /// The address of the implementation contract. + implementation: StorageAddress, + /// The address of the owner of the contract. + owner: StorageAddress, +} diff --git a/contracts/src/proxy/erc1967/mod.rs b/contracts/src/proxy/erc1967/mod.rs index 7ae58246b..1d876ff12 100644 --- a/contracts/src/proxy/erc1967/mod.rs +++ b/contracts/src/proxy/erc1967/mod.rs @@ -1,11 +1,22 @@ -//! Proxy Storage Slots and the events as defined in -//! the [ERC-1967]. +//! This contract implements an upgradeable proxy. It is upgradeable because +//! calls are delegated to an implementation address that can be changed. This +//! address is stored in storage in the location specified by +//! [ERC-1967], so that it doesn't conflict with the storage layout of the +//! implementation behind the proxy. +//! +//! [ERC-1967]: https://eips.ethereum.org/EIPS/eip-1967 +use alloc::{vec, vec::Vec}; + +use alloy_primitives::Address; +use stylus_sdk::{abi::Bytes, prelude::*}; + +use crate::proxy::IProxy; -//! [ERC-1967]: -pub mod proxy; pub mod utils; pub use sol::*; +pub use utils::{Erc1967Utils, Error}; + #[cfg_attr(coverage_nightly, coverage(off))] mod sol { use alloy_sol_macro::sol; @@ -27,3 +38,40 @@ mod sol { event BeaconUpgraded(address indexed beacon); } } + +/// State of an [`Erc1967Proxy`] token. +#[storage] +pub struct Erc1967Proxy; + +/// 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 Erc1967Proxy {} + +impl Erc1967Proxy { + /// Constructor. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `implementation` - Address of the implementation contract. + /// * `data` - Data to pass to the implementation contract. + pub fn constructor( + &mut self, + implementation: Address, + data: Bytes, + ) -> Result<(), Error> { + Erc1967Utils::upgrade_to_and_call(implementation, data) + } +} + +impl IProxy for Erc1967Proxy { + /** + * @dev This is a virtual function that should be overridden so it + * returns the address to which the fallback function and + * {_fallback} should delegate. + */ + fn implementation(&self) -> Result { + Ok(Erc1967Utils::get_implementation()) + } +} diff --git a/contracts/src/proxy/erc1967/proxy.rs b/contracts/src/proxy/erc1967/proxy.rs deleted file mode 100644 index 33fc0b8f9..000000000 --- a/contracts/src/proxy/erc1967/proxy.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! Module with a contract that implement an upgradeable proxy. -//! -//! It is upgradeable because calls are delegated to an implementation address -//! that can be changed. This address is stored in storage in the location -//! specified by [ERC-1967], so that it doesn't conflict with the storage layout -//! of the implementation behind the proxy. -//! -//! [ERC-1967]: https://eips.ethereum.org/EIPS/eip-1967 -use alloc::{vec, vec::Vec}; - -use alloy_primitives::Address; -use stylus_sdk::{ - abi::Bytes, - prelude::{public, storage}, - ArbResult, -}; - -use crate::proxy::{erc1967::utils::IErc1967Utils, IProxy}; - -/// TODO -pub struct Erc1967Proxy {} - -impl Erc1967Proxy {} diff --git a/contracts/src/proxy/erc1967/utils.rs b/contracts/src/proxy/erc1967/utils.rs index c379bbcd1..94e409206 100644 --- a/contracts/src/proxy/erc1967/utils.rs +++ b/contracts/src/proxy/erc1967/utils.rs @@ -3,15 +3,19 @@ //! //! [ERC-1967]: https://eips.ethereum.org/EIPS/eip-1967 -use alloc::{vec, vec::Vec}; - -use alloy_primitives::{b256, Address, B256}; +use alloy_primitives::{uint, Address, U256}; pub use sol::*; -use stylus_sdk::{abi::Bytes, call::Call, evm, msg, prelude::*}; +use stylus_sdk::{ + abi::Bytes, + call::{MethodError, RawCall}, + evm, msg, + prelude::*, + storage::StorageAddress, +}; use crate::{ - proxy::{beacon::IBeacon, erc1967}, - utils::{address::AddressHelper, storage_slot::StorageSlot}, + proxy::{beacon::IBeaconInterface, erc1967}, + utils::storage_slot::StorageSlot, }; #[cfg_attr(coverage_nightly, coverage(off))] @@ -69,46 +73,44 @@ pub enum Error { NonPayable(ERC1967NonPayable), } -/// State of an [`Erc1967Utils`] contract. -#[storage] -pub struct Erc1967Utils { - storage_slot: StorageSlot, - address_helper: AddressHelper, +impl MethodError for Error { + fn encode(self) -> alloc::vec::Vec { + self.into() + } } /// Storage slot with the address of the current implementation. /// This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by /// 1. -const IMPLEMENTATION_SLOT: B256 = - b256!("360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"); +const IMPLEMENTATION_SLOT: U256 = uint!( + 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc_U256 +); /// Storage slot with the admin of the contract. /// This is the keccak-256 hash of "eip1967.proxy.admin" subtracted by 1. -const ADMIN_SLOT: B256 = - b256!("b53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103"); +const ADMIN_SLOT: U256 = uint!( + 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103_U256 +); /// The storage slot of the UpgradeableBeacon contract which defines the /// implementation for this proxy. /// /// This is the keccak-256 hash of "eip1967.proxy.beacon" subtracted by 1. -const BEACON_SLOT: B256 = - b256!("a3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50"); - -/// 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 Erc1967Utils {} +const BEACON_SLOT: U256 = uint!( + 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50_U256 +); -/// Interface for the [`Erc1967Utils`] contract. -pub trait IErc1967Utils { - /// The error type associated with this interface implementation. - type Error: Into>; +/// This library provides getters and event emitting update functions for +/// [ERC-1967] slots. +/// +/// [ERC-1967]: https://eips.ethereum.org/EIPS/eip-1967 +pub struct Erc1967Utils; +/// Implementation of the [`Erc1967Utils`] library. +impl Erc1967Utils { /// Returns the current implementation address. - /// - /// # Arguments - /// - /// * `&self` - Read access to the contract's state. - fn get_implementation(&self) -> Address; + pub fn get_implementation() -> Address { + StorageSlot::get_slot::(IMPLEMENTATION_SLOT).get() + } /// Performs implementation upgrade with additional setup call if /// data is nonempty. This function is payable only if the setup call @@ -122,33 +124,52 @@ pub trait IErc1967Utils { /// * `data` - The data to pass to the setup call. /// /// # Errors (TODO) - fn upgrade_to_and_call( - &mut self, + pub fn upgrade_to_and_call( new_implementation: Address, data: Bytes, - ) -> Result<(), Self::Error>; + ) -> Result<(), Error> { + Erc1967Utils::_set_implementation(new_implementation)?; + + evm::log(erc1967::Upgraded { implementation: new_implementation }); + + if data.len() > 0 { + // TODO: extract to Address library + unsafe { + RawCall::new_delegate() + .flush_storage_cache() + .call(new_implementation, data.as_slice()) + .expect("TODO: handle error"); + } + } else { + Erc1967Utils::_check_non_payable()?; + } + + Ok(()) + } /// Returns the current admin. - /// - /// # Arguments - /// - /// * `&self` - Read access to the contract's state. - fn get_admin(&self) -> Address; + pub fn get_admin() -> Address { + StorageSlot::get_slot::(ADMIN_SLOT).get() + } /// Changes the admin of the proxy. /// /// # Arguments /// - /// * `&mut self` - Mutable access to the contract's state. /// * `new_admin` - The new admin address. - fn change_admin(&mut self, new_admin: Address); + pub fn change_admin(new_admin: Address) -> Result<(), Error> { + evm::log(erc1967::AdminChanged { + previous_admin: Erc1967Utils::get_admin(), + new_admin, + }); + + Erc1967Utils::_set_admin(new_admin) + } /// Returns the current beacon. - /// - /// # Arguments - /// - /// * `&self` - Read access to the contract's state. - fn get_beacon(&self) -> Address; + pub fn get_beacon() -> Address { + StorageSlot::get_slot::(BEACON_SLOT).get() + } /// Change the beacon and trigger a setup call if data is nonempty. /// This function is payable only if the setup call is performed, @@ -160,76 +181,27 @@ pub trait IErc1967Utils { /// * `&mut self` - Mutable access to the contract's state. /// * `new_beacon` - The new beacon address. /// * `data` - The data to pass to the setup call. - fn upgrade_beacon_to_and_call( - &mut self, - new_beacon: Address, - data: Bytes, - ) -> Result<(), Self::Error>; -} - -impl IErc1967Utils for Erc1967Utils { - type Error = Error; - - fn get_implementation(&self) -> Address { - self.storage_slot.get_address_slot(IMPLEMENTATION_SLOT) - } - - fn upgrade_to_and_call( - &mut self, - new_implementation: Address, - data: Bytes, - ) -> Result<(), Self::Error> { - self._set_implementation(new_implementation)?; - evm::log(erc1967::Upgraded { implementation: new_implementation }); - - if data.len() > 0 { - self._check_non_payable()?; - } else { - self.address_helper - .function_delegate_call(new_implementation, data) - .expect("TODO: handle error"); - } - - Ok(()) - } - - fn get_admin(&self) -> Address { - self.storage_slot.get_address_slot(ADMIN_SLOT) - } - - fn change_admin(&mut self, new_admin: Address) { - evm::log(erc1967::AdminChanged { - previous_admin: self.get_admin(), - new_admin, - }); - - self._set_admin(new_admin).expect("TODO: handle error"); - } - - fn get_beacon(&self) -> Address { - self.storage_slot.get_address_slot(BEACON_SLOT) - } - - fn upgrade_beacon_to_and_call( - &mut self, + pub fn upgrade_beacon_to_and_call( + context: &T, new_beacon: Address, data: Bytes, - ) -> Result<(), Self::Error> { - self._set_beacon(new_beacon)?; + ) -> Result<(), Error> { + Erc1967Utils::_set_beacon(context, new_beacon)?; evm::log(erc1967::BeaconUpgraded { beacon: new_beacon }); if data.len() > 0 { let beacon_implementation = - self.get_beacon_implementation(new_beacon)?; - - _ = self - .address_helper - .function_delegate_call(beacon_implementation, data) - .map_err(|e| { - panic!("TODO: handle error: {:?}", e); - }); + Erc1967Utils::get_beacon_implementation(context, new_beacon)?; + + // TODO: extract to Address library + unsafe { + RawCall::new_delegate() + .flush_storage_cache() + .call(beacon_implementation, data.as_slice()) + .expect("TODO: handle error"); + } } else { - self._check_non_payable()?; + Erc1967Utils::_check_non_payable()?; } Ok(()) @@ -237,37 +209,15 @@ impl IErc1967Utils for Erc1967Utils { } impl Erc1967Utils { - /// TODO: docs - fn _set_implementation( - &mut self, - new_implementation: Address, - ) -> Result<(), Error> { - if self.vm().code_size(new_implementation) == 0 { - return Err(ERC1967InvalidImplementation { - implementation: new_implementation, - } - .into()); - } - - self.storage_slot - .set_address_slot(IMPLEMENTATION_SLOT, new_implementation); - - Ok(()) - } - /// Reverts if [`msg::value()`] is not [`alloy_primitives::U256::ZERO`]. It /// can be used to avoid [`msg::value()`] stuck in the contract if an /// upgrade does not perform an initialization call. /// - /// # Arguments - /// - /// * `&self` - Read access to the contract's state. - /// /// # Errors /// /// * [`Error::NonPayable`] - If [`msg::value()`] is not /// [`alloy_primitives::U256::ZERO`]. - fn _check_non_payable(&self) -> Result<(), Error> { + fn _check_non_payable() -> Result<(), Error> { if msg::value().is_zero() { Ok(()) } else { @@ -275,20 +225,43 @@ impl Erc1967Utils { } } + /// Stores a new address in the ERC-1967 implementation slot. + /// + /// # Arguments + /// + /// * `new_implementation` - The new implementation address. + /// + /// # Errors + /// + /// * [`Error::InvalidImplementation`] - If the `new_implementation` address + /// is not a valid implementation. + fn _set_implementation(new_implementation: Address) -> Result<(), Error> { + if !new_implementation.has_code() { + return Err(ERC1967InvalidImplementation { + implementation: new_implementation, + } + .into()); + } + + StorageSlot::get_slot::(IMPLEMENTATION_SLOT) + .set(new_implementation); + + Ok(()) + } + /// Stores a new address in the ERC-1967 admin slot. /// /// # Arguments /// - /// * `&mut self` - Mutable access to the contract's state. /// * `new_admin` - The new admin address. /// /// # Errors (TODO) - fn _set_admin(&mut self, new_admin: Address) -> Result<(), Error> { + fn _set_admin(new_admin: Address) -> Result<(), Error> { if new_admin.is_zero() { return Err(ERC1967InvalidAdmin { admin: new_admin }.into()); } - self.storage_slot.set_address_slot(ADMIN_SLOT, new_admin); + StorageSlot::get_slot::(ADMIN_SLOT).set(new_admin); Ok(()) } @@ -301,19 +274,22 @@ impl Erc1967Utils { /// * `new_beacon` - The new beacon address. /// /// # Errors (TODO) - fn _set_beacon(&mut self, new_beacon: Address) -> Result<(), Error> { - if self.vm().code_size(new_beacon) == 0 { + fn _set_beacon( + context: &T, + new_beacon: Address, + ) -> Result<(), Error> { + if !new_beacon.has_code() { return Err(ERC1967InvalidBeacon { beacon: new_beacon }.into()); } - self.storage_slot.set_address_slot(BEACON_SLOT, new_beacon); + StorageSlot::get_slot::(BEACON_SLOT).set(new_beacon); let beacon_implementation = - self.get_beacon_implementation(new_beacon)?; + Erc1967Utils::get_beacon_implementation(context, new_beacon)?; - if self.vm().code_size(beacon_implementation) == 0 { + if !beacon_implementation.has_code() { return Err(ERC1967InvalidImplementation { - implementation: beacon_implementation.into(), + implementation: beacon_implementation, } .into()); } @@ -323,11 +299,11 @@ impl Erc1967Utils { } impl Erc1967Utils { - fn get_beacon_implementation( - &mut self, + fn get_beacon_implementation( + context: &T, beacon: Address, ) -> Result { - IBeacon::new(beacon).implementation(Call::new_in(self)).map_err(|e| { + IBeaconInterface::new(beacon).implementation(context).map_err(|e| { panic!("TODO: handle error: {:?}", e); }) } diff --git a/contracts/src/proxy/mod.rs b/contracts/src/proxy/mod.rs index 017928d0c..7a470329d 100644 --- a/contracts/src/proxy/mod.rs +++ b/contracts/src/proxy/mod.rs @@ -1,43 +1,66 @@ +//! This is a low-level set of contracts implementing different proxy patterns +//! with and without upgradeability. +use alloc::vec::Vec; + use alloy_primitives::Address; -use stylus_sdk::ArbResult; +use stylus_sdk::{ + call::{self, Call, Error}, + prelude::*, +}; pub mod beacon; pub mod erc1967; -/** - * @dev This abstract contract provides a fallback function that delegates - * all calls to another contract using the EVM instruction `delegatecall`. - * We refer to the second contract as the _implementation_ behind the proxy, - * and it has to be specified by overriding the virtual {_implementation} - * function. - * - * Additionally, delegation to the implementation can be triggered manually - * through the {_fallback} function, or to a different contract through the - * {_delegate} function. - * - * The success and return data of the delegated call will be returned back - * to the caller of the proxy. - */ -pub trait IProxy { - /** - * @dev Delegates the current call to `implementation`. - * - * This function does not return to its internal call site, it will - * return directly to the external caller. - */ - fn delegate(&mut self, implementation: Address) -> ArbResult; +/// This trait provides a fallback function that delegates all calls to another +/// contract using the EVM instruction `delegatecall`. We refer to the second +/// contract as the _implementation_ behind the proxy, and it has to be +/// specified by overriding the virtual [`IProxy::implementation`] function. +/// +/// Additionally, delegation to the implementation can be triggered manually +/// through the [`IProxy::do_fallback`] function, or to a different contract +/// through the [`IProxy::delegate`] function. +/// +/// The success and return data of the delegated call will be returned back +/// to the caller of the proxy. +pub trait IProxy: TopLevelStorage + Sized { + /// Delegates the current call to [`IProxy::implementation`]. + /// + /// This function does not return to its internal call site, it will + /// return directly to the external caller. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `implementation` - The address of the implementation contract. + /// * `calldata` - The calldata to delegate to the implementation contract. + fn delegate( + &mut self, + implementation: Address, + calldata: &[u8], + ) -> Result, Error> { + unsafe { + call::delegate_call(Call::new_in(self), implementation, calldata) + } + } - /** - * @dev This is a virtual function that should be overridden so it - * returns the address to which the fallback function and - * {_fallback} should delegate. - */ - fn implementation(&self) -> Address; + /// This is a virtual function that should be overridden so it + /// returns the address to which the fallback function and + /// [`IProxy::do_fallback`] should delegate. + /// + /// # Arguments + /// + /// * `&self` - Read access to the contract's state. + fn implementation(&self) -> Result; - /** - * @dev Fallback function that delegates calls to the address returned - * by `_implementation()`. Will run if no other function in the - * contract matches the call data. - */ - fn do_fallback(&mut self) -> ArbResult; + /// Fallback function that delegates calls to the address returned + /// by [`IProxy::implementation`]. Will run if no other function in the + /// contract matches the call data. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `calldata` - The calldata to delegate to the implementation contract. + fn do_fallback(&mut self, calldata: &[u8]) -> Result, Error> { + self.delegate(self.implementation()?, calldata) + } } diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index d4887eb83..5302e55e1 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -1,5 +1,5 @@ //! Implementation of the ERC-2309 "Consecutive Transfer Extension" as defined -//! in the [ERC]. +//! in [ERC-2309]. //! //! This extension allows the minting large batches of tokens, during //! contract construction only. For upgradeable contracts, this implies that @@ -15,13 +15,7 @@ //! `max_batch_size` (used to restrict maximum batch size) can be assigned //! during construction. //! -//! IMPORTANT: Consecutive mint of [`Erc721Consecutive`] tokens is only allowed -//! inside the contract's Solidity constructor. -//! As opposed to the Solidity implementation of Consecutive, there is no -//! restriction on the [`Erc721Consecutive::_update`] function call since it is -//! not possible to call a Rust function from the Solidity constructor. -//! -//! [ERC]: https://eips.ethereum.org/EIPS/eip-2309 +//! [ERC-2309]: https://eips.ethereum.org/EIPS/eip-2309 use alloc::{vec, vec::Vec}; diff --git a/contracts/src/utils/address.rs b/contracts/src/utils/address.rs deleted file mode 100644 index bd1b1128f..000000000 --- a/contracts/src/utils/address.rs +++ /dev/null @@ -1,23 +0,0 @@ -use alloc::{vec, vec::Vec}; - -use alloy_primitives::Address; -use stylus_sdk::{abi::Bytes, prelude::*}; - -/// An [`AddressHelper`] error. -#[derive(SolidityError, Debug)] -pub enum Error {} - -/// State of an [`AddressHelper`] contract. -#[storage] -pub struct AddressHelper {} - -impl AddressHelper { - /// TODO: docs - pub fn function_delegate_call( - &self, - _target: Address, - _data: Bytes, - ) -> Result { - unimplemented!() - } -} diff --git a/contracts/src/utils/mod.rs b/contracts/src/utils/mod.rs index 8ab2343c3..0c33b908e 100644 --- a/contracts/src/utils/mod.rs +++ b/contracts/src/utils/mod.rs @@ -1,5 +1,4 @@ //! Common Smart Contracts utilities. -pub mod address; pub mod cryptography; pub mod introspection; pub mod math; diff --git a/contracts/src/utils/storage_slot.rs b/contracts/src/utils/storage_slot.rs index ccac70050..05fe229c8 100644 --- a/contracts/src/utils/storage_slot.rs +++ b/contracts/src/utils/storage_slot.rs @@ -1,31 +1,138 @@ -use alloc::{vec, vec::Vec}; +//! Helper for reading and writing primitive types to specific storage slots. +use alloy_primitives::U256; +use stylus_sdk::{host::VM, prelude::*}; -use alloy_primitives::{Address, B256}; -use stylus_sdk::prelude::*; +const SLOT_BYTE_SPACE: u8 = 32; -#[storage] -pub struct StorageSlot {} +/// Helper for reading and writing primitive types to specific storage slots. +/// +/// Storage slots are often used to avoid storage conflict when dealing with +/// upgradeable contracts. This library helps with reading and writing to such +/// slots without the need for low-level operations. +/// +/// The functions in this library return appropriate storage types that contain +/// a `value` member that can be used to read or write. +/// +/// Example usage to set ERC-1967 implementation slot: +/// +/// ```rust +/// const IMPLEMENTATION_SLOT: B256 = b256!("0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"); +/// +/// #[storage] +/// #[entrypoint] +/// pub struct Erc1967; +/// +/// #[public] +/// impl Erc1967 { +/// fn get_implementation(&self) -> Address { +/// return StorageSlot::get_slot::(IMPLEMENTATION_SLOT).get(); +/// } +/// +/// fn set_implementation(&mut self, new_implementation: Address) { +/// assert!(Address::has_code(new_implementation)); +/// StorageSlot::get_slot::(IMPLEMENTATION_SLOT).set(new_implementation); +/// } +/// } +/// ``` + +pub struct StorageSlot; -#[public] impl StorageSlot { - /// Returns an [`Address`] with member `value` located at `slot`. + /// Returns a [`StorageType`] located at `slot`. /// /// # Arguments /// - /// * `&self` - Read access to the contract's state. /// * `slot` - The slot to get the address from. - pub fn get_address_slot(&self, _slot: B256) -> Address { - unimplemented!() + pub fn get_slot(slot: U256) -> ST { + #[cfg(not(target_arch = "wasm32"))] + let host = + VM { host: alloc::boxed::Box::new(stylus_sdk::host::WasmVM {}) }; + #[cfg(target_arch = "wasm32")] + let host = VM(stylus_sdk::host::WasmVM {}); + + unsafe { ST::new(slot, SLOT_BYTE_SPACE - ST::SLOT_BYTES as u8, host) } } +} - /// TODO: docs - /// - /// # Arguments - /// - /// * `&mut self` - Write access to the contract's state. - /// * `slot` - The slot to set the address to. - /// * `value` - The address to set. - pub fn set_address_slot(&self, _slot: B256, _value: Address) { - unimplemented!() +#[cfg(test)] +mod tests { + use alloy_primitives::{uint, Address, U256}; + use motsu::prelude::*; + use stylus_sdk::storage::StorageAddress; + + use super::*; + + const IMPLEMENTATION_SLOT: U256 = uint!(12345_U256); + + #[storage] + #[entrypoint] + pub struct Erc1967 { + address: StorageAddress, + } + + #[public] + impl Erc1967 { + fn get_implementation(&self) -> Address { + StorageSlot::get_slot::(IMPLEMENTATION_SLOT).get() + } + + fn set_implementation(&self, new_implementation: Address) { + StorageSlot::get_slot::(IMPLEMENTATION_SLOT) + .set(new_implementation); + } + + fn get_address(&self) -> Address { + self.address.get() + } + + fn set_address(&mut self, new_address: Address) { + self.address.set(new_address); + } + + fn get_address_at_zero_slot(&self) -> Address { + StorageSlot::get_slot::(U256::ZERO).get() + } + + fn set_address_at_zero_slot(&mut self, new_address: Address) { + StorageSlot::get_slot::(U256::ZERO) + .set(new_address); + } + } + + #[motsu::test] + fn test_storage_slot( + contract: Contract, + alice: Address, + impl_address: Address, + ) { + let implementation = contract.sender(alice).get_implementation(); + assert_eq!(implementation, Address::ZERO); + + contract.sender(alice).set_implementation(impl_address); + + let implementation = contract.sender(alice).get_implementation(); + assert_eq!(implementation, impl_address); + let address = contract.sender(alice).get_address_at_zero_slot(); + assert_eq!(address, Address::ZERO); + let address = contract.sender(alice).get_address(); + assert_eq!(address, Address::ZERO); + + contract.sender(alice).set_address(impl_address); + + let implementation = contract.sender(alice).get_implementation(); + assert_eq!(implementation, impl_address); + let address = contract.sender(alice).get_address_at_zero_slot(); + assert_eq!(address, impl_address); + let address = contract.sender(alice).get_address(); + assert_eq!(address, impl_address); + + contract.sender(alice).set_address_at_zero_slot(alice); + + let implementation = contract.sender(alice).get_implementation(); + assert_eq!(implementation, impl_address); + let address = contract.sender(alice).get_address_at_zero_slot(); + assert_eq!(address, alice); + let address = contract.sender(alice).get_address(); + assert_eq!(address, alice); } } diff --git a/examples/erc1967/Cargo.toml b/examples/erc1967/Cargo.toml new file mode 100644 index 000000000..03a75b2dd --- /dev/null +++ b/examples/erc1967/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "erc1967-example" +edition.workspace = true +license.workspace = true +repository.workspace = true +publish = false +version.workspace = true + +[dependencies] +openzeppelin-stylus.workspace = true +alloy-primitives.workspace = true +alloy-sol-types.workspace = true +stylus-sdk.workspace = true + +[dev-dependencies] +alloy.workspace = true +e2e.workspace = true +tokio.workspace = true +eyre.workspace = true + +[lib] +crate-type = ["lib", "cdylib"] + +[features] +e2e = [] +export-abi = ["openzeppelin-stylus/export-abi"] + +[[bin]] +name = "erc1967-example" +path = "src/main.rs" diff --git a/examples/erc1967/src/lib.rs b/examples/erc1967/src/lib.rs new file mode 100644 index 000000000..d8057565c --- /dev/null +++ b/examples/erc1967/src/lib.rs @@ -0,0 +1,45 @@ +#![cfg_attr(not(any(test, feature = "export-abi")), no_main)] +extern crate alloc; + +use alloc::vec::Vec; + +use openzeppelin_stylus::proxy::{ + erc1967::{self, Erc1967Proxy}, + IProxy, +}; +use stylus_sdk::{ + abi::Bytes, alloy_primitives::Address, prelude::*, ArbResult, +}; + +#[entrypoint] +#[storage] +struct Erc1967Example { + erc1967: Erc1967Proxy, +} + +#[public] +impl Erc1967Example { + #[constructor] + pub fn constructor( + &mut self, + implementation: Address, + data: Bytes, + ) -> Result<(), erc1967::utils::Error> { + self.erc1967.constructor(implementation, data) + } + + fn implementation(&self) -> Result { + IProxy::implementation(self) + } + + #[fallback] + fn fallback(&mut self, calldata: &[u8]) -> ArbResult { + Ok(self.do_fallback(calldata)?) + } +} + +impl IProxy for Erc1967Example { + fn implementation(&self) -> Result { + self.erc1967.implementation() + } +} diff --git a/examples/erc1967/src/main.rs b/examples/erc1967/src/main.rs new file mode 100644 index 000000000..14d72fe7c --- /dev/null +++ b/examples/erc1967/src/main.rs @@ -0,0 +1,10 @@ +#![cfg_attr(not(any(test, feature = "export-abi")), no_main)] + +#[cfg(not(any(test, feature = "export-abi")))] +#[no_mangle] +pub extern "C" fn main() {} + +#[cfg(feature = "export-abi")] +fn main() { + erc1967_example::print_from_args(); +} diff --git a/examples/erc1967/tests/abi/mod.rs b/examples/erc1967/tests/abi/mod.rs new file mode 100644 index 000000000..7acab1cd9 --- /dev/null +++ b/examples/erc1967/tests/abi/mod.rs @@ -0,0 +1,34 @@ +#![allow(dead_code)] +use alloy::sol; + +sol!( + #[sol(rpc)] + contract Erc1967Example { + function implementation() public view returns (address implementation); + + // ERC20 functions that we want to delegate to the implementation. + function name() external view returns (string name); + function symbol() external view returns (string symbol); + function decimals() external view returns (uint8 decimals); + 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; + + // ERC20 errors that will be bubbled up to the caller. + 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); + + #[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/erc1967/tests/erc1967.rs b/examples/erc1967/tests/erc1967.rs new file mode 100644 index 000000000..a7b54bca7 --- /dev/null +++ b/examples/erc1967/tests/erc1967.rs @@ -0,0 +1,140 @@ +#![cfg(feature = "e2e")] + +use abi::Erc1967Example; +use alloy::{ + primitives::{Address, U256}, + sol_types::SolCall, +}; +use e2e::{ + constructor, receipt, send, watch, Account, Constructor, EventExt, Revert, +}; +use eyre::Result; +use mock::{erc20, erc20::ERC20Mock}; +use stylus_sdk::abi::Bytes; + +mod abi; +mod mock; + +fn ctr(implementation: Address, data: Bytes) -> Constructor { + constructor!(implementation, data.clone()) +} + +#[e2e::test] +async fn constructs(alice: Account) -> Result<()> { + let implementation_addr = erc20::deploy(&alice.wallet).await?; + let contract_addr = alice + .as_deployer() + .with_constructor(ctr(implementation_addr, vec![].into())) + .deploy() + .await? + .contract_address; + let contract = Erc1967Example::new(contract_addr, &alice.wallet); + + let implementation = contract.implementation().call().await?.implementation; + assert_eq!(implementation, implementation_addr); + + Ok(()) +} + +#[e2e::test] +async fn constructs_with_data(alice: Account) -> Result<()> { + let implementation_addr = erc20::deploy(&alice.wallet).await?; + + // mint 1000 tokens + let amount = U256::from(1000); + + let data = ERC20Mock::mintCall { account: alice.address(), value: amount }; + let data = data.abi_encode(); + + let contract_addr = alice + .as_deployer() + .with_constructor(ctr(implementation_addr, data.into())) + .deploy() + .await? + .contract_address; + let contract = Erc1967Example::new(contract_addr, &alice.wallet); + + let implementation = contract.implementation().call().await?.implementation; + assert_eq!(implementation, implementation_addr); + + // check that the balance can be accurately fetched through the proxy + let balance = contract.balanceOf(alice.address()).call().await?.balance; + assert_eq!(balance, amount); + + let total_supply = contract.totalSupply().call().await?.totalSupply; + assert_eq!(total_supply, amount); + + Ok(()) +} + +#[e2e::test] +async fn delegate(alice: Account, bob: Account) -> Result<()> { + let implementation_addr = erc20::deploy(&alice.wallet).await?; + let contract_addr = alice + .as_deployer() + .with_constructor(ctr(implementation_addr, vec![].into())) + .deploy() + .await? + .contract_address; + let contract = Erc1967Example::new(contract_addr, &alice.wallet); + + // verify initial balance is 0 + let balance = contract.balanceOf(alice.address()).call().await?.balance; + assert_eq!(balance, U256::ZERO); + + let total_supply = contract.totalSupply().call().await?.totalSupply; + assert_eq!(total_supply, U256::ZERO); + + // mint 1000 tokens + let amount = U256::from(1000); + watch!(contract.mint(alice.address(), amount))?; + + // check that the balance can be accurately fetched through the proxy + let balance = contract.balanceOf(alice.address()).call().await?.balance; + assert_eq!(balance, amount); + + let total_supply = contract.totalSupply().call().await?.totalSupply; + assert_eq!(total_supply, amount); + + // check that the balance can be transferred through the proxy + let receipt = receipt!(contract.transfer(bob.address(), amount))?; + + assert!(receipt.emits(Erc1967Example::Transfer { + from: alice.address(), + to: bob.address(), + value: amount, + })); + + let balance = contract.balanceOf(alice.address()).call().await?.balance; + assert_eq!(balance, U256::ZERO); + + let balance = contract.balanceOf(bob.address()).call().await?.balance; + assert_eq!(balance, amount); + + let total_supply = contract.totalSupply().call().await?.totalSupply; + assert_eq!(total_supply, amount); + + Ok(()) +} + +#[e2e::test] +async fn delegate_returns_error(alice: Account, bob: Account) -> Result<()> { + let implementation_addr = erc20::deploy(&alice.wallet).await?; + let contract_addr = alice + .as_deployer() + .with_constructor(ctr(implementation_addr, vec![].into())) + .deploy() + .await? + .contract_address; + let contract = Erc1967Example::new(contract_addr, &alice.wallet); + + let err = send!(contract.transfer(bob.address(), U256::from(1000))) + .expect_err("should revert"); + assert!(err.reverted_with(Erc1967Example::ERC20InsufficientBalance { + sender: alice.address(), + balance: U256::ZERO, + needed: U256::from(1000), + }),); + + Ok(()) +} diff --git a/examples/erc1967/tests/mock/erc20.rs b/examples/erc1967/tests/mock/erc20.rs new file mode 100644 index 000000000..68514daea --- /dev/null +++ b/examples/erc1967/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/erc1967/tests/mock/mod.rs b/examples/erc1967/tests/mock/mod.rs new file mode 100644 index 000000000..8f3777f6b --- /dev/null +++ b/examples/erc1967/tests/mock/mod.rs @@ -0,0 +1 @@ +pub mod erc20; diff --git a/examples/proxy/Cargo.toml b/examples/proxy/Cargo.toml new file mode 100644 index 000000000..e3fa2e18b --- /dev/null +++ b/examples/proxy/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "proxy-example" +edition.workspace = true +license.workspace = true +repository.workspace = true +publish = false +version.workspace = true + +[dependencies] +openzeppelin-stylus.workspace = true +alloy-primitives.workspace = true +alloy-sol-types.workspace = true +stylus-sdk.workspace = true + +[dev-dependencies] +alloy.workspace = true +e2e.workspace = true +tokio.workspace = true +eyre.workspace = true + +[lib] +crate-type = ["lib", "cdylib"] + +[features] +e2e = [] +export-abi = ["openzeppelin-stylus/export-abi"] + +[[bin]] +name = "proxy-example" +path = "src/main.rs" diff --git a/examples/proxy/src/lib.rs b/examples/proxy/src/lib.rs new file mode 100644 index 000000000..548bfc97d --- /dev/null +++ b/examples/proxy/src/lib.rs @@ -0,0 +1,38 @@ +#![cfg_attr(not(any(test, feature = "export-abi")), no_main)] +extern crate alloc; + +use alloc::vec::Vec; + +use openzeppelin_stylus::proxy::IProxy; +use stylus_sdk::{ + alloy_primitives::Address, prelude::*, storage::StorageAddress, ArbResult, +}; + +#[entrypoint] +#[storage] +struct ProxyExample { + implementation: StorageAddress, +} + +#[public] +impl ProxyExample { + #[constructor] + pub fn constructor(&mut self, implementation: Address) { + self.implementation.set(implementation); + } + + fn implementation(&self) -> Result { + IProxy::implementation(self) + } + + #[fallback] + fn fallback(&mut self, calldata: &[u8]) -> ArbResult { + Ok(self.do_fallback(calldata)?) + } +} + +impl IProxy for ProxyExample { + fn implementation(&self) -> Result { + Ok(self.implementation.get()) + } +} diff --git a/examples/proxy/src/main.rs b/examples/proxy/src/main.rs new file mode 100644 index 000000000..6b69692ea --- /dev/null +++ b/examples/proxy/src/main.rs @@ -0,0 +1,10 @@ +#![cfg_attr(not(any(test, feature = "export-abi")), no_main)] + +#[cfg(not(any(test, feature = "export-abi")))] +#[no_mangle] +pub extern "C" fn main() {} + +#[cfg(feature = "export-abi")] +fn main() { + proxy_example::print_from_args(); +} diff --git a/examples/proxy/tests/abi/mod.rs b/examples/proxy/tests/abi/mod.rs new file mode 100644 index 000000000..a8fb31d82 --- /dev/null +++ b/examples/proxy/tests/abi/mod.rs @@ -0,0 +1,34 @@ +#![allow(dead_code)] +use alloy::sol; + +sol!( + #[sol(rpc)] + contract ProxyExample { + function implementation() public view returns (address implementation); + + // ERC20 functions that we want to delegate to the implementation. + function name() external view returns (string name); + function symbol() external view returns (string symbol); + function decimals() external view returns (uint8 decimals); + 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; + + // ERC20 errors that will be bubbled up to the caller. + 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); + + #[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/proxy/tests/mock/erc20.rs b/examples/proxy/tests/mock/erc20.rs new file mode 100644 index 000000000..68514daea --- /dev/null +++ b/examples/proxy/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/proxy/tests/mock/mod.rs b/examples/proxy/tests/mock/mod.rs new file mode 100644 index 000000000..8f3777f6b --- /dev/null +++ b/examples/proxy/tests/mock/mod.rs @@ -0,0 +1 @@ +pub mod erc20; diff --git a/examples/proxy/tests/proxy.rs b/examples/proxy/tests/proxy.rs new file mode 100644 index 000000000..963465f29 --- /dev/null +++ b/examples/proxy/tests/proxy.rs @@ -0,0 +1,105 @@ +#![cfg(feature = "e2e")] + +use abi::ProxyExample; +use alloy::primitives::{Address, U256}; +use e2e::{ + constructor, receipt, send, watch, Account, Constructor, EventExt, Revert, +}; +use eyre::Result; +use mock::erc20; + +mod abi; +mod mock; + +fn ctr(implementation: Address) -> Constructor { + constructor!(implementation) +} + +#[e2e::test] +async fn constructs(alice: Account) -> Result<()> { + let implementation_addr = erc20::deploy(&alice.wallet).await?; + let contract_addr = alice + .as_deployer() + .with_constructor(ctr(implementation_addr)) + .deploy() + .await? + .contract_address; + let contract = ProxyExample::new(contract_addr, &alice.wallet); + + let implementation = contract.implementation().call().await?.implementation; + assert_eq!(implementation, implementation_addr); + + Ok(()) +} + +#[e2e::test] +async fn delegate(alice: Account, bob: Account) -> Result<()> { + let implementation_addr = erc20::deploy(&alice.wallet).await?; + let contract_addr = alice + .as_deployer() + .with_constructor(ctr(implementation_addr)) + .deploy() + .await? + .contract_address; + let contract = ProxyExample::new(contract_addr, &alice.wallet); + + // verify initial balance is 0 + let balance = contract.balanceOf(alice.address()).call().await?.balance; + assert_eq!(balance, U256::ZERO); + + let total_supply = contract.totalSupply().call().await?.totalSupply; + assert_eq!(total_supply, U256::ZERO); + + // mint 1000 tokens + let amount = U256::from(1000); + watch!(contract.mint(alice.address(), amount))?; + + // check that the balance can be accurately fetched through the proxy + let balance = contract.balanceOf(alice.address()).call().await?.balance; + assert_eq!(balance, amount); + + let total_supply = contract.totalSupply().call().await?.totalSupply; + assert_eq!(total_supply, amount); + + // check that the balance can be transferred through the proxy + let receipt = receipt!(contract.transfer(bob.address(), amount))?; + + assert!(receipt.emits(ProxyExample::Transfer { + from: alice.address(), + to: bob.address(), + value: amount, + })); + + let balance = contract.balanceOf(alice.address()).call().await?.balance; + assert_eq!(balance, U256::ZERO); + + let balance = contract.balanceOf(bob.address()).call().await?.balance; + assert_eq!(balance, amount); + + let total_supply = contract.totalSupply().call().await?.totalSupply; + assert_eq!(total_supply, amount); + + Ok(()) +} + +#[e2e::test] +async fn delegate_returns_error(alice: Account, bob: Account) -> Result<()> { + let implementation_addr = erc20::deploy(&alice.wallet).await?; + let contract_addr = alice + .as_deployer() + .with_constructor(ctr(implementation_addr)) + .deploy() + .await? + .contract_address; + let contract = ProxyExample::new(contract_addr, &alice.wallet); + + let err = send!(contract.transfer(bob.address(), U256::from(1000))) + .expect_err("should revert"); + assert!(err.reverted_with(ProxyExample::ERC20InsufficientBalance { + sender: alice.address(), + balance: U256::ZERO, + needed: U256::from(1000), + }),); + + Ok(()) +} diff --git a/lib/e2e/src/constructor_macro.rs b/lib/e2e/src/constructor_macro.rs index 29f09e82f..48e62b4d0 100644 --- a/lib/e2e/src/constructor_macro.rs +++ b/lib/e2e/src/constructor_macro.rs @@ -1,3 +1,5 @@ +use alloy::primitives::{Address, U256, U8}; + /// Constructor data. pub struct Constructor { /// Constructor signature. @@ -6,6 +8,31 @@ pub struct Constructor { pub args: Vec, } +/// Helper trait to convert values to string representation +pub trait AbiTypeToString { + /// Stringify ABI type. + fn abi_type_to_string(&self) -> String; +} + +macro_rules! impl_to_arg_string { + ($($abi_type:ident),* $(,)?) => {$( + impl AbiTypeToString for $abi_type { + fn abi_type_to_string(&self) -> String { + self.to_string() + } + } + )*}; +} + +impl_to_arg_string!(U256, u64, String, U8, Address); + +// Special implementation for Bytes +impl AbiTypeToString for stylus_sdk::abi::Bytes { + fn abi_type_to_string(&self) -> String { + format!("0x{}", stylus_sdk::hex::encode(self)) + } +} + /// Generates a function selector for the given method and its args. #[macro_export] macro_rules! constructor { @@ -27,7 +54,7 @@ macro_rules! constructor { params.join(",") }; - let args = vec![$first.to_string()$(, $rest.to_string())*]; + let args = vec![$crate::AbiTypeToString::abi_type_to_string(&$first)$(, $crate::AbiTypeToString::abi_type_to_string(&$rest))*]; $crate::Constructor { signature: format!("constructor({})", signature_params), diff --git a/lib/e2e/src/deploy.rs b/lib/e2e/src/deploy.rs index cc92cd427..9525ea218 100644 --- a/lib/e2e/src/deploy.rs +++ b/lib/e2e/src/deploy.rs @@ -86,7 +86,7 @@ impl Deployer { Self { rpc_url, private_key, ctor: None } } - /// Add solidity constructor to the deployer. + /// Sets the constructor to be used during contract deployment. #[allow(clippy::needless_pass_by_value)] pub fn with_constructor(mut self, ctor: Constructor) -> Deployer { self.ctor = Some(ctor); diff --git a/lib/e2e/src/lib.rs b/lib/e2e/src/lib.rs index 65ec68bf3..1b030c908 100644 --- a/lib/e2e/src/lib.rs +++ b/lib/e2e/src/lib.rs @@ -10,7 +10,7 @@ mod receipt; mod system; pub use account::Account; -pub use constructor_macro::Constructor; +pub use constructor_macro::{AbiTypeToString, Constructor}; pub use deploy::{ContractDeploymentError, ContractInitializationError}; pub use e2e_proc::test; pub use error::{Panic, PanicCode, Revert};