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};