diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 50e03b27..21176ef8 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -39,7 +39,7 @@ jobs: cache-on-failure: true - name: install cargo-stylus - run: cargo install cargo-stylus cargo-stylus-check + run: cargo install cargo-stylus@0.4.1 cargo-stylus-check@0.4.1 - name: install solc run: | diff --git a/Cargo.lock b/Cargo.lock index 7b730dad..ab2ae278 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1597,6 +1597,23 @@ dependencies = [ "tokio", ] +[[package]] +name = "erc721-consecutive-example" +version = "0.0.0" +dependencies = [ + "alloy", + "alloy-primitives 0.3.3", + "alloy-sol-types 0.3.1", + "e2e", + "eyre", + "mini-alloc", + "openzeppelin-stylus", + "rand", + "stylus-proc", + "stylus-sdk", + "tokio", +] + [[package]] name = "erc721-example" version = "0.0.0" @@ -2224,9 +2241,9 @@ checksum = "57d8d8ce877200136358e0bbff3a77965875db3af755a11e1fa6b1b3e2df13ea" [[package]] name = "koba" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31de3702e0ac9b1f6927d12a8157af9c5796bc1caa9e89e43bda20b98af6685" +checksum = "80e92e1148d087df999396266311bece9e5311c352821872cccaf5dc67117cfc" dependencies = [ "alloy", "brotli2", diff --git a/Cargo.toml b/Cargo.toml index fe95030b..5e0d14f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "examples/erc20", "examples/erc20-permit", "examples/erc721", + "examples/erc721-consecutive", "examples/erc721-metadata", "examples/merkle-proofs", "examples/ownable", @@ -27,6 +28,7 @@ default-members = [ "examples/erc20", "examples/erc20-permit", "examples/erc721", + "examples/erc721-consecutive", "examples/erc721-metadata", "examples/merkle-proofs", "examples/ownable", @@ -81,7 +83,7 @@ alloy-sol-types = { version = "0.3.1", default-features = false } const-hex = { version = "1.11.1", default-features = false } eyre = "0.6.8" keccak-const = "0.2.0" -koba = "0.1.2" +koba = "0.2.0" once_cell = "1.19.0" rand = "0.8.5" regex = "1.10.4" diff --git a/benches/src/lib.rs b/benches/src/lib.rs index b9f27b35..85a37ca0 100644 --- a/benches/src/lib.rs +++ b/benches/src/lib.rs @@ -6,7 +6,7 @@ use alloy::{ }, }; use alloy_primitives::U128; -use e2e::Account; +use e2e::{Account, ReceiptExt}; use koba::config::{Deploy, Generate, PrivateKey}; use serde::Deserialize; @@ -77,5 +77,9 @@ async fn deploy( quiet: true, }; - koba::deploy(&config).await.expect("should deploy contract") + koba::deploy(&config) + .await + .expect("should deploy contract") + .address() + .expect("should return contract address") } diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs new file mode 100644 index 00000000..bfdc5c78 --- /dev/null +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -0,0 +1,1011 @@ +//! Implementation of the ERC-2309 "Consecutive Transfer Extension" as defined +//! in https://eips.ethereum.org/EIPS/eip-2309[ERC-2309]. +//! +//! This extension allows the minting large batches of tokens, during +//! contract construction only. For upgradeable contracts, this implies that +//! batch minting is only available during proxy deployment, and not in +//! subsequent upgrades. These batches are limited to 5000 tokens at a time by +//! default to accommodate off-chain indexers. +//! +//! Using this extension removes the ability to mint single tokens during +//! contract construction. This ability is regained after construction. During +//! construction, only batch minting is allowed. +//! +//! Fields `_first_consecutive_id` (used to offset first token id) and +//! `_max_batch_size` (used to restrict maximum batch size) can be assigned +//! during construction with `koba` (stylus construction tooling) within +//! solidity constructor file. +//! +//! 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. + +use alloc::vec; + +use alloy_primitives::{uint, Address, U256}; +use alloy_sol_types::sol; +use stylus_proc::{external, sol_storage, SolidityError}; +use stylus_sdk::{ + abi::Bytes, call::MethodError, evm, msg, prelude::TopLevelStorage, +}; + +use crate::{ + token::{ + erc721, + erc721::{ + Approval, ERC721IncorrectOwner, ERC721InvalidApprover, + ERC721InvalidReceiver, ERC721InvalidSender, ERC721NonexistentToken, + Erc721, IErc721, Transfer, + }, + }, + utils::{ + math::storage::{AddAssignUnchecked, SubAssignUnchecked}, + structs::{ + bitmap::BitMap, + checkpoints, + checkpoints::{Trace160, U96}, + }, + }, +}; + +sol_storage! { + /// State of an [`Erc721Consecutive`] token. + pub struct Erc721Consecutive { + /// Erc721 contract storage. + Erc721 erc721; + /// Checkpoint library contract for sequential ownership. + Trace160 _sequential_ownership; + /// BitMap library contract for sequential burn of tokens. + BitMap _sequential_burn; + /// Used to offset the first token id in + /// [`Erc721Consecutive::_next_consecutive_id`]. + uint96 _first_consecutive_id; + /// Maximum size of a batch of consecutive tokens. This is designed to limit + /// stress on off-chain indexing services that have to record one entry per + /// token, and have protections against "unreasonably large" batches of + /// tokens. + uint96 _max_batch_size; + } +} + +sol! { + /// Emitted when the tokens from `from_token_id` to `to_token_id` are transferred from `from_address` to `to_address`. + /// + /// * `from_token_id` - First token being transferred. + /// * `to_token_id` - Last token being transferred. + /// * `from_address` - Address from which tokens will be transferred. + /// * `to_address` - Address where the tokens will be transferred to. + event ConsecutiveTransfer( + uint256 indexed from_token_id, + uint256 to_token_id, + address indexed from_address, + address indexed to_address + ); +} + +sol! { + /// Batch mint is restricted to the constructor. + /// Any batch mint not emitting the [`Transfer`] event outside of the constructor + /// is non ERC-721 compliant. + #[derive(Debug)] + #[allow(missing_docs)] + error ERC721ForbiddenBatchMint(); + + /// Exceeds the max number of mints per batch. + #[derive(Debug)] + #[allow(missing_docs)] + error ERC721ExceededMaxBatchMint(uint256 batch_size, uint256 max_batch); + + /// Individual minting is not allowed. + #[derive(Debug)] + #[allow(missing_docs)] + error ERC721ForbiddenMint(); + + /// Batch burn is not supported. + #[derive(Debug)] + #[allow(missing_docs)] + error ERC721ForbiddenBatchBurn(); +} + +/// An [`Erc721Consecutive`] error. +#[derive(SolidityError, Debug)] +pub enum Error { + /// Error type from [`Erc721`] contract [`erc721::Error`]. + Erc721(erc721::Error), + /// Error type from checkpoint contract [`checkpoints::Error`]. + Checkpoints(checkpoints::Error), + /// Batch mint is restricted to the constructor. + /// Any batch mint not emitting the [`Transfer`] event outside of + /// the constructor is non ERC-721 compliant. + ForbiddenBatchMint(ERC721ForbiddenBatchMint), + /// Exceeds the max amount of mints per batch. + ExceededMaxBatchMint(ERC721ExceededMaxBatchMint), + /// Individual minting is not allowed. + ForbiddenMint(ERC721ForbiddenMint), + /// Batch burn is not supported. + ForbiddenBatchBurn(ERC721ForbiddenBatchBurn), +} + +unsafe impl TopLevelStorage for Erc721Consecutive {} + +impl MethodError for erc721::Error { + fn encode(self) -> alloc::vec::Vec { + self.into() + } +} + +impl MethodError for checkpoints::Error { + fn encode(self) -> alloc::vec::Vec { + self.into() + } +} + +// ************** ERC-721 External ************** + +#[external] +impl IErc721 for Erc721Consecutive { + type Error = Error; + + fn balance_of(&self, owner: Address) -> Result { + Ok(self.erc721.balance_of(owner)?) + } + + fn owner_of(&self, token_id: U256) -> Result { + self._require_owned(token_id) + } + + fn safe_transfer_from( + &mut self, + from: Address, + to: Address, + token_id: U256, + ) -> Result<(), Error> { + // TODO: Once the SDK supports the conversion, + // use alloy_primitives::bytes!("") here. + self.safe_transfer_from_with_data(from, to, token_id, vec![].into()) + } + + #[selector(name = "safeTransferFrom")] + fn safe_transfer_from_with_data( + &mut self, + from: Address, + to: Address, + token_id: U256, + data: Bytes, + ) -> Result<(), Error> { + self.transfer_from(from, to, token_id)?; + Ok(self.erc721._check_on_erc721_received( + msg::sender(), + from, + to, + token_id, + &data, + )?) + } + + fn transfer_from( + &mut self, + from: Address, + to: Address, + token_id: U256, + ) -> Result<(), Error> { + if to.is_zero() { + return Err(erc721::Error::InvalidReceiver( + ERC721InvalidReceiver { receiver: Address::ZERO }, + ) + .into()); + } + + // Setting an "auth" argument enables the `_is_authorized` check which + // verifies that the token exists (`!from.is_zero()`). Therefore, it is + // not needed to verify that the return value is not 0 here. + let previous_owner = self._update(to, token_id, msg::sender())?; + if previous_owner != from { + return Err(erc721::Error::IncorrectOwner(ERC721IncorrectOwner { + sender: from, + token_id, + owner: previous_owner, + }) + .into()); + } + Ok(()) + } + + fn approve(&mut self, to: Address, token_id: U256) -> Result<(), Error> { + self._approve(to, token_id, msg::sender(), true) + } + + fn set_approval_for_all( + &mut self, + operator: Address, + approved: bool, + ) -> Result<(), Error> { + Ok(self.erc721.set_approval_for_all(operator, approved)?) + } + + fn get_approved(&self, token_id: U256) -> Result { + self._require_owned(token_id)?; + Ok(self.erc721._get_approved(token_id)) + } + + fn is_approved_for_all(&self, owner: Address, operator: Address) -> bool { + self.erc721.is_approved_for_all(owner, operator) + } +} + +// ************** Consecutive ************** + +impl Erc721Consecutive { + /// Override of [`Erc721::_owner_of`] that checks the sequential + /// ownership structure for tokens that have been minted as part of a + /// batch, and not yet transferred. + /// + /// # Arguments + /// + /// * `&self` - Read access to the contract's state. + /// * `token_id` - Token id as a number. + pub fn _owner_of(&self, token_id: U256) -> Address { + let owner = self.erc721._owner_of(token_id); + // If token is owned by the core, or beyond consecutive range, return + // base value. + if !owner.is_zero() + || token_id < U256::from(self._first_consecutive_id()) + || token_id > U256::from(U96::MAX) + { + return owner; + } + + // Otherwise, check the token was not burned, and fetch ownership from + // the anchors. + if self._sequential_burn.get(token_id) { + Address::ZERO + } else { + // NOTE: Bounds already checked. No need for safe cast of token_id + self._sequential_ownership.lower_lookup(U96::from(token_id)).into() + } + } + + /// Mint a batch of tokens with length `batch_size` for `to`. + /// Returns the token id of the first token minted in the batch; if + /// `batch_size` is 0, returns the number of consecutive ids minted so + /// far. + /// + /// Requirements: + /// + /// * `batch_size` must not be greater than + /// [`Erc721Consecutive::_max_batch_size`]. + /// * The function is called in the constructor of the contract (directly or + /// indirectly). + /// + /// CAUTION: Does not emit a [Transfer] event. + /// This is ERC-721 compliant as + /// long as it is done inside of the constructor, which is enforced by + /// this function. + /// + /// CAUTION: Does not invoke + /// [`erc721::IERC721Receiver::on_erc_721_received`] on the receiver. + /// + /// # Arguments + /// + /// * `&self` - Write access to the contract's state. + /// * `token_id` - Token id as a number. + /// + /// # Errors + /// + /// If `to` is `Address::ZERO`, then the error + /// [`erc721::Error::InvalidReceiver`] is returned. + /// If `batch_size` exceeds [`Erc721Consecutive::_max_batch_size`], + /// then the error [`Error::ExceededMaxBatchMint`] is returned. + /// + /// # Events + /// + /// Emits a [`ConsecutiveTransfer`] event. + #[cfg(all(test, feature = "std"))] + fn _mint_consecutive( + &mut self, + to: Address, + batch_size: U96, + ) -> Result { + let next = self._next_consecutive_id(); + + // Minting a batch of size 0 is a no-op. + if batch_size > U96::ZERO { + if to.is_zero() { + return Err(erc721::Error::InvalidReceiver( + ERC721InvalidReceiver { receiver: Address::ZERO }, + ) + .into()); + } + + if batch_size > self._max_batch_size() { + return Err(ERC721ExceededMaxBatchMint { + batch_size: U256::from(batch_size), + max_batch: U256::from(self._max_batch_size()), + } + .into()); + } + + // Push an ownership checkpoint & emit event. + let last = next + batch_size - uint!(1_U96); + self._sequential_ownership.push(last, to.into())?; + + // The invariant required by this function is preserved because the + // new sequential_ownership checkpoint is attributing + // ownership of `batch_size` new tokens to account `to`. + self.erc721._increase_balance( + to, + alloy_primitives::U128::from(batch_size), + ); + + evm::log(ConsecutiveTransfer { + from_token_id: next.to::(), + to_token_id: last.to::(), + from_address: Address::ZERO, + to_address: to, + }); + }; + Ok(next) + } + + /// Override of [`Erc721::_update`] that restricts normal minting to after + /// construction. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `to` - Account of the recipient. + /// * `token_id` - Token id as a number. + /// * `auth` - Account used for authorization of the update. + /// + /// # Errors + /// + /// If token does not exist and `auth` is not `Address::ZERO`, then the + /// error [`erc721::Error::NonexistentToken`] is returned. + /// If `auth` is not `Address::ZERO` and `auth` does not have a right to + /// approve this token, then the error + /// [`erc721::Error::InsufficientApproval`] is returned. + /// + /// # Events + /// + /// Emits a [`Transfer`] event. + pub fn _update( + &mut self, + to: Address, + token_id: U256, + auth: Address, + ) -> Result { + let previous_owner = self._update_base(to, token_id, auth)?; + + // if we burn + if to.is_zero() + // and the token_id was minted in a batch + && token_id < U256::from(self._next_consecutive_id()) + // and the token was never marked as burnt + && !self._sequential_burn.get(token_id) + { + // record burn + self._sequential_burn.set(token_id); + } + + Ok(previous_owner) + } + + /// Returns the next token_id to mint using [`Self::_mint_consecutive`]. It + /// will return [`Erc721Consecutive::_first_consecutive_id`] if no + /// consecutive token_id has been minted before. + /// + /// # Arguments + /// + /// * `&self` - Read access to the contract's state. + fn _next_consecutive_id(&self) -> U96 { + match self._sequential_ownership.latest_checkpoint() { + None => self._first_consecutive_id(), + Some((latest_id, _)) => latest_id + uint!(1_U96), + } + } + + /// Used to offset the first token id in + /// [`Erc721Consecutive::_next_consecutive_id`]. + fn _first_consecutive_id(&self) -> U96 { + self._first_consecutive_id.get() + } + + /// Maximum size of consecutive token's batch. + /// This is designed to limit stress on off-chain indexing services that + /// have to record one entry per token, and have protections against + /// "unreasonably large" batches of tokens. + fn _max_batch_size(&self) -> U96 { + self._max_batch_size.get() + } +} + +// ************** ERC-721 Internal ************** + +impl Erc721Consecutive { + /// Transfers `token_id` from its current owner to `to`, or alternatively + /// mints (or burns) if the current owner (or `to`) is the `Address::ZERO`. + /// Returns the owner of the `token_id` before the update. + /// + /// The `auth` argument is optional. If the value passed is non-zero, then + /// this function will check that `auth` is either the owner of the + /// token, or approved to operate on the token (by the owner). + /// + /// NOTE: If overriding this function in a way that tracks balances, see + /// also [`Erc721::_increase_balance`]. + fn _update_base( + &mut self, + to: Address, + token_id: U256, + auth: Address, + ) -> Result { + let from = self._owner_of(token_id); + + // Perform (optional) operator check. + if !auth.is_zero() { + self.erc721._check_authorized(from, auth, token_id)?; + } + + // Execute the update. + if !from.is_zero() { + // Clear approval. No need to re-authorize or emit the `Approval` + // event. + self._approve(Address::ZERO, token_id, Address::ZERO, false)?; + self.erc721 + ._balances + .setter(from) + .sub_assign_unchecked(uint!(1_U256)); + } + + if !to.is_zero() { + self.erc721 + ._balances + .setter(to) + .add_assign_unchecked(uint!(1_U256)); + } + + self.erc721._owners.setter(token_id).set(to); + evm::log(Transfer { from, to, token_id }); + Ok(from) + } + + /// Mints `token_id` and transfers it to `to`. + /// + /// WARNING: Usage of this method is discouraged, use [`Self::_safe_mint`] + /// whenever possible. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `to` - Account of the recipient. + /// * `token_id` - Token id as a number. + /// + /// # Errors + /// + /// If `token_id` already exists, then the error + /// [`erc721::Error::InvalidSender`] is returned. + /// If `to` is `Address::ZERO`, then the error + /// [`erc721::Error::InvalidReceiver`] is returned. + /// + /// # Requirements: + /// + /// * `token_id` must not exist. + /// * `to` cannot be `Address::ZERO`. + /// + /// # Events + /// + /// Emits a [`Transfer`] event. + pub fn _mint(&mut self, to: Address, token_id: U256) -> Result<(), Error> { + if to.is_zero() { + return Err(erc721::Error::InvalidReceiver( + ERC721InvalidReceiver { receiver: Address::ZERO }, + ) + .into()); + } + + let previous_owner = self._update(to, token_id, Address::ZERO)?; + if !previous_owner.is_zero() { + return Err(erc721::Error::InvalidSender(ERC721InvalidSender { + sender: Address::ZERO, + }) + .into()); + } + Ok(()) + } + + /// Mints `token_id`, transfers it to `to`, + /// and checks for `to`'s acceptance. + /// + /// An additional `data` parameter is forwarded to + /// [`erc721::IERC721Receiver::on_erc_721_received`] to contract recipients. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `to` - Account of the recipient. + /// * `token_id` - Token id as a number. + /// * `data` - Additional data with no specified format, sent in the call to + /// [`Erc721::_check_on_erc721_received`]. + /// + /// # Errors + /// + /// If `token_id` already exists, then the error + /// [`erc721::Error::InvalidSender`] is returned. + /// If `to` is `Address::ZERO`, then the error + /// [`erc721::Error::InvalidReceiver`] is returned. + /// If [`erc721::IERC721Receiver::on_erc_721_received`] hasn't returned its + /// interface id or returned with error, then the error + /// [`erc721::Error::InvalidReceiver`] is returned. + /// + /// # Requirements: + /// + /// * `token_id` must not exist. + /// * If `to` refers to a smart contract, it must implement + /// [`erc721::IERC721Receiver::on_erc_721_received`], which is called upon + /// a `safe_transfer`. + /// + /// # Events + /// + /// Emits a [`Transfer`] event. + pub fn _safe_mint( + &mut self, + to: Address, + token_id: U256, + data: Bytes, + ) -> Result<(), Error> { + self._mint(to, token_id)?; + Ok(self.erc721._check_on_erc721_received( + msg::sender(), + Address::ZERO, + to, + token_id, + &data, + )?) + } + + /// Destroys `token_id`. + /// + /// The approval is cleared when the token is burned. This is an + /// internal function that does not check if the sender is authorized + /// to operate on the token. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `token_id` - Token id as a number. + /// + /// # Errors + /// + /// If token does not exist, then the error + /// [`erc721::Error::NonexistentToken`] is returned. + /// + /// # Requirements: + /// + /// * `token_id` must exist. + /// + /// # Events + /// + /// Emits a [`Transfer`] event. + pub fn _burn(&mut self, token_id: U256) -> Result<(), Error> { + let previous_owner = + self._update(Address::ZERO, token_id, Address::ZERO)?; + if previous_owner.is_zero() { + return Err(erc721::Error::NonexistentToken( + ERC721NonexistentToken { token_id }, + ) + .into()); + } + Ok(()) + } + + /// Transfers `token_id` from `from` to `to`. + /// + /// As opposed to [`Self::transfer_from`], this imposes no restrictions on + /// `msg::sender`. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `from` - Account of the sender. + /// * `to` - Account of the recipient. + /// * `token_id` - Token id as a number. + /// + /// # Errors + /// + /// If `to` is `Address::ZERO`, then the error + /// [`erc721::Error::InvalidReceiver`] is returned. + /// If `token_id` does not exist, then the error + /// [`erc721::Error::NonexistentToken`] is returned. + /// If the previous owner is not `from`, then the error + /// [`erc721::Error::IncorrectOwner`] is returned. + /// + /// # Requirements: + /// + /// * `to` cannot be `Address::ZERO`. + /// * The `token_id` token must be owned by `from`. + /// + /// # Events + /// + /// Emits a [`Transfer`] event. + pub fn _transfer( + &mut self, + from: Address, + to: Address, + token_id: U256, + ) -> Result<(), Error> { + if to.is_zero() { + return Err(erc721::Error::InvalidReceiver( + ERC721InvalidReceiver { receiver: Address::ZERO }, + ) + .into()); + } + + let previous_owner = self._update(to, token_id, Address::ZERO)?; + if previous_owner.is_zero() { + return Err(erc721::Error::NonexistentToken( + ERC721NonexistentToken { token_id }, + ) + .into()); + } else if previous_owner != from { + return Err(erc721::Error::IncorrectOwner(ERC721IncorrectOwner { + sender: from, + token_id, + owner: previous_owner, + }) + .into()); + } + + Ok(()) + } + + /// Safely transfers `token_id` token from `from` to `to`, checking that + /// contract recipients are aware of the [`Erc721`] standard to prevent + /// tokens from being forever locked. + /// + /// `data` is additional data, it has + /// no specified format and it is sent in call to `to`. This internal + /// function is like [`Self::safe_transfer_from`] in the sense that it + /// invokes [`erc721::IERC721Receiver::on_erc_721_received`] on the + /// receiver, and can be used to e.g. implement alternative mechanisms + /// to perform token transfer, such as signature-based. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `from` - Account of the sender. + /// * `to` - Account of the recipient. + /// * `token_id` - Token id as a number. + /// * `data` - Additional data with no specified format, sent in the call to + /// [`Erc721::_check_on_erc721_received`]. + /// + /// # Errors + /// + /// If `to` is `Address::ZERO`, then the error + /// [`erc721::Error::InvalidReceiver`] is returned. + /// If `token_id` does not exist, then the error + /// [`erc721::Error::NonexistentToken`] is returned. + /// If the previous owner is not `from`, then the error + /// [`erc721::Error::IncorrectOwner`] is returned. + /// + /// # Requirements: + /// + /// * The `token_id` token must exist and be owned by `from`. + /// * `to` cannot be `Address::ZERO`. + /// * `from` cannot be `Address::ZERO`. + /// * If `to` refers to a smart contract, it must implement + /// [`erc721::IERC721Receiver::on_erc_721_received`], which is called upon + /// a `safe_transfer`. + /// + /// # Events + /// + /// Emits a [`Transfer`] event. + pub fn _safe_transfer( + &mut self, + from: Address, + to: Address, + token_id: U256, + data: Bytes, + ) -> Result<(), Error> { + self._transfer(from, to, token_id)?; + Ok(self.erc721._check_on_erc721_received( + msg::sender(), + from, + to, + token_id, + &data, + )?) + } + + /// Approve `to` to operate on `token_id`. + /// + /// The `auth` argument is optional. If the value passed is non + /// `Address::ZERO`, then this function will check that `auth` is either + /// the owner of the token, or approved to operate on all tokens held by + /// this owner. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `to` - Account of the recipient. + /// * `token_id` - Token id as a number. + /// * `auth` - Account used for authorization of the update. + /// * `emit_event` - Emit an [`Approval`] event flag. + /// + /// # Errors + /// + /// If the token does not exist, then the error + /// [`erc721::Error::NonexistentToken`] is returned. + /// If `auth` does not have a right to approve this token, then the error + /// [`erc721::Error::InvalidApprover`] is returned. + /// + /// # Events + /// + /// Emits an [`Approval`] event. + pub fn _approve( + &mut self, + to: Address, + token_id: U256, + auth: Address, + emit_event: bool, + ) -> Result<(), Error> { + // Avoid reading the owner unless necessary. + if emit_event || !auth.is_zero() { + let owner = self._require_owned(token_id)?; + + // We do not use [`Self::_is_authorized`] because single-token + // approvals should not be able to call `approve`. + if !auth.is_zero() + && owner != auth + && !self.is_approved_for_all(owner, auth) + { + return Err(erc721::Error::InvalidApprover( + ERC721InvalidApprover { approver: auth }, + ) + .into()); + } + + if emit_event { + evm::log(Approval { owner, approved: to, token_id }); + } + } + + self.erc721._token_approvals.setter(token_id).set(to); + Ok(()) + } + + /// Reverts if the `token_id` doesn't have a current owner (it hasn't been + /// minted, or it has been burned). Returns the owner. + /// + /// Overrides to ownership logic should be done to + /// [`Self::_owner_of`]. + /// + /// # Errors + /// + /// If token does not exist, then the error + /// [`erc721::Error::NonexistentToken`] is returned. + /// + /// # Arguments + /// + /// * `&self` - Read access to the contract's state. + /// * `token_id` - Token id as a number. + pub fn _require_owned(&self, token_id: U256) -> Result { + let owner = self._owner_of(token_id); + if owner.is_zero() { + return Err(erc721::Error::NonexistentToken( + ERC721NonexistentToken { token_id }, + ) + .into()); + } + Ok(owner) + } +} + +#[cfg(all(test, feature = "std"))] +mod tests { + use alloy_primitives::{address, uint, Address, U256}; + use stylus_sdk::msg; + + use crate::{ + token::{ + erc721, + erc721::{ + extensions::consecutive::{ + ERC721ExceededMaxBatchMint, Erc721Consecutive, Error, + }, + tests::random_token_id, + ERC721InvalidReceiver, ERC721NonexistentToken, IErc721, + }, + }, + utils::structs::checkpoints::U96, + }; + + const BOB: Address = address!("F4EaCDAbEf3c8f1EdE91b6f2A6840bc2E4DD3526"); + + fn init( + contract: &mut Erc721Consecutive, + receivers: Vec
, + batches: Vec, + ) -> Vec { + contract._first_consecutive_id.set(uint!(0_U96)); + contract._max_batch_size.set(uint!(5000_U96)); + receivers + .into_iter() + .zip(batches) + .map(|(to, batch_size)| { + contract + ._mint_consecutive(to, batch_size) + .expect("should mint consecutively") + }) + .collect() + } + + #[motsu::test] + fn mints(contract: Erc721Consecutive) { + let alice = msg::sender(); + + let initial_balance = contract + .balance_of(alice) + .expect("should return the balance of Alice"); + + let init_tokens_count = uint!(10_U96); + init(contract, vec![alice], vec![init_tokens_count]); + + let balance1 = contract + .balance_of(alice) + .expect("should return the balance of Alice"); + assert_eq!(balance1, initial_balance + U256::from(init_tokens_count)); + + // Check non-consecutive mint. + let token_id = random_token_id(); + contract._mint(alice, token_id).expect("should mint a token for Alice"); + let owner = contract + .owner_of(token_id) + .expect("should return the owner of the token"); + assert_eq!(owner, alice); + + let balance2 = contract + .balance_of(alice) + .expect("should return the balance of Alice"); + + assert_eq!(balance2, balance1 + uint!(1_U256)); + } + + #[motsu::test] + fn error_when_to_is_zero(contract: Erc721Consecutive) { + let err = contract + ._mint_consecutive(Address::ZERO, uint!(11_U96)) + .expect_err("should not mint consecutive"); + assert!(matches!( + err, + Error::Erc721(erc721::Error::InvalidReceiver( + ERC721InvalidReceiver { receiver: Address::ZERO } + )) + )); + } + + #[motsu::test] + fn error_when_exceed_batch_size(contract: Erc721Consecutive) { + let alice = msg::sender(); + let batch_size = contract._max_batch_size() + uint!(1_U96); + let err = contract + ._mint_consecutive(alice, batch_size) + .expect_err("should not mint consecutive"); + assert!(matches!( + err, + Error::ExceededMaxBatchMint(ERC721ExceededMaxBatchMint { + batch_size, + max_batch + }) + if batch_size == U256::from(batch_size) && max_batch == U256::from(contract._max_batch_size()) + )); + } + + #[motsu::test] + fn transfers_from(contract: Erc721Consecutive) { + let alice = msg::sender(); + let bob = BOB; + + // Mint batches of 1000 tokens to Alice and Bob. + let [first_consecutive_token_id, _] = init( + contract, + vec![alice, bob], + vec![uint!(1000_U96), uint!(1000_U96)], + ) + .try_into() + .expect("should have two elements in return vec"); + + // Transfer first consecutive token from Alice to Bob. + contract + .transfer_from(alice, bob, U256::from(first_consecutive_token_id)) + .expect("should transfer a token from Alice to Bob"); + + let owner = contract + .owner_of(U256::from(first_consecutive_token_id)) + .expect("token should be owned"); + assert_eq!(owner, bob); + + // Check that balances changed. + let alice_balance = contract + .balance_of(alice) + .expect("should return the balance of Alice"); + assert_eq!(alice_balance, uint!(1000_U256) - uint!(1_U256)); + let bob_balance = + contract.balance_of(bob).expect("should return the balance of Bob"); + assert_eq!(bob_balance, uint!(1000_U256) + uint!(1_U256)); + + // Check non-consecutive mint. + let token_id = random_token_id(); + contract._mint(alice, token_id).expect("should mint a token to Alice"); + let alice_balance = contract + .balance_of(alice) + .expect("should return the balance of Alice"); + assert_eq!(alice_balance, uint!(1000_U256)); + + // Check transfer of the token that wasn't minted consecutive. + contract + .transfer_from(alice, BOB, token_id) + .expect("should transfer a token from Alice to Bob"); + let alice_balance = contract + .balance_of(alice) + .expect("should return the balance of Alice"); + assert_eq!(alice_balance, uint!(1000_U256) - uint!(1_U256)); + } + + #[motsu::test] + fn burns(contract: Erc721Consecutive) { + let alice = msg::sender(); + + // Mint batch of 1000 tokens to Alice. + let [first_consecutive_token_id] = + init(contract, vec![alice], vec![uint!(1000_U96)]) + .try_into() + .expect("should have two elements in return vec"); + + // Check consecutive token burn. + contract + ._burn(U256::from(first_consecutive_token_id)) + .expect("should burn token"); + + let alice_balance = contract + .balance_of(alice) + .expect("should return the balance of Alice"); + assert_eq!(alice_balance, uint!(1000_U256) - uint!(1_U256)); + + let err = contract + .owner_of(U256::from(first_consecutive_token_id)) + .expect_err("token should not exist"); + + assert!(matches!( + err, + Error::Erc721(erc721::Error::NonexistentToken(ERC721NonexistentToken { token_id })) + if token_id == U256::from(first_consecutive_token_id) + )); + + // Check non-consecutive token burn. + let non_consecutive_token_id = random_token_id(); + contract + ._mint(alice, non_consecutive_token_id) + .expect("should mint a token to Alice"); + let owner = contract + .owner_of(non_consecutive_token_id) + .expect("should return owner of the token"); + assert_eq!(owner, alice); + let alice_balance = contract + .balance_of(alice) + .expect("should return the balance of Alice"); + assert_eq!(alice_balance, uint!(1000_U256)); + + contract._burn(non_consecutive_token_id).expect("should burn token"); + + let err = contract + .owner_of(U256::from(non_consecutive_token_id)) + .expect_err("token should not exist"); + + assert!(matches!( + err, + Error::Erc721(erc721::Error::NonexistentToken(ERC721NonexistentToken { token_id })) + if token_id == U256::from(non_consecutive_token_id) + )); + } +} diff --git a/contracts/src/token/erc721/extensions/enumerable.rs b/contracts/src/token/erc721/extensions/enumerable.rs index f2aa522d..88c8d8d1 100644 --- a/contracts/src/token/erc721/extensions/enumerable.rs +++ b/contracts/src/token/erc721/extensions/enumerable.rs @@ -14,7 +14,7 @@ use alloy_primitives::{uint, Address, U256}; use alloy_sol_types::sol; use stylus_proc::{external, sol_storage, SolidityError}; -use crate::token::erc721::IErc721; +use crate::token::{erc721, erc721::IErc721}; sol! { /// Indicates an error when an `owner`'s token query @@ -158,8 +158,8 @@ impl Erc721Enumerable { &mut self, to: Address, token_id: U256, - erc721: &impl IErc721, - ) -> Result<(), crate::token::erc721::Error> { + erc721: &impl IErc721, + ) -> Result<(), erc721::Error> { let length = erc721.balance_of(to)? - uint!(1_U256); self._owned_tokens.setter(to).setter(length).set(token_id); self._owned_tokens_index.setter(token_id).set(length); @@ -209,8 +209,8 @@ impl Erc721Enumerable { &mut self, from: Address, token_id: U256, - erc721: &impl IErc721, - ) -> Result<(), crate::token::erc721::Error> { + erc721: &impl IErc721, + ) -> Result<(), erc721::Error> { // To prevent a gap in from's tokens array, // we store the last token in the index of the token to delete, // and then delete the last slot (swap and pop). diff --git a/contracts/src/token/erc721/extensions/mod.rs b/contracts/src/token/erc721/extensions/mod.rs index d994a03c..6a49373d 100644 --- a/contracts/src/token/erc721/extensions/mod.rs +++ b/contracts/src/token/erc721/extensions/mod.rs @@ -1,5 +1,6 @@ //! Common extensions to the ERC-721 standard. pub mod burnable; +pub mod consecutive; pub mod enumerable; pub mod metadata; pub mod uri_storage; diff --git a/contracts/src/token/erc721/mod.rs b/contracts/src/token/erc721/mod.rs index 1abe8600..e9cebc44 100644 --- a/contracts/src/token/erc721/mod.rs +++ b/contracts/src/token/erc721/mod.rs @@ -193,6 +193,7 @@ unsafe impl TopLevelStorage for Erc721 {} /// Required interface of an [`Erc721`] compliant contract. pub trait IErc721 { + type Error: Into>; /// Returns the number of tokens in `owner`'s account. /// /// # Arguments @@ -204,7 +205,7 @@ pub trait IErc721 { /// /// If owner address is `Address::ZERO`, then the error /// [`Error::InvalidOwner`] is returned. - fn balance_of(&self, owner: Address) -> Result; + fn balance_of(&self, owner: Address) -> Result; /// Returns the owner of the `token_id` token. /// @@ -221,7 +222,7 @@ pub trait IErc721 { /// # Requirements /// /// * `token_id` must exist. - fn owner_of(&self, token_id: U256) -> Result; + fn owner_of(&self, token_id: U256) -> Result; /// Safely transfers `token_id` token from `from` to `to`, checking first /// that contract recipients are aware of the [`Erc721`] protocol to @@ -267,7 +268,7 @@ pub trait IErc721 { from: Address, to: Address, token_id: U256, - ) -> Result<(), Error>; + ) -> Result<(), Self::Error>; /// Safely transfers `token_id` token from `from` to `to`. /// @@ -300,7 +301,7 @@ pub trait IErc721 { /// * `to` cannot be the zero address. /// * The `token_id` token must exist and be owned by `from`. /// * If the caller is not `from`, it must be approved to move this token by - /// either [`Erc721::_approve`] or [`Erc721::set_approval_for_all`]. + /// either [`Erc721::_approve`] or [`Self::set_approval_for_all`]. /// * If `to` refers to a smart contract, it must implement /// [`IERC721Receiver::on_erc_721_received`], which is called upon a /// `safe_transfer`. @@ -314,7 +315,7 @@ pub trait IErc721 { to: Address, token_id: U256, data: Bytes, - ) -> Result<(), Error>; + ) -> Result<(), Self::Error>; /// Transfers `token_id` token from `from` to `to`. /// @@ -358,7 +359,7 @@ pub trait IErc721 { from: Address, to: Address, token_id: U256, - ) -> Result<(), Error>; + ) -> Result<(), Self::Error>; /// Gives permission to `to` to transfer `token_id` token to another /// account. The approval is cleared when the token is transferred. @@ -388,7 +389,11 @@ pub trait IErc721 { /// # Events /// /// Emits an [`Approval`] event. - fn approve(&mut self, to: Address, token_id: U256) -> Result<(), Error>; + fn approve( + &mut self, + to: Address, + token_id: U256, + ) -> Result<(), Self::Error>; /// Approve or remove `operator` as an operator for the caller. /// @@ -419,7 +424,7 @@ pub trait IErc721 { &mut self, operator: Address, approved: bool, - ) -> Result<(), Error>; + ) -> Result<(), Self::Error>; /// Returns the account approved for `token_id` token. /// @@ -436,7 +441,7 @@ pub trait IErc721 { /// # Requirements: /// /// * `token_id` must exist. - fn get_approved(&self, token_id: U256) -> Result; + fn get_approved(&self, token_id: U256) -> Result; /// Returns whether the `operator` is allowed to manage all the assets of /// `owner`. @@ -451,6 +456,8 @@ pub trait IErc721 { #[external] impl IErc721 for Erc721 { + type Error = Error; + fn balance_of(&self, owner: Address) -> Result { if owner.is_zero() { return Err(ERC721InvalidOwner { owner: Address::ZERO }.into()); @@ -526,7 +533,7 @@ impl IErc721 for Erc721 { fn get_approved(&self, token_id: U256) -> Result { self._require_owned(token_id)?; - Ok(self._get_approved_inner(token_id)) + Ok(self._get_approved(token_id)) } fn is_approved_for_all(&self, owner: Address, operator: Address) -> bool { @@ -542,15 +549,15 @@ impl Erc721 { /// not tracked by the core [`Erc721`] logic MUST be matched with the use /// of [`Self::_increase_balance`] to keep balances consistent with /// ownership. The invariant to preserve is that for any address `a` the - /// value returned by `balance_of(a)` must be equal to the number of - /// tokens such that `owner_of_inner(token_id)` is `a`. + /// value returned by [`Self::balance_of(a)`] must be equal to the number of + /// tokens such that [`Self::_owner_of(token_id)`] is `a`. /// /// # Arguments /// /// * `&self` - Read access to the contract's state. /// * `token_id` - Token id as a number. #[must_use] - pub fn _owner_of_inner(&self, token_id: U256) -> Address { + pub fn _owner_of(&self, token_id: U256) -> Address { self._owners.get(token_id) } @@ -562,7 +569,7 @@ impl Erc721 { /// * `&self` - Read access to the contract's state. /// * `token_id` - Token id as a number. #[must_use] - pub fn _get_approved_inner(&self, token_id: U256) -> Address { + pub fn _get_approved(&self, token_id: U256) -> Address { self._token_approvals.get(token_id) } @@ -588,7 +595,7 @@ impl Erc721 { !spender.is_zero() && (owner == spender || self.is_approved_for_all(owner, spender) - || self._get_approved_inner(token_id) == spender) + || self._get_approved(token_id) == spender) } /// Checks if `operator` can operate on `token_id`, assuming the provided @@ -638,7 +645,7 @@ impl Erc721 { /// values. /// /// WARNING: Increasing an account's balance using this function tends to - /// be paired with an override of the [`Self::_owner_of_inner`] function to + /// be paired with an override of the [`Self::_owner_of`] function to /// resolve the ownership of the corresponding tokens so that balances and /// ownership remain consistent with one another. /// @@ -689,7 +696,7 @@ impl Erc721 { token_id: U256, auth: Address, ) -> Result { - let from = self._owner_of_inner(token_id); + let from = self._owner_of(token_id); // Perform (optional) operator check. if !auth.is_zero() { @@ -765,7 +772,7 @@ impl Erc721 { /// * `to` - Account of the recipient. /// * `token_id` - Token id as a number. /// * `data` - Additional data with no specified format, sent in the call to - /// [`Self::_check_on_erc721_received`]. + /// [`Erc721::_check_on_erc721_received`]. /// /// # Errors /// @@ -852,7 +859,7 @@ impl Erc721 { /// If `to` is `Address::ZERO`, then the error /// [`Error::InvalidReceiver`] is returned. /// If `token_id` does not exist, then the error - /// [`ERC721NonexistentToken`] is returned. + /// [`Error::NonexistentToken`] is returned. /// If the previous owner is not `from`, then the error /// [`Error::IncorrectOwner`] is returned. /// @@ -909,14 +916,14 @@ impl Erc721 { /// * `to` - Account of the recipient. /// * `token_id` - Token id as a number. /// * `data` - Additional data with no specified format, sent in the call to - /// [`Self::_check_on_erc721_received`]. + /// [`Erc721::_check_on_erc721_received`]. /// /// # Errors /// /// If `to` is `Address::ZERO`, then the error /// [`Error::InvalidReceiver`] is returned. /// If `token_id` does not exist, then the error - /// [`ERC721NonexistentToken`] is returned. + /// [`Error::NonexistentToken`] is returned. /// If the previous owner is not `from`, then the error /// [`Error::IncorrectOwner`] is returned. /// @@ -943,9 +950,11 @@ impl Erc721 { self._check_on_erc721_received(msg::sender(), from, to, token_id, &data) } - /// Variant of `approve_inner` with an optional flag to enable or disable - /// the [`Approval`] event. The event is not emitted in the context of - /// transfers. + /// Approve `to` to operate on `token_id`. + /// + /// The `auth` argument is optional. If the value passed is non 0, then this + /// function will check that `auth` is either the owner of the token, or + /// approved to operate on all tokens held by this owner. /// /// # Arguments /// @@ -1034,7 +1043,7 @@ impl Erc721 { /// minted, or it has been burned). Returns the owner. /// /// Overrides to ownership logic should be done to - /// [`Self::_owner_of_inner`]. + /// [`Self::_owner_of`]. /// /// # Errors /// @@ -1046,7 +1055,7 @@ impl Erc721 { /// * `&self` - Read access to the contract's state. /// * `token_id` - Token id as a number. pub fn _require_owned(&self, token_id: U256) -> Result { - let owner = self._owner_of_inner(token_id); + let owner = self._owner_of(token_id); if owner.is_zero() { return Err(ERC721NonexistentToken { token_id }.into()); } @@ -1853,40 +1862,40 @@ mod tests { } #[motsu::test] - fn owner_of_inner_works(contract: Erc721) { + fn owner_of_works(contract: Erc721) { let token_id = random_token_id(); contract._mint(BOB, token_id).expect("should mint a token"); - let owner = contract._owner_of_inner(token_id); + let owner = contract._owner_of(token_id); assert_eq!(BOB, owner); } #[motsu::test] - fn owner_of_inner_nonexistent_token(contract: Erc721) { + fn owner_of_nonexistent_token(contract: Erc721) { let token_id = random_token_id(); - let owner = contract._owner_of_inner(token_id); + let owner = contract._owner_of(token_id); assert_eq!(Address::ZERO, owner); } #[motsu::test] - fn get_approved_inner_nonexistent_token(contract: Erc721) { + fn get_approved_nonexistent_token(contract: Erc721) { let token_id = random_token_id(); - let approved = contract._get_approved_inner(token_id); + let approved = contract._get_approved(token_id); assert_eq!(Address::ZERO, approved); } #[motsu::test] - fn get_approved_inner_token_without_approval(contract: Erc721) { + fn get_approved_token_without_approval(contract: Erc721) { let alice = msg::sender(); let token_id = random_token_id(); contract._mint(alice, token_id).expect("should mint a token"); - let approved = contract._get_approved_inner(token_id); + let approved = contract._get_approved(token_id); assert_eq!(Address::ZERO, approved); } #[motsu::test] - fn get_approved_inner_token_with_approval(contract: Erc721) { + fn get_approved_token_with_approval(contract: Erc721) { let alice = msg::sender(); let token_id = random_token_id(); @@ -1895,12 +1904,12 @@ mod tests { .approve(BOB, token_id) .expect("should approve Bob for operations on token"); - let approved = contract._get_approved_inner(token_id); + let approved = contract._get_approved(token_id); assert_eq!(BOB, approved); } #[motsu::test] - fn get_approved_inner_token_with_approval_for_all(contract: Erc721) { + fn get_approved_token_with_approval_for_all(contract: Erc721) { let alice = msg::sender(); let token_id = random_token_id(); @@ -1909,7 +1918,7 @@ mod tests { .set_approval_for_all(BOB, true) .expect("should approve Bob for operations on all Alice's tokens"); - let approved = contract._get_approved_inner(token_id); + let approved = contract._get_approved(token_id); assert_eq!(Address::ZERO, approved); } diff --git a/contracts/src/utils/structs/checkpoints.rs b/contracts/src/utils/structs/checkpoints.rs index 03128c21..963b102d 100644 --- a/contracts/src/utils/structs/checkpoints.rs +++ b/contracts/src/utils/structs/checkpoints.rs @@ -13,8 +13,8 @@ use crate::utils::math::alloy::Math; // TODO: add generics for other pairs (uint32, uint224) and (uint48, uint208). // Logic should be the same. -type U96 = Uint<96, 2>; -type U160 = Uint<160, 3>; +pub type U96 = Uint<96, 2>; +pub type U160 = Uint<160, 3>; sol! { /// A value was attempted to be inserted into a past checkpoint. diff --git a/examples/access-control/tests/access_control.rs b/examples/access-control/tests/access_control.rs index e33f9f60..15de129d 100644 --- a/examples/access-control/tests/access_control.rs +++ b/examples/access-control/tests/access_control.rs @@ -4,9 +4,12 @@ use abi::AccessControl::{ self, AccessControlBadConfirmation, AccessControlUnauthorizedAccount, RoleAdminChanged, RoleGranted, RoleRevoked, }; -use alloy::{hex, primitives::Address, sol_types::SolConstructor}; -use e2e::{receipt, send, watch, Account, EventExt, Revert}; -use eyre::Result; +use alloy::{ + hex, network::ReceiptResponse, primitives::Address, + sol_types::SolConstructor, +}; +use e2e::{receipt, send, watch, Account, EventExt, ReceiptExt, Revert}; +use eyre::{ContextCompat, Result}; mod abi; @@ -19,7 +22,7 @@ const NEW_ADMIN_ROLE: [u8; 32] = async fn deploy(account: &Account) -> eyre::Result
{ let args = AccessControl::constructorCall {}; let args = alloy::hex::encode(args.abi_encode()); - e2e::deploy(account.url(), &account.pk(), Some(args)).await + e2e::deploy(account.url(), &account.pk(), Some(args)).await?.address() } // ============================================================================ diff --git a/examples/basic/script/src/main.rs b/examples/basic/script/src/main.rs index 0003a369..517b3801 100644 --- a/examples/basic/script/src/main.rs +++ b/examples/basic/script/src/main.rs @@ -1,6 +1,10 @@ use alloy::{ - network::EthereumWallet, primitives::Address, providers::ProviderBuilder, - signers::local::PrivateKeySigner, sol, sol_types::SolConstructor, + network::{EthereumWallet, ReceiptResponse}, + primitives::Address, + providers::ProviderBuilder, + signers::local::PrivateKeySigner, + sol, + sol_types::SolConstructor, }; use koba::config::Deploy; @@ -91,5 +95,9 @@ async fn deploy() -> Address { quiet: false, }; - koba::deploy(&config).await.expect("should deploy contract") + koba::deploy(&config) + .await + .expect("should deploy contract") + .contract_address() + .expect("should return contract address") } diff --git a/examples/ecdsa/tests/ecdsa.rs b/examples/ecdsa/tests/ecdsa.rs index 9d41af38..2afc92f3 100644 --- a/examples/ecdsa/tests/ecdsa.rs +++ b/examples/ecdsa/tests/ecdsa.rs @@ -6,7 +6,7 @@ use alloy::{ sol, sol_types::SolConstructor, }; -use e2e::{Account, Revert}; +use e2e::{Account, ReceiptExt, Revert}; use eyre::Result; use openzeppelin_stylus::utils::cryptography::ecdsa::SIGNATURE_S_UPPER_BOUND; @@ -17,7 +17,7 @@ sol!("src/constructor.sol"); async fn deploy(account: &Account) -> eyre::Result
{ let args = ECDSAExample::constructorCall {}; let args = alloy::hex::encode(args.abi_encode()); - e2e::deploy(account.url(), &account.pk(), Some(args)).await + e2e::deploy(account.url(), &account.pk(), Some(args)).await?.address() } const HASH: B256 = diff --git a/examples/erc20-permit/tests/erc20permit.rs b/examples/erc20-permit/tests/erc20permit.rs index c443afda..04211c1f 100644 --- a/examples/erc20-permit/tests/erc20permit.rs +++ b/examples/erc20-permit/tests/erc20permit.rs @@ -7,7 +7,7 @@ use alloy::{ sol_types::{SolConstructor, SolType}, }; use alloy_primitives::uint; -use e2e::{receipt, send, watch, Account, EventExt, Revert}; +use e2e::{receipt, send, watch, Account, EventExt, ReceiptExt, Revert}; use eyre::Result; mod abi; @@ -43,7 +43,7 @@ macro_rules! domain_separator { async fn deploy(rpc_url: &str, private_key: &str) -> eyre::Result
{ let args = Erc20PermitExample::constructorCall {}; let args = alloy::hex::encode(args.abi_encode()); - e2e::deploy(rpc_url, private_key, Some(args)).await + e2e::deploy(rpc_url, private_key, Some(args)).await?.address() } fn to_typed_data_hash(domain_separator: B256, struct_hash: B256) -> B256 { diff --git a/examples/erc20/tests/erc20.rs b/examples/erc20/tests/erc20.rs index b14c208c..4c85bec8 100644 --- a/examples/erc20/tests/erc20.rs +++ b/examples/erc20/tests/erc20.rs @@ -2,13 +2,17 @@ use abi::Erc20; use alloy::{ + network::ReceiptResponse, primitives::{Address, U256}, sol, sol_types::{SolConstructor, SolError}, }; use alloy_primitives::uint; -use e2e::{receipt, send, watch, Account, EventExt, Panic, PanicCode, Revert}; -use eyre::Result; +use e2e::{ + receipt, send, watch, Account, EventExt, Panic, PanicCode, ReceiptExt, + Revert, +}; +use eyre::{ContextCompat, Result}; mod abi; @@ -29,7 +33,7 @@ async fn deploy( cap_: cap.unwrap_or(CAP), }; let args = alloy::hex::encode(args.abi_encode()); - e2e::deploy(rpc_url, private_key, Some(args)).await + e2e::deploy(rpc_url, private_key, Some(args)).await?.address() } // ============================================================================ diff --git a/examples/erc721-consecutive/Cargo.toml b/examples/erc721-consecutive/Cargo.toml new file mode 100644 index 00000000..2c6eef27 --- /dev/null +++ b/examples/erc721-consecutive/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "erc721-consecutive-example" +edition.workspace = true +license.workspace = true +repository.workspace = true +publish = false +version = "0.0.0" + +[dependencies] +openzeppelin-stylus = { path = "../../contracts" } +alloy-primitives.workspace = true +alloy-sol-types.workspace = true +stylus-sdk.workspace = true +stylus-proc.workspace = true +mini-alloc.workspace = true + +[dev-dependencies] +alloy.workspace = true +e2e = { path = "../../lib/e2e" } +tokio.workspace = true +eyre.workspace = true +rand.workspace = true + +[features] +e2e = [] + +[lib] +crate-type = ["lib", "cdylib"] diff --git a/examples/erc721-consecutive/src/constructor.sol b/examples/erc721-consecutive/src/constructor.sol new file mode 100644 index 00000000..f875a3b4 --- /dev/null +++ b/examples/erc721-consecutive/src/constructor.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +contract Erc721ConsecutiveExample { + mapping(uint256 tokenId => address) private _owners; + mapping(address owner => uint256) private _balances; + mapping(uint256 tokenId => address) private _tokenApprovals; + mapping(address owner => mapping(address operator => bool)) private _operatorApprovals; + + Checkpoint160[] private _checkpoints; // _sequentialOwnership + mapping(uint256 bucket => uint256) private _data; // _sequentialBurn + uint96 private _firstConsecutiveId; + uint96 private _maxBatchSize; + + error ERC721InvalidReceiver(address receiver); + error ERC721ForbiddenBatchMint(); + error ERC721ExceededMaxBatchMint(uint256 batchSize, uint256 maxBatch); + error ERC721ForbiddenMint(); + error ERC721ForbiddenBatchBurn(); + error CheckpointUnorderedInsertion(); + + event ConsecutiveTransfer( + uint256 indexed fromTokenId, + uint256 toTokenId, + address indexed fromAddress, + address indexed toAddress + ); + + struct Checkpoint160 { + uint96 _key; + uint160 _value; + } + + constructor( + address[] memory receivers, + uint96[] memory amounts, + uint96 firstConsecutiveId, + uint96 maxBatchSize) + { + _firstConsecutiveId = firstConsecutiveId; + _maxBatchSize = maxBatchSize; + for (uint256 i = 0; i < receivers.length; ++i) { + _mintConsecutive(receivers[i], amounts[i]); + } + } + + function latestCheckpoint() internal view returns (bool exists, uint96 _key, uint160 _value) { + uint256 pos = _checkpoints.length; + if (pos == 0) { + return (false, 0, 0); + } else { + Checkpoint160 storage ckpt = _checkpoints[pos - 1]; + return (true, ckpt._key, ckpt._value); + } + } + + function push(uint96 key, uint160 value) internal returns (uint160, uint160) { + return _insert(key, value); + } + + function _insert(uint96 key, uint160 value) private returns (uint160, uint160) { + uint256 pos = _checkpoints.length; + + if (pos > 0) { + Checkpoint160 storage last = _checkpoints[pos - 1]; + uint96 lastKey = last._key; + uint160 lastValue = last._value; + + // Checkpoint keys must be non-decreasing. + if (lastKey > key) { + revert CheckpointUnorderedInsertion(); + } + + // Update or push new checkpoint. + if (lastKey == key) { + _checkpoints[pos - 1]._value = value; + } else { + _checkpoints.push(Checkpoint160({_key: key, _value: value})); + } + return (lastValue, value); + } else { + _checkpoints.push(Checkpoint160({_key: key, _value: value})); + return (0, value); + } + } + + function _mintConsecutive(address to, uint96 batchSize) internal virtual returns (uint96) { + uint96 next = _nextConsecutiveId(); + + // minting a batch of size 0 is a no-op. + if (batchSize > 0) { + if (address(this).code.length > 0) { + revert ERC721ForbiddenBatchMint(); + } + if (to == address(0)) { + revert ERC721InvalidReceiver(address(0)); + } + + uint256 maxBatchSize = _maxBatchSize; + if (batchSize > maxBatchSize) { + revert ERC721ExceededMaxBatchMint(batchSize, maxBatchSize); + } + + // push an ownership checkpoint & emit event. + uint96 last = next + batchSize - 1; + push(last, uint160(to)); + + // The invariant required by this function is preserved because the new sequentialOwnership checkpoint + // is attributing ownership of `batchSize` new tokens to account `to`. + _increaseBalance(to, batchSize); + + emit ConsecutiveTransfer(next, last, address(0), to); + } + + return next; + } + + function _nextConsecutiveId() private view returns (uint96) { + (bool exists, uint96 latestId,) = latestCheckpoint(); + return exists ? latestId + 1 : _firstConsecutiveId; + } + + function _increaseBalance(address account, uint128 value) internal virtual { + unchecked { + _balances[account] += value; + } + } +} diff --git a/examples/erc721-consecutive/src/lib.rs b/examples/erc721-consecutive/src/lib.rs new file mode 100644 index 00000000..d847499e --- /dev/null +++ b/examples/erc721-consecutive/src/lib.rs @@ -0,0 +1,28 @@ +#![cfg_attr(not(test), no_main, no_std)] +extern crate alloc; + +use alloy_primitives::{Address, U256}; +use openzeppelin_stylus::token::erc721::extensions::consecutive::{ + Erc721Consecutive, Error, +}; +use stylus_sdk::prelude::*; + +sol_storage! { + #[entrypoint] + struct Erc721ConsecutiveExample { + #[borrow] + Erc721Consecutive erc721_consecutive; + } +} + +#[external] +#[inherit(Erc721Consecutive)] +impl Erc721ConsecutiveExample { + pub fn burn(&mut self, token_id: U256) -> Result<(), Error> { + self.erc721_consecutive._burn(token_id) + } + + pub fn mint(&mut self, to: Address, token_id: U256) -> Result<(), Error> { + self.erc721_consecutive._mint(to, token_id) + } +} diff --git a/examples/erc721-consecutive/tests/abi.rs b/examples/erc721-consecutive/tests/abi.rs new file mode 100644 index 00000000..55234523 --- /dev/null +++ b/examples/erc721-consecutive/tests/abi.rs @@ -0,0 +1,53 @@ +#![allow(dead_code)] +use alloy::sol; + +sol!( + #[sol(rpc)] + contract Erc721 { + #[derive(Debug)] + function balanceOf(address owner) external view returns (uint256 balance); + #[derive(Debug)] + function ownerOf(uint256 tokenId) external view returns (address ownerOf); + function safeTransferFrom(address from, address to, uint256 tokenId) external; + function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external; + function transferFrom(address from, address to, uint256 tokenId) external; + function approve(address to, uint256 tokenId) external; + function setApprovalForAll(address operator, bool approved) external; + function getApproved(uint256 tokenId) external view returns (address); + function isApprovedForAll(address owner, address operator) external view returns (bool); + function mint(address to, uint256 tokenId) external; + + function burn(uint256 tokenId) external; + + error ERC721InvalidOwner(address owner); + error ERC721NonexistentToken(uint256 tokenId); + error ERC721IncorrectOwner(address sender, uint256 tokenId, address owner); + error ERC721InvalidSender(address sender); + error ERC721InvalidReceiver(address receiver); + error ERC721InsufficientApproval(address operator, uint256 tokenId); + error ERC721InvalidApprover(address approver); + error ERC721InvalidOperator(address operator); + + error ERC721ForbiddenBatchMint(); + error ERC721ExceededMaxBatchMint(uint256 batchSize, uint256 maxBatch); + error ERC721ForbiddenMint(); + error ERC721ForbiddenBatchBurn(); + + #[derive(Debug, PartialEq)] + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + + #[derive(Debug, PartialEq)] + event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); + + #[derive(Debug, PartialEq)] + event ApprovalForAll(address indexed owner, address indexed operator, bool approved); + + #[derive(Debug, PartialEq)] + event ConsecutiveTransfer( + uint256 indexed fromTokenId, + uint256 toTokenId, + address indexed fromAddress, + address indexed toAddress + ); + } +); diff --git a/examples/erc721-consecutive/tests/erc721-consecutive.rs b/examples/erc721-consecutive/tests/erc721-consecutive.rs new file mode 100644 index 00000000..f60ae965 --- /dev/null +++ b/examples/erc721-consecutive/tests/erc721-consecutive.rs @@ -0,0 +1,223 @@ +#![cfg(feature = "e2e")] + +use alloy::{ + primitives::{Address, U256}, + rpc::types::TransactionReceipt, + sol, + sol_types::SolConstructor, +}; +use alloy_primitives::uint; +use e2e::{receipt, watch, Account, EventExt, ReceiptExt, Revert}; + +use crate::{abi::Erc721, Erc721ConsecutiveExample::constructorCall}; + +mod abi; + +sol!("src/constructor.sol"); + +const FIRST_CONSECUTIVE_ID: u128 = 0; +const MAX_BATCH_SIZE: u128 = 5000; + +fn random_token_id() -> U256 { + let num: u32 = rand::random(); + U256::from(num) +} + +async fn deploy( + account: &Account, + constructor: C, +) -> eyre::Result { + let args = alloy::hex::encode(constructor.abi_encode()); + e2e::deploy(account.url(), &account.pk(), Some(args)).await +} + +fn constructor(receivers: Vec
, amounts: Vec) -> constructorCall { + constructorCall { + receivers, + amounts, + firstConsecutiveId: FIRST_CONSECUTIVE_ID, + maxBatchSize: MAX_BATCH_SIZE, + } +} + +#[e2e::test] +async fn constructs(alice: Account) -> eyre::Result<()> { + let alice_addr = alice.address(); + let receivers = vec![alice_addr]; + let amounts = vec![10_u128]; + let receipt = deploy(&alice, constructor(receivers, amounts)).await?; + let contract = Erc721::new(receipt.address()?, &alice.wallet); + + let balance = contract.balanceOf(alice_addr).call().await?.balance; + assert_eq!(balance, uint!(10_U256)); + Ok(()) +} + +#[e2e::test] +async fn mints(alice: Account) -> eyre::Result<()> { + let batch_size = 10_u128; + let receivers = vec![alice.address()]; + let amounts = vec![batch_size]; + let receipt = deploy(&alice, constructor(receivers, amounts)).await?; + let contract = Erc721::new(receipt.address()?, &alice.wallet); + + assert!(receipt.emits(Erc721::ConsecutiveTransfer { + fromTokenId: U256::from(FIRST_CONSECUTIVE_ID), + toTokenId: uint!(9_U256), + fromAddress: Address::ZERO, + toAddress: alice.address(), + })); + + let Erc721::balanceOfReturn { balance: balance1 } = + contract.balanceOf(alice.address()).call().await?; + assert_eq!(balance1, U256::from(batch_size)); + + let token_id = random_token_id(); + let _ = watch!(contract.mint(alice.address(), token_id))?; + + let Erc721::balanceOfReturn { balance: balance2 } = + contract.balanceOf(alice.address()).call().await?; + + assert_eq!(balance2, balance1 + uint!(1_U256)); + Ok(()) +} + +#[e2e::test] +async fn error_when_to_is_zero(alice: Account) -> eyre::Result<()> { + let receivers = vec![Address::ZERO]; + let amounts = vec![10_u128]; + let err = deploy(&alice, constructor(receivers, amounts)) + .await + .expect_err("should not mint consecutive"); + + assert!(err.reverted_with(Erc721::ERC721InvalidReceiver { + receiver: Address::ZERO + })); + Ok(()) +} + +#[e2e::test] +async fn error_when_exceed_batch_size(alice: Account) -> eyre::Result<()> { + let receivers = vec![alice.address()]; + let amounts = vec![MAX_BATCH_SIZE + 1]; + let err = deploy(&alice, constructor(receivers, amounts)) + .await + .expect_err("should not mint consecutive"); + + assert!(err.reverted_with(Erc721::ERC721ExceededMaxBatchMint { + batchSize: U256::from(MAX_BATCH_SIZE + 1), + maxBatch: U256::from(MAX_BATCH_SIZE), + })); + Ok(()) +} + +#[e2e::test] +async fn transfers_from(alice: Account, bob: Account) -> eyre::Result<()> { + let receivers = vec![alice.address(), bob.address()]; + let amounts = vec![1000_u128, 1000_u128]; + // Deploy and mint batches of 1000 tokens to Alice and Bob. + let receipt = deploy(&alice, constructor(receivers, amounts)).await?; + let contract = Erc721::new(receipt.address()?, &alice.wallet); + + let first_consecutive_token_id = U256::from(FIRST_CONSECUTIVE_ID); + + // Transfer first consecutive token from Alice to Bob. + let _ = watch!(contract.transferFrom( + alice.address(), + bob.address(), + first_consecutive_token_id + ))?; + + let Erc721::ownerOfReturn { ownerOf } = + contract.ownerOf(first_consecutive_token_id).call().await?; + assert_eq!(ownerOf, bob.address()); + + // Check that balances changed. + let Erc721::balanceOfReturn { balance: alice_balance } = + contract.balanceOf(alice.address()).call().await?; + assert_eq!(alice_balance, uint!(1000_U256) - uint!(1_U256)); + let Erc721::balanceOfReturn { balance: bob_balance } = + contract.balanceOf(bob.address()).call().await?; + assert_eq!(bob_balance, uint!(1000_U256) + uint!(1_U256)); + + // Test non-consecutive mint. + let token_id = random_token_id(); + let _ = watch!(contract.mint(alice.address(), token_id))?; + let Erc721::balanceOfReturn { balance: alice_balance } = + contract.balanceOf(alice.address()).call().await?; + assert_eq!(alice_balance, uint!(1000_U256)); + + // Test transfer of the token that wasn't minted consecutive. + let _ = watch!(contract.transferFrom( + alice.address(), + bob.address(), + token_id + ))?; + let Erc721::balanceOfReturn { balance: alice_balance } = + contract.balanceOf(alice.address()).call().await?; + assert_eq!(alice_balance, uint!(1000_U256) - uint!(1_U256)); + Ok(()) +} + +#[e2e::test] +async fn burns(alice: Account) -> eyre::Result<()> { + let receivers = vec![alice.address()]; + let amounts = vec![1000_u128]; + // Mint batch of 1000 tokens to Alice. + let receipt = deploy(&alice, constructor(receivers, amounts)).await?; + let contract = Erc721::new(receipt.address()?, &alice.wallet); + + let first_consecutive_token_id = U256::from(FIRST_CONSECUTIVE_ID); + + // Check consecutive token burn. + let receipt = receipt!(contract.burn(first_consecutive_token_id))?; + + assert!(receipt.emits(Erc721::Transfer { + from: alice.address(), + to: Address::ZERO, + tokenId: first_consecutive_token_id, + })); + + let Erc721::balanceOfReturn { balance: alice_balance } = + contract.balanceOf(alice.address()).call().await?; + assert_eq!(alice_balance, uint!(1000_U256) - uint!(1_U256)); + + let err = contract + .ownerOf(first_consecutive_token_id) + .call() + .await + .expect_err("should return `ERC721NonexistentToken`"); + + assert!(err.reverted_with(Erc721::ERC721NonexistentToken { + tokenId: first_consecutive_token_id + })); + + // Check non-consecutive token burn. + let non_consecutive_token_id = random_token_id(); + let _ = watch!(contract.mint(alice.address(), non_consecutive_token_id))?; + let Erc721::ownerOfReturn { ownerOf } = + contract.ownerOf(non_consecutive_token_id).call().await?; + assert_eq!(ownerOf, alice.address()); + let Erc721::balanceOfReturn { balance: alice_balance } = + contract.balanceOf(alice.address()).call().await?; + assert_eq!(alice_balance, uint!(1000_U256)); + + let receipt = receipt!(contract.burn(non_consecutive_token_id))?; + + assert!(receipt.emits(Erc721::Transfer { + from: alice.address(), + to: Address::ZERO, + tokenId: non_consecutive_token_id, + })); + + let err = contract + .ownerOf(non_consecutive_token_id) + .call() + .await + .expect_err("should return `ERC721NonexistentToken`"); + + assert!(err.reverted_with(Erc721::ERC721NonexistentToken { + tokenId: non_consecutive_token_id + })); + Ok(()) +} diff --git a/examples/erc721-metadata/tests/erc721.rs b/examples/erc721-metadata/tests/erc721.rs index 658cacd8..a63bb2c0 100644 --- a/examples/erc721-metadata/tests/erc721.rs +++ b/examples/erc721-metadata/tests/erc721.rs @@ -2,11 +2,13 @@ use abi::Erc721; use alloy::{ + network::ReceiptResponse, primitives::{Address, U256}, sol, sol_types::SolConstructor, }; -use e2e::{receipt, watch, Account, EventExt, Revert}; +use e2e::{receipt, watch, Account, EventExt, ReceiptExt, Revert}; +use eyre::ContextCompat; mod abi; @@ -31,7 +33,7 @@ async fn deploy( baseUri_: base_uri.to_owned(), }; let args = alloy::hex::encode(args.abi_encode()); - e2e::deploy(rpc_url, private_key, Some(args)).await + e2e::deploy(rpc_url, private_key, Some(args)).await?.address() } // ============================================================================ diff --git a/examples/erc721/tests/erc721.rs b/examples/erc721/tests/erc721.rs index b3cb264a..2d89cc1e 100644 --- a/examples/erc721/tests/erc721.rs +++ b/examples/erc721/tests/erc721.rs @@ -2,12 +2,14 @@ use abi::Erc721; use alloy::{ + network::ReceiptResponse, primitives::{fixed_bytes, Address, Bytes, U256}, sol, sol_types::SolConstructor, }; use alloy_primitives::uint; -use e2e::{receipt, send, watch, Account, EventExt, Revert}; +use e2e::{receipt, send, watch, Account, EventExt, ReceiptExt, Revert}; +use eyre::ContextCompat; use mock::{receiver, receiver::ERC721ReceiverMock}; mod abi; @@ -23,7 +25,7 @@ fn random_token_id() -> U256 { async fn deploy(rpc_url: &str, private_key: &str) -> eyre::Result
{ let args = Erc721Example::constructorCall {}; let args = alloy::hex::encode(args.abi_encode()); - e2e::deploy(rpc_url, private_key, Some(args)).await + e2e::deploy(rpc_url, private_key, Some(args)).await?.address() } // ============================================================================ diff --git a/examples/ownable/tests/ownable.rs b/examples/ownable/tests/ownable.rs index 342ea545..ee155270 100644 --- a/examples/ownable/tests/ownable.rs +++ b/examples/ownable/tests/ownable.rs @@ -2,14 +2,15 @@ use abi::{Ownable, Ownable::OwnershipTransferred}; use alloy::{ + network::ReceiptResponse, primitives::Address, providers::Provider, rpc::types::{BlockNumberOrTag, Filter}, sol, sol_types::{SolConstructor, SolError, SolEvent}, }; -use e2e::{receipt, send, Account, EventExt, Revert}; -use eyre::Result; +use e2e::{receipt, send, Account, EventExt, ReceiptExt, Revert}; +use eyre::{ContextCompat, Result}; mod abi; @@ -18,7 +19,7 @@ sol!("src/constructor.sol"); async fn deploy(account: &Account, owner: Address) -> eyre::Result
{ let args = OwnableExample::constructorCall { initialOwner: owner }; let args = alloy::hex::encode(args.abi_encode()); - e2e::deploy(account.url(), &account.pk(), Some(args)).await + e2e::deploy(account.url(), &account.pk(), Some(args)).await?.address() } // ============================================================================ diff --git a/lib/e2e/src/deploy.rs b/lib/e2e/src/deploy.rs index 5bb8a8a6..5194db4a 100644 --- a/lib/e2e/src/deploy.rs +++ b/lib/e2e/src/deploy.rs @@ -1,4 +1,4 @@ -use alloy::primitives::Address; +use alloy::{primitives::Address, rpc::types::TransactionReceipt}; use koba::config::Deploy; use crate::project::Crate; @@ -17,15 +17,16 @@ pub async fn deploy( rpc_url: &str, private_key: &str, args: Option, -) -> eyre::Result
{ +) -> eyre::Result { let pkg = Crate::new()?; let sol_path = pkg.manifest_dir.join("src/constructor.sol"); let wasm_path = pkg.wasm; + let has_constructor = args.is_some(); let config = Deploy { generate_config: koba::config::Generate { wasm: wasm_path.clone(), - sol: Some(sol_path), + sol: if has_constructor { Some(sol_path) } else { None }, args, legacy: false, }, @@ -40,6 +41,6 @@ pub async fn deploy( quiet: false, }; - let address = koba::deploy(&config).await?; - Ok(address) + let receipt = koba::deploy(&config).await?; + Ok(receipt) } diff --git a/lib/e2e/src/error.rs b/lib/e2e/src/error.rs index 82b26f1d..360ebd9c 100644 --- a/lib/e2e/src/error.rs +++ b/lib/e2e/src/error.rs @@ -1,4 +1,7 @@ -use alloy::sol_types::SolError; +use alloy::{ + sol_types::SolError, + transports::{RpcError, TransportErrorKind}, +}; /// Possible panic codes for a revert. /// @@ -92,3 +95,22 @@ impl Revert for alloy::contract::Error { expected == actual } } + +impl Revert for eyre::Report { + fn reverted_with(&self, expected: E) -> bool { + let Some(received) = self + .chain() + .find_map(|err| err.downcast_ref::>()) + else { + return false; + }; + let RpcError::ErrorResp(received) = received else { + return false; + }; + let Some(received) = &received.data else { + return false; + }; + let expected = alloy::hex::encode(expected.abi_encode()); + received.to_string().contains(&expected) + } +} diff --git a/lib/e2e/src/lib.rs b/lib/e2e/src/lib.rs index 773fa006..4a14809f 100644 --- a/lib/e2e/src/lib.rs +++ b/lib/e2e/src/lib.rs @@ -5,6 +5,7 @@ mod environment; mod error; mod event; mod project; +mod receipt; mod system; pub use account::Account; @@ -12,6 +13,7 @@ pub use deploy::deploy; pub use e2e_proc::test; pub use error::{Panic, PanicCode, Revert}; pub use event::EventExt; +pub use receipt::ReceiptExt; pub use system::{fund_account, provider, Provider, Wallet}; /// This macro provides a shorthand for broadcasting the transaction to the diff --git a/lib/e2e/src/receipt.rs b/lib/e2e/src/receipt.rs new file mode 100644 index 00000000..631a250e --- /dev/null +++ b/lib/e2e/src/receipt.rs @@ -0,0 +1,17 @@ +use alloy::{ + network::ReceiptResponse, primitives::Address, + rpc::types::TransactionReceipt, +}; +use eyre::ContextCompat; + +/// Extension trait to recover address of the contract that was deployed. +pub trait ReceiptExt { + /// Returns the address of the contract from the [`TransactionReceipt`]. + fn address(&self) -> eyre::Result
; +} + +impl ReceiptExt for TransactionReceipt { + fn address(&self) -> eyre::Result
{ + self.contract_address().context("should contain contract address") + } +}