From c5f18be09d9eb90edf1838920d7a835292a2159c Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Tue, 18 Jun 2024 18:56:43 +0400 Subject: [PATCH 01/95] ++ --- contracts/src/utils/structs/Checkpoints.sol | 606 ++++++++++++++++++++ contracts/src/utils/structs/checkpoints.rs | 50 ++ contracts/src/utils/structs/mod.rs | 1 + 3 files changed, 657 insertions(+) create mode 100644 contracts/src/utils/structs/Checkpoints.sol create mode 100644 contracts/src/utils/structs/checkpoints.rs diff --git a/contracts/src/utils/structs/Checkpoints.sol b/contracts/src/utils/structs/Checkpoints.sol new file mode 100644 index 00000000..5ba6ad92 --- /dev/null +++ b/contracts/src/utils/structs/Checkpoints.sol @@ -0,0 +1,606 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/structs/Checkpoints.sol) +// This file was procedurally generated from scripts/generate/templates/Checkpoints.js. + +pragma solidity ^0.8.20; + +import {Math} from "../math/Math.sol"; + +/** + * @dev This library defines the `Trace*` struct, for checkpointing values as they change at different points in + * time, and later looking up past values by block number. See {Votes} as an example. + * + * To create a history of checkpoints define a variable type `Checkpoints.Trace*` in your contract, and store a new + * checkpoint for the current transaction block using the {push} function. + */ +library Checkpoints { + /** + * @dev A value was attempted to be inserted on a past checkpoint. + */ + error CheckpointUnorderedInsertion(); + + struct Trace224 { + Checkpoint224[] _checkpoints; + } + + struct Checkpoint224 { + uint32 _key; + uint224 _value; + } + + /** + * @dev Pushes a (`key`, `value`) pair into a Trace224 so that it is stored as the checkpoint. + * + * Returns previous value and new value. + * + * IMPORTANT: Never accept `key` as a user input, since an arbitrary `type(uint32).max` key set will disable the + * library. + */ + function push(Trace224 storage self, uint32 key, uint224 value) internal returns (uint224, uint224) { + return _insert(self._checkpoints, key, value); + } + + /** + * @dev Returns the value in the first (oldest) checkpoint with key greater or equal than the search key, or zero if + * there is none. + */ + function lowerLookup(Trace224 storage self, uint32 key) internal view returns (uint224) { + uint256 len = self._checkpoints.length; + uint256 pos = _lowerBinaryLookup(self._checkpoints, key, 0, len); + return pos == len ? 0 : _unsafeAccess(self._checkpoints, pos)._value; + } + + /** + * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero + * if there is none. + */ + function upperLookup(Trace224 storage self, uint32 key) internal view returns (uint224) { + uint256 len = self._checkpoints.length; + uint256 pos = _upperBinaryLookup(self._checkpoints, key, 0, len); + return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; + } + + /** + * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero + * if there is none. + * + * NOTE: This is a variant of {upperLookup} that is optimised to find "recent" checkpoint (checkpoints with high + * keys). + */ + function upperLookupRecent(Trace224 storage self, uint32 key) internal view returns (uint224) { + uint256 len = self._checkpoints.length; + + uint256 low = 0; + uint256 high = len; + + if (len > 5) { + uint256 mid = len - Math.sqrt(len); + if (key < _unsafeAccess(self._checkpoints, mid)._key) { + high = mid; + } else { + low = mid + 1; + } + } + + uint256 pos = _upperBinaryLookup(self._checkpoints, key, low, high); + + return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; + } + + /** + * @dev Returns the value in the most recent checkpoint, or zero if there are no checkpoints. + */ + function latest(Trace224 storage self) internal view returns (uint224) { + uint256 pos = self._checkpoints.length; + return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; + } + + /** + * @dev Returns whether there is a checkpoint in the structure (i.e. it is not empty), and if so the key and value + * in the most recent checkpoint. + */ + function latestCheckpoint(Trace224 storage self) internal view returns (bool exists, uint32 _key, uint224 _value) { + uint256 pos = self._checkpoints.length; + if (pos == 0) { + return (false, 0, 0); + } else { + Checkpoint224 storage ckpt = _unsafeAccess(self._checkpoints, pos - 1); + return (true, ckpt._key, ckpt._value); + } + } + + /** + * @dev Returns the number of checkpoint. + */ + function length(Trace224 storage self) internal view returns (uint256) { + return self._checkpoints.length; + } + + /** + * @dev Returns checkpoint at given position. + */ + function at(Trace224 storage self, uint32 pos) internal view returns (Checkpoint224 memory) { + return self._checkpoints[pos]; + } + + /** + * @dev Pushes a (`key`, `value`) pair into an ordered list of checkpoints, either by inserting a new checkpoint, + * or by updating the last one. + */ + function _insert(Checkpoint224[] storage self, uint32 key, uint224 value) private returns (uint224, uint224) { + uint256 pos = self.length; + + if (pos > 0) { + Checkpoint224 storage last = _unsafeAccess(self, pos - 1); + uint32 lastKey = last._key; + uint224 lastValue = last._value; + + // Checkpoint keys must be non-decreasing. + if (lastKey > key) { + revert CheckpointUnorderedInsertion(); + } + + // Update or push new checkpoint + if (lastKey == key) { + _unsafeAccess(self, pos - 1)._value = value; + } else { + self.push(Checkpoint224({_key: key, _value: value})); + } + return (lastValue, value); + } else { + self.push(Checkpoint224({_key: key, _value: value})); + return (0, value); + } + } + + /** + * @dev Return the index of the last (most recent) checkpoint with key lower or equal than the search key, or `high` + * if there is none. `low` and `high` define a section where to do the search, with inclusive `low` and exclusive + * `high`. + * + * WARNING: `high` should not be greater than the array's length. + */ + function _upperBinaryLookup( + Checkpoint224[] storage self, + uint32 key, + uint256 low, + uint256 high + ) private view returns (uint256) { + while (low < high) { + uint256 mid = Math.average(low, high); + if (_unsafeAccess(self, mid)._key > key) { + high = mid; + } else { + low = mid + 1; + } + } + return high; + } + + /** + * @dev Return the index of the first (oldest) checkpoint with key is greater or equal than the search key, or + * `high` if there is none. `low` and `high` define a section where to do the search, with inclusive `low` and + * exclusive `high`. + * + * WARNING: `high` should not be greater than the array's length. + */ + function _lowerBinaryLookup( + Checkpoint224[] storage self, + uint32 key, + uint256 low, + uint256 high + ) private view returns (uint256) { + while (low < high) { + uint256 mid = Math.average(low, high); + if (_unsafeAccess(self, mid)._key < key) { + low = mid + 1; + } else { + high = mid; + } + } + return high; + } + + /** + * @dev Access an element of the array without performing bounds check. The position is assumed to be within bounds. + */ + function _unsafeAccess( + Checkpoint224[] storage self, + uint256 pos + ) private pure returns (Checkpoint224 storage result) { + assembly { + mstore(0, self.slot) + result.slot := add(keccak256(0, 0x20), pos) + } + } + + struct Trace208 { + Checkpoint208[] _checkpoints; + } + + struct Checkpoint208 { + uint48 _key; + uint208 _value; + } + + /** + * @dev Pushes a (`key`, `value`) pair into a Trace208 so that it is stored as the checkpoint. + * + * Returns previous value and new value. + * + * IMPORTANT: Never accept `key` as a user input, since an arbitrary `type(uint48).max` key set will disable the + * library. + */ + function push(Trace208 storage self, uint48 key, uint208 value) internal returns (uint208, uint208) { + return _insert(self._checkpoints, key, value); + } + + /** + * @dev Returns the value in the first (oldest) checkpoint with key greater or equal than the search key, or zero if + * there is none. + */ + function lowerLookup(Trace208 storage self, uint48 key) internal view returns (uint208) { + uint256 len = self._checkpoints.length; + uint256 pos = _lowerBinaryLookup(self._checkpoints, key, 0, len); + return pos == len ? 0 : _unsafeAccess(self._checkpoints, pos)._value; + } + + /** + * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero + * if there is none. + */ + function upperLookup(Trace208 storage self, uint48 key) internal view returns (uint208) { + uint256 len = self._checkpoints.length; + uint256 pos = _upperBinaryLookup(self._checkpoints, key, 0, len); + return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; + } + + /** + * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero + * if there is none. + * + * NOTE: This is a variant of {upperLookup} that is optimised to find "recent" checkpoint (checkpoints with high + * keys). + */ + function upperLookupRecent(Trace208 storage self, uint48 key) internal view returns (uint208) { + uint256 len = self._checkpoints.length; + + uint256 low = 0; + uint256 high = len; + + if (len > 5) { + uint256 mid = len - Math.sqrt(len); + if (key < _unsafeAccess(self._checkpoints, mid)._key) { + high = mid; + } else { + low = mid + 1; + } + } + + uint256 pos = _upperBinaryLookup(self._checkpoints, key, low, high); + + return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; + } + + /** + * @dev Returns the value in the most recent checkpoint, or zero if there are no checkpoints. + */ + function latest(Trace208 storage self) internal view returns (uint208) { + uint256 pos = self._checkpoints.length; + return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; + } + + /** + * @dev Returns whether there is a checkpoint in the structure (i.e. it is not empty), and if so the key and value + * in the most recent checkpoint. + */ + function latestCheckpoint(Trace208 storage self) internal view returns (bool exists, uint48 _key, uint208 _value) { + uint256 pos = self._checkpoints.length; + if (pos == 0) { + return (false, 0, 0); + } else { + Checkpoint208 storage ckpt = _unsafeAccess(self._checkpoints, pos - 1); + return (true, ckpt._key, ckpt._value); + } + } + + /** + * @dev Returns the number of checkpoint. + */ + function length(Trace208 storage self) internal view returns (uint256) { + return self._checkpoints.length; + } + + /** + * @dev Returns checkpoint at given position. + */ + function at(Trace208 storage self, uint32 pos) internal view returns (Checkpoint208 memory) { + return self._checkpoints[pos]; + } + + /** + * @dev Pushes a (`key`, `value`) pair into an ordered list of checkpoints, either by inserting a new checkpoint, + * or by updating the last one. + */ + function _insert(Checkpoint208[] storage self, uint48 key, uint208 value) private returns (uint208, uint208) { + uint256 pos = self.length; + + if (pos > 0) { + Checkpoint208 storage last = _unsafeAccess(self, pos - 1); + uint48 lastKey = last._key; + uint208 lastValue = last._value; + + // Checkpoint keys must be non-decreasing. + if (lastKey > key) { + revert CheckpointUnorderedInsertion(); + } + + // Update or push new checkpoint + if (lastKey == key) { + _unsafeAccess(self, pos - 1)._value = value; + } else { + self.push(Checkpoint208({_key: key, _value: value})); + } + return (lastValue, value); + } else { + self.push(Checkpoint208({_key: key, _value: value})); + return (0, value); + } + } + + /** + * @dev Return the index of the last (most recent) checkpoint with key lower or equal than the search key, or `high` + * if there is none. `low` and `high` define a section where to do the search, with inclusive `low` and exclusive + * `high`. + * + * WARNING: `high` should not be greater than the array's length. + */ + function _upperBinaryLookup( + Checkpoint208[] storage self, + uint48 key, + uint256 low, + uint256 high + ) private view returns (uint256) { + while (low < high) { + uint256 mid = Math.average(low, high); + if (_unsafeAccess(self, mid)._key > key) { + high = mid; + } else { + low = mid + 1; + } + } + return high; + } + + /** + * @dev Return the index of the first (oldest) checkpoint with key is greater or equal than the search key, or + * `high` if there is none. `low` and `high` define a section where to do the search, with inclusive `low` and + * exclusive `high`. + * + * WARNING: `high` should not be greater than the array's length. + */ + function _lowerBinaryLookup( + Checkpoint208[] storage self, + uint48 key, + uint256 low, + uint256 high + ) private view returns (uint256) { + while (low < high) { + uint256 mid = Math.average(low, high); + if (_unsafeAccess(self, mid)._key < key) { + low = mid + 1; + } else { + high = mid; + } + } + return high; + } + + /** + * @dev Access an element of the array without performing bounds check. The position is assumed to be within bounds. + */ + function _unsafeAccess( + Checkpoint208[] storage self, + uint256 pos + ) private pure returns (Checkpoint208 storage result) { + assembly { + mstore(0, self.slot) + result.slot := add(keccak256(0, 0x20), pos) + } + } + + struct Trace160 { + Checkpoint160[] _checkpoints; + } + + struct Checkpoint160 { + uint96 _key; + uint160 _value; + } + + /** + * @dev Pushes a (`key`, `value`) pair into a Trace160 so that it is stored as the checkpoint. + * + * Returns previous value and new value. + * + * IMPORTANT: Never accept `key` as a user input, since an arbitrary `type(uint96).max` key set will disable the + * library. + */ + function push(Trace160 storage self, uint96 key, uint160 value) internal returns (uint160, uint160) { + return _insert(self._checkpoints, key, value); + } + + /** + * @dev Returns the value in the first (oldest) checkpoint with key greater or equal than the search key, or zero if + * there is none. + */ + function lowerLookup(Trace160 storage self, uint96 key) internal view returns (uint160) { + uint256 len = self._checkpoints.length; + uint256 pos = _lowerBinaryLookup(self._checkpoints, key, 0, len); + return pos == len ? 0 : _unsafeAccess(self._checkpoints, pos)._value; + } + + /** + * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero + * if there is none. + */ + function upperLookup(Trace160 storage self, uint96 key) internal view returns (uint160) { + uint256 len = self._checkpoints.length; + uint256 pos = _upperBinaryLookup(self._checkpoints, key, 0, len); + return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; + } + + /** + * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero + * if there is none. + * + * NOTE: This is a variant of {upperLookup} that is optimised to find "recent" checkpoint (checkpoints with high + * keys). + */ + function upperLookupRecent(Trace160 storage self, uint96 key) internal view returns (uint160) { + uint256 len = self._checkpoints.length; + + uint256 low = 0; + uint256 high = len; + + if (len > 5) { + uint256 mid = len - Math.sqrt(len); + if (key < _unsafeAccess(self._checkpoints, mid)._key) { + high = mid; + } else { + low = mid + 1; + } + } + + uint256 pos = _upperBinaryLookup(self._checkpoints, key, low, high); + + return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; + } + + /** + * @dev Returns the value in the most recent checkpoint, or zero if there are no checkpoints. + */ + function latest(Trace160 storage self) internal view returns (uint160) { + uint256 pos = self._checkpoints.length; + return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; + } + + /** + * @dev Returns whether there is a checkpoint in the structure (i.e. it is not empty), and if so the key and value + * in the most recent checkpoint. + */ + function latestCheckpoint(Trace160 storage self) internal view returns (bool exists, uint96 _key, uint160 _value) { + uint256 pos = self._checkpoints.length; + if (pos == 0) { + return (false, 0, 0); + } else { + Checkpoint160 storage ckpt = _unsafeAccess(self._checkpoints, pos - 1); + return (true, ckpt._key, ckpt._value); + } + } + + /** + * @dev Returns the number of checkpoint. + */ + function length(Trace160 storage self) internal view returns (uint256) { + return self._checkpoints.length; + } + + /** + * @dev Returns checkpoint at given position. + */ + function at(Trace160 storage self, uint32 pos) internal view returns (Checkpoint160 memory) { + return self._checkpoints[pos]; + } + + /** + * @dev Pushes a (`key`, `value`) pair into an ordered list of checkpoints, either by inserting a new checkpoint, + * or by updating the last one. + */ + function _insert(Checkpoint160[] storage self, uint96 key, uint160 value) private returns (uint160, uint160) { + uint256 pos = self.length; + + if (pos > 0) { + Checkpoint160 storage last = _unsafeAccess(self, 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) { + _unsafeAccess(self, pos - 1)._value = value; + } else { + self.push(Checkpoint160({_key: key, _value: value})); + } + return (lastValue, value); + } else { + self.push(Checkpoint160({_key: key, _value: value})); + return (0, value); + } + } + + /** + * @dev Return the index of the last (most recent) checkpoint with key lower or equal than the search key, or `high` + * if there is none. `low` and `high` define a section where to do the search, with inclusive `low` and exclusive + * `high`. + * + * WARNING: `high` should not be greater than the array's length. + */ + function _upperBinaryLookup( + Checkpoint160[] storage self, + uint96 key, + uint256 low, + uint256 high + ) private view returns (uint256) { + while (low < high) { + uint256 mid = Math.average(low, high); + if (_unsafeAccess(self, mid)._key > key) { + high = mid; + } else { + low = mid + 1; + } + } + return high; + } + + /** + * @dev Return the index of the first (oldest) checkpoint with key is greater or equal than the search key, or + * `high` if there is none. `low` and `high` define a section where to do the search, with inclusive `low` and + * exclusive `high`. + * + * WARNING: `high` should not be greater than the array's length. + */ + function _lowerBinaryLookup( + Checkpoint160[] storage self, + uint96 key, + uint256 low, + uint256 high + ) private view returns (uint256) { + while (low < high) { + uint256 mid = Math.average(low, high); + if (_unsafeAccess(self, mid)._key < key) { + low = mid + 1; + } else { + high = mid; + } + } + return high; + } + + /** + * @dev Access an element of the array without performing bounds check. The position is assumed to be within bounds. + */ + function _unsafeAccess( + Checkpoint160[] storage self, + uint256 pos + ) private pure returns (Checkpoint160 storage result) { + assembly { + mstore(0, self.slot) + result.slot := add(keccak256(0, 0x20), pos) + } + } +} diff --git a/contracts/src/utils/structs/checkpoints.rs b/contracts/src/utils/structs/checkpoints.rs new file mode 100644 index 00000000..de042d42 --- /dev/null +++ b/contracts/src/utils/structs/checkpoints.rs @@ -0,0 +1,50 @@ +//! Contract module for checkpointing values as they +//! change at different points in time, and later looking up past values by +//! block number. See {Votes} as an example. To create a history of checkpoints +//! define a variable type `Checkpoints.Trace*` in your contract, and store a +//! new checkpoint for the current transaction block using the {push} function. +use alloy_primitives::{Uint, U256, U32}; +use alloy_sol_types::sol; +use stylus_proc::sol_storage; +type U96 = Uint<96, 2>; +type U160 = Uint<160, 3>; + +sol! { + /// A value was attempted to be inserted on a past checkpoint. + error CheckpointUnorderedInsertion(); +} + +sol_storage! { + struct Trace160 { + Checkpoint160[] _checkpoints; + } + + struct Checkpoint160 { + uint96 _key; + uint160 _value; + } +} + +impl Trace160 { + /** + * @dev Pushes a (`key`, `value`) pair into a Trace160 so that it is + * stored as the checkpoint. + * + * Returns previous value and new value. + * + * IMPORTANT: Never accept `key` as a user input, since an arbitrary + * `type(uint96).max` key set will disable the library. + */ + pub fn push(&mut self, key: U96, value: U160) -> (U160, U160) { + self._insert(key, value) + } + + /** + * @dev Pushes a (`key`, `value`) pair into an ordered list of + * checkpoints, either by inserting a new checkpoint, or by updating + * the last one. + */ + fn _insert(&mut self, key: U96, value: U160) -> (U160, U160) { + todo!() + } +} diff --git a/contracts/src/utils/structs/mod.rs b/contracts/src/utils/structs/mod.rs index f5510ad5..b6a94061 100644 --- a/contracts/src/utils/structs/mod.rs +++ b/contracts/src/utils/structs/mod.rs @@ -1,2 +1,3 @@ //! Solidity storage types used by other contracts. pub mod bitmap; +mod checkpoints; From e7424c947da94b357bd2239a7275f4465c930677 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Wed, 19 Jun 2024 18:30:56 +0400 Subject: [PATCH 02/95] ++ --- contracts/src/utils/structs/checkpoints.rs | 152 +++++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/contracts/src/utils/structs/checkpoints.rs b/contracts/src/utils/structs/checkpoints.rs index de042d42..e3fd3575 100644 --- a/contracts/src/utils/structs/checkpoints.rs +++ b/contracts/src/utils/structs/checkpoints.rs @@ -6,6 +6,8 @@ use alloy_primitives::{Uint, U256, U32}; use alloy_sol_types::sol; use stylus_proc::sol_storage; +use stylus_sdk::prelude::StorageType; + type U96 = Uint<96, 2>; type U160 = Uint<160, 3>; @@ -39,6 +41,107 @@ impl Trace160 { self._insert(key, value) } + /** + * @dev Returns the value in the first (oldest) checkpoint with key + * greater or equal than the search key, or zero if there is none. + */ + pub fn lower_lookup(&mut self, key: U96) -> U160 { + let len = self.length(); + let pos = self._lower_binary_lookup(key, U256::ZERO, len); + if pos == len { + U160::ZERO + } else { + self._unsafe_access_value(pos) + } + } + + /** + * @dev Returns the value in the last (most recent) checkpoint with key + * lower or equal than the search key, or zero if there is none. + */ + pub fn upper_lookup(&mut self, key: U96) -> U160 { + let len = self.length(); + let pos = self._lower_binary_lookup(key, U256::ZERO, len); + if pos == len { + U160::ZERO + } else { + self._unsafe_access_value(pos) + } + } + + /** + * @dev Returns the value in the last (most recent) checkpoint with key + * lower or equal than the search key, or zero if there is none. + * + * NOTE: This is a variant of {upperLookup} that is optimised to find + * "recent" checkpoint (checkpoints with high keys). + */ + pub fn upper_lookup_recent(&mut self, key: U96) -> U160 { + let len = self.length(); + + let mut low = U256::ZERO; + let mut high = len; + if len > U256::from(5) { + let mid = len - len.root(2); + if key < self._unsafe_access_key(mid) { + high = mid; + } else { + low = mid + U256::from(1); + } + } + + let pos = self._upper_binary_lookup(key, low, high); + + if pos == U256::ZERO { + U160::ZERO + } else { + self._unsafe_access_value(pos - U256::from(1)) + } + } + + /** + * @dev Returns the value in the most recent checkpoint, or zero if + * there are no checkpoints. + */ + pub fn latest(&mut self) -> U160 { + let pos = self.length(); + if pos == U256::ZERO { + U160::ZERO + } else { + self._unsafe_access_value(pos - U256::from(1)) + } + } + + /** + * @dev Returns whether there is a checkpoint in the structure (i.e. it + * is not empty), and if so the key and value in the most recent + * checkpoint. + */ + pub fn latest_checkpoint(&self) -> (bool, U96, U160) { + let pos = self.length(); + if pos == U256::ZERO { + (false, U96::ZERO, U160::ZERO) + } else { + let checkpoint = self._unsafe_access(pos - U256::from(1)); + (true, checkpoint._key.load(), checkpoint._value.load()) + } + } + + /** + * @dev Returns the number of checkpoint. + */ + pub fn length(&self) -> U256 { + // TODO#q: think how to retrieve U256 without conversion + U256::from(self._checkpoints.len()) + } + + /** + * @dev Returns checkpoint at given position. + */ + pub fn at(&self, pos: U32) -> Checkpoint160 { + unsafe { self._checkpoints.getter(pos).unwrap().into_raw() } + } + /** * @dev Pushes a (`key`, `value`) pair into an ordered list of * checkpoints, either by inserting a new checkpoint, or by updating @@ -47,4 +150,53 @@ impl Trace160 { fn _insert(&mut self, key: U96, value: U160) -> (U160, U160) { todo!() } + + /** + * @dev Return the index of the last (most recent) checkpoint with key + * lower or equal than the search key, or `high` if there is none. + * `low` and `high` define a section where to do the search, with + * inclusive `low` and exclusive `high`. + * + * WARNING: `high` should not be greater than the array's length. + */ + fn _upper_binary_lookup(&self, key: U96, low: U256, hight: U256) -> U256 { + todo!() + } + + /** + * @dev Return the index of the first (oldest) checkpoint with key is + * greater or equal than the search key, or `high` if there is none. + * `low` and `high` define a section where to do the search, with + * inclusive `low` and exclusive `high`. + * + * WARNING: `high` should not be greater than the array's length. + */ + fn _lower_binary_lookup(&self, key: U96, low: U256, high: U256) -> U256 { + todo!() + } + + /** + * @dev Access an element of the array without performing bounds check. + * The position is assumed to be within bounds. + */ + fn _unsafe_access(&self, pos: U256) -> Checkpoint160 { + // TODO#q: think how access it without bounds check + unsafe { self._checkpoints.getter(pos).unwrap().into_raw() } + } + + /// Access on a key + fn _unsafe_access_key(&self, pos: U256) -> U96 { + // TODO#q: think how access it without bounds check + let check_point = + self._checkpoints.get(pos).expect("get checkpoint by index"); + check_point._key.get() + } + + /// Access on a value + fn _unsafe_access_value(&self, pos: U256) -> U160 { + // TODO#q: think how access it without bounds check + let check_point = + self._checkpoints.get(pos).expect("get checkpoint by index"); + check_point._value.get() + } } From ee6c923e0f28360169ceb43c3ffbbb9800c06c4b Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Thu, 20 Jun 2024 16:15:04 +0400 Subject: [PATCH 03/95] ++ --- contracts/src/utils/math.rs | 73 ++++++++++++++++++++++ contracts/src/utils/mod.rs | 1 + contracts/src/utils/structs/checkpoints.rs | 8 ++- 3 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 contracts/src/utils/math.rs diff --git a/contracts/src/utils/math.rs b/contracts/src/utils/math.rs new file mode 100644 index 00000000..78b6cab9 --- /dev/null +++ b/contracts/src/utils/math.rs @@ -0,0 +1,73 @@ +use alloy_primitives::{uint, U256}; + +// TODO#q: use more smart way + +/** + * @dev Returns the square root of a number. If the number is not a perfect + * square, the value is rounded towards zero. + * + * This method is based on Newton's method for computing square roots; the + * algorithm is restricted to only using integer operations. + */ +pub fn sqrt(a: U256) -> U256 { + // TODO#q: refactor this + let one = uint!(1_U256); + if a <= one { + return a; + } + + let mut aa = a; + let mut xn = one; + + if aa >= (one << 128) { + aa >>= 128; + xn <<= 64; + } + if aa >= (one << 64) { + aa >>= 64; + xn <<= 32; + } + if aa >= (one << 32) { + aa >>= 32; + xn <<= 16; + } + if aa >= (one << 16) { + aa >>= 16; + xn <<= 8; + } + if aa >= (one << 8) { + aa >>= 8; + xn <<= 4; + } + if aa >= (one << 4) { + aa >>= 4; + xn <<= 2; + } + if aa >= (one << 2) { + xn <<= 1; + } + + xn = (uint!(3_U256) * xn) >> 1; + + xn = (xn + a / xn) >> 1; + xn = (xn + a / xn) >> 1; + xn = (xn + a / xn) >> 1; + xn = (xn + a / xn) >> 1; + xn = (xn + a / xn) >> 1; + xn = (xn + a / xn) >> 1; + + xn - U256::from(xn > a / xn) +} + +#[cfg(all(test, feature = "std"))] +mod tests { + use alloy_primitives::uint; + + use crate::utils::math::sqrt; + + #[test] + fn check_sqrt() { + // TODO#q: use proptest + assert_eq!(sqrt(uint!(27_U256)), uint!(5_U256)); + } +} diff --git a/contracts/src/utils/mod.rs b/contracts/src/utils/mod.rs index 14c84808..a0cbd236 100644 --- a/contracts/src/utils/mod.rs +++ b/contracts/src/utils/mod.rs @@ -1,5 +1,6 @@ //! Common Smart Contracts utilities. +pub mod math; pub mod structs; cfg_if::cfg_if! { diff --git a/contracts/src/utils/structs/checkpoints.rs b/contracts/src/utils/structs/checkpoints.rs index e3fd3575..855a20ce 100644 --- a/contracts/src/utils/structs/checkpoints.rs +++ b/contracts/src/utils/structs/checkpoints.rs @@ -3,11 +3,13 @@ //! block number. See {Votes} as an example. To create a history of checkpoints //! define a variable type `Checkpoints.Trace*` in your contract, and store a //! new checkpoint for the current transaction block using the {push} function. -use alloy_primitives::{Uint, U256, U32}; +use alloy_primitives::{uint, Uint, U256, U32}; use alloy_sol_types::sol; use stylus_proc::sol_storage; use stylus_sdk::prelude::StorageType; +use crate::utils::math::sqrt; + type U96 = Uint<96, 2>; type U160 = Uint<160, 3>; @@ -78,11 +80,13 @@ impl Trace160 { */ pub fn upper_lookup_recent(&mut self, key: U96) -> U160 { let len = self.length(); + // TODO#q: use uint!(1_U256); let mut low = U256::ZERO; let mut high = len; if len > U256::from(5) { - let mid = len - len.root(2); + // NOTE#q: square root from `ruint` crate works just with std + let mid = len - sqrt(len); if key < self._unsafe_access_key(mid) { high = mid; } else { From f373f7b67ded024b0c9c3e54ef58a0ce81929714 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Thu, 20 Jun 2024 19:16:23 +0400 Subject: [PATCH 04/95] ++ --- contracts/src/utils/math.rs | 8 +++ contracts/src/utils/structs/checkpoints.rs | 83 +++++++++++++++++++--- 2 files changed, 82 insertions(+), 9 deletions(-) diff --git a/contracts/src/utils/math.rs b/contracts/src/utils/math.rs index 78b6cab9..c35a7eda 100644 --- a/contracts/src/utils/math.rs +++ b/contracts/src/utils/math.rs @@ -59,6 +59,14 @@ pub fn sqrt(a: U256) -> U256 { xn - U256::from(xn > a / xn) } +/** + * @dev Returns the average of two numbers. The result is rounded towards + * zero. + */ +pub fn average(a: U256, b: U256) -> U256 { + (a & b) + (a ^ b) / uint!(2_U256) +} + #[cfg(all(test, feature = "std"))] mod tests { use alloy_primitives::uint; diff --git a/contracts/src/utils/structs/checkpoints.rs b/contracts/src/utils/structs/checkpoints.rs index 855a20ce..6ae98a7e 100644 --- a/contracts/src/utils/structs/checkpoints.rs +++ b/contracts/src/utils/structs/checkpoints.rs @@ -5,19 +5,25 @@ //! new checkpoint for the current transaction block using the {push} function. use alloy_primitives::{uint, Uint, U256, U32}; use alloy_sol_types::sol; -use stylus_proc::sol_storage; +use stylus_proc::{sol_storage, SolidityError}; use stylus_sdk::prelude::StorageType; -use crate::utils::math::sqrt; +use crate::utils::math::{average, sqrt}; type U96 = Uint<96, 2>; type U160 = Uint<160, 3>; sol! { /// A value was attempted to be inserted on a past checkpoint. + #[derive(Debug)] error CheckpointUnorderedInsertion(); } +#[derive(SolidityError, Debug)] +pub enum Error { + CheckpointUnorderedInsertion(CheckpointUnorderedInsertion), +} + sol_storage! { struct Trace160 { Checkpoint160[] _checkpoints; @@ -39,7 +45,11 @@ impl Trace160 { * IMPORTANT: Never accept `key` as a user input, since an arbitrary * `type(uint96).max` key set will disable the library. */ - pub fn push(&mut self, key: U96, value: U160) -> (U160, U160) { + pub fn push( + &mut self, + key: U96, + value: U160, + ) -> Result<(U160, U160), Error> { self._insert(key, value) } @@ -151,8 +161,37 @@ impl Trace160 { * checkpoints, either by inserting a new checkpoint, or by updating * the last one. */ - fn _insert(&mut self, key: U96, value: U160) -> (U160, U160) { - todo!() + fn _insert( + &mut self, + key: U96, + value: U160, + ) -> Result<(U160, U160), Error> { + let pos = self.length(); + if pos > U256::ZERO { + let last = self._unsafe_access(pos - uint!(1_U256)); + let last_key = last._key.get(); + let last_value = last._value.get(); + + // Checkpoint keys must be non-decreasing. + if last_key > key { + return Err(CheckpointUnorderedInsertion {}.into()); + } + + // Update or push new checkpoint + if last_key > key { + self._checkpoints + .setter(pos - uint!(1_U256)) + .unwrap() + ._value + .set(value); + } else { + self.push(key, value)?; + } + Ok((last_value, value)) + } else { + self.push(key, value)?; + Ok((U160::ZERO, value)) + } } /** @@ -163,8 +202,21 @@ impl Trace160 { * * WARNING: `high` should not be greater than the array's length. */ - fn _upper_binary_lookup(&self, key: U96, low: U256, hight: U256) -> U256 { - todo!() + fn _upper_binary_lookup( + &self, + key: U96, + mut low: U256, + mut high: U256, + ) -> U256 { + while low < high { + let mid = average(low, high); + if self._unsafe_access_key(mid) > key { + high = mid; + } else { + low = mid + uint!(1_U256); + } + } + high } /** @@ -175,8 +227,21 @@ impl Trace160 { * * WARNING: `high` should not be greater than the array's length. */ - fn _lower_binary_lookup(&self, key: U96, low: U256, high: U256) -> U256 { - todo!() + fn _lower_binary_lookup( + &self, + key: U96, + mut low: U256, + mut high: U256, + ) -> U256 { + while low < high { + let mid = average(low, high); + if self._unsafe_access_key(mid) < key { + low = mid + uint!(1_U256); + } else { + high = mid; + } + } + high } /** From a13560f0e8f5c688ec60376adb3e0658f4adb03f Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Thu, 20 Jun 2024 20:53:17 +0400 Subject: [PATCH 05/95] ++ --- contracts/src/utils/math.rs | 16 +--- contracts/src/utils/structs/checkpoints.rs | 104 ++++++++------------- 2 files changed, 45 insertions(+), 75 deletions(-) diff --git a/contracts/src/utils/math.rs b/contracts/src/utils/math.rs index c35a7eda..bf7991a7 100644 --- a/contracts/src/utils/math.rs +++ b/contracts/src/utils/math.rs @@ -2,13 +2,10 @@ use alloy_primitives::{uint, U256}; // TODO#q: use more smart way -/** - * @dev Returns the square root of a number. If the number is not a perfect - * square, the value is rounded towards zero. - * - * This method is based on Newton's method for computing square roots; the - * algorithm is restricted to only using integer operations. - */ +/// Returns the square root of a number. If the number is not a perfect +/// square, the value is rounded towards zero. +/// This method is based on Newton's method for computing square roots; the +/// algorithm is restricted to only using integer operations. pub fn sqrt(a: U256) -> U256 { // TODO#q: refactor this let one = uint!(1_U256); @@ -59,10 +56,7 @@ pub fn sqrt(a: U256) -> U256 { xn - U256::from(xn > a / xn) } -/** - * @dev Returns the average of two numbers. The result is rounded towards - * zero. - */ +/// Returns the average of two numbers. The result is rounded towards zero. pub fn average(a: U256, b: U256) -> U256 { (a & b) + (a ^ b) / uint!(2_U256) } diff --git a/contracts/src/utils/structs/checkpoints.rs b/contracts/src/utils/structs/checkpoints.rs index 6ae98a7e..7e6bcb92 100644 --- a/contracts/src/utils/structs/checkpoints.rs +++ b/contracts/src/utils/structs/checkpoints.rs @@ -36,15 +36,13 @@ sol_storage! { } impl Trace160 { - /** - * @dev Pushes a (`key`, `value`) pair into a Trace160 so that it is - * stored as the checkpoint. - * - * Returns previous value and new value. - * - * IMPORTANT: Never accept `key` as a user input, since an arbitrary - * `type(uint96).max` key set will disable the library. - */ + /// Pushes a (`key`, `value`) pair into a Trace160 so that it is + /// stored as the checkpoint. + /// + /// Returns previous value and new value. + /// + /// IMPORTANT: Never accept `key` as a user input, since an arbitrary + /// `type(uint96).max` key set will disable the library. pub fn push( &mut self, key: U96, @@ -53,10 +51,8 @@ impl Trace160 { self._insert(key, value) } - /** - * @dev Returns the value in the first (oldest) checkpoint with key - * greater or equal than the search key, or zero if there is none. - */ + /// Returns the value in the first (oldest) checkpoint with key + /// greater or equal than the search key, or zero if there is none. pub fn lower_lookup(&mut self, key: U96) -> U160 { let len = self.length(); let pos = self._lower_binary_lookup(key, U256::ZERO, len); @@ -67,10 +63,8 @@ impl Trace160 { } } - /** - * @dev Returns the value in the last (most recent) checkpoint with key - * lower or equal than the search key, or zero if there is none. - */ + /// Returns the value in the last (most recent) checkpoint with key + /// lower or equal than the search key, or zero if there is none. pub fn upper_lookup(&mut self, key: U96) -> U160 { let len = self.length(); let pos = self._lower_binary_lookup(key, U256::ZERO, len); @@ -81,13 +75,11 @@ impl Trace160 { } } - /** - * @dev Returns the value in the last (most recent) checkpoint with key - * lower or equal than the search key, or zero if there is none. - * - * NOTE: This is a variant of {upperLookup} that is optimised to find - * "recent" checkpoint (checkpoints with high keys). - */ + /// Returns the value in the last (most recent) checkpoint with key + /// lower or equal than the search key, or zero if there is none. + /// + /// NOTE: This is a variant of {upperLookup} that is optimised to find + /// "recent" checkpoint (checkpoints with high keys). pub fn upper_lookup_recent(&mut self, key: U96) -> U160 { let len = self.length(); // TODO#q: use uint!(1_U256); @@ -113,10 +105,8 @@ impl Trace160 { } } - /** - * @dev Returns the value in the most recent checkpoint, or zero if - * there are no checkpoints. - */ + /// Returns the value in the most recent checkpoint, or zero if + /// there are no checkpoints. pub fn latest(&mut self) -> U160 { let pos = self.length(); if pos == U256::ZERO { @@ -126,11 +116,9 @@ impl Trace160 { } } - /** - * @dev Returns whether there is a checkpoint in the structure (i.e. it - * is not empty), and if so the key and value in the most recent - * checkpoint. - */ + /// Returns whether there is a checkpoint in the structure (i.e. it + /// is not empty), and if so the key and value in the most recent + /// checkpoint. pub fn latest_checkpoint(&self) -> (bool, U96, U160) { let pos = self.length(); if pos == U256::ZERO { @@ -141,26 +129,20 @@ impl Trace160 { } } - /** - * @dev Returns the number of checkpoint. - */ + /// Returns the number of checkpoint. pub fn length(&self) -> U256 { // TODO#q: think how to retrieve U256 without conversion U256::from(self._checkpoints.len()) } - /** - * @dev Returns checkpoint at given position. - */ + /// Returns checkpoint at given position. pub fn at(&self, pos: U32) -> Checkpoint160 { unsafe { self._checkpoints.getter(pos).unwrap().into_raw() } } - /** - * @dev Pushes a (`key`, `value`) pair into an ordered list of - * checkpoints, either by inserting a new checkpoint, or by updating - * the last one. - */ + /// Pushes a (`key`, `value`) pair into an ordered list of + /// checkpoints, either by inserting a new checkpoint, or by updating + /// the last one. fn _insert( &mut self, key: U96, @@ -194,14 +176,12 @@ impl Trace160 { } } - /** - * @dev Return the index of the last (most recent) checkpoint with key - * lower or equal than the search key, or `high` if there is none. - * `low` and `high` define a section where to do the search, with - * inclusive `low` and exclusive `high`. - * - * WARNING: `high` should not be greater than the array's length. - */ + /// Return the index of the last (most recent) checkpoint with key + /// lower or equal than the search key, or `high` if there is none. + /// `low` and `high` define a section where to do the search, with + /// inclusive `low` and exclusive `high`. + /// + /// WARNING: `high` should not be greater than the array's length. fn _upper_binary_lookup( &self, key: U96, @@ -219,14 +199,12 @@ impl Trace160 { high } - /** - * @dev Return the index of the first (oldest) checkpoint with key is - * greater or equal than the search key, or `high` if there is none. - * `low` and `high` define a section where to do the search, with - * inclusive `low` and exclusive `high`. - * - * WARNING: `high` should not be greater than the array's length. - */ + /// Return the index of the first (oldest) checkpoint with key is + /// greater or equal than the search key, or `high` if there is none. + /// `low` and `high` define a section where to do the search, with + /// inclusive `low` and exclusive `high`. + /// + /// WARNING: `high` should not be greater than the array's length. fn _lower_binary_lookup( &self, key: U96, @@ -244,10 +222,8 @@ impl Trace160 { high } - /** - * @dev Access an element of the array without performing bounds check. - * The position is assumed to be within bounds. - */ + /// Access an element of the array without performing bounds check. + /// The position is assumed to be within bounds. fn _unsafe_access(&self, pos: U256) -> Checkpoint160 { // TODO#q: think how access it without bounds check unsafe { self._checkpoints.getter(pos).unwrap().into_raw() } From 14369c8cfe7f076314f290e8378c1715acbec62c Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Fri, 21 Jun 2024 11:43:55 +0400 Subject: [PATCH 06/95] ++ --- contracts/src/utils/math.rs | 95 ++++++++++++++++++++++++++++++++----- 1 file changed, 82 insertions(+), 13 deletions(-) diff --git a/contracts/src/utils/math.rs b/contracts/src/utils/math.rs index bf7991a7..537886cb 100644 --- a/contracts/src/utils/math.rs +++ b/contracts/src/utils/math.rs @@ -1,18 +1,31 @@ use alloy_primitives::{uint, U256}; -// TODO#q: use more smart way +// TODO#q: add those helpers as an extension to U256 /// Returns the square root of a number. If the number is not a perfect /// square, the value is rounded towards zero. /// This method is based on Newton's method for computing square roots; the /// algorithm is restricted to only using integer operations. pub fn sqrt(a: U256) -> U256 { - // TODO#q: refactor this let one = uint!(1_U256); if a <= one { return a; } + // In this function, we use Newton's method to get a root of `f(x) := x² - + // a`. It involves building a sequence x_n that converges toward + // sqrt(a). For each iteration x_n, we also define the error between the + // current value as `ε_n = | x_n - sqrt(a) |`. + // + // For our first estimation, we consider `e` the smallest power of 2 which + // is bigger than the square root of the target. (i.e. `2**(e-1) ≤ + // sqrt(a) < 2**e`). We know that `e ≤ 128` because `(2¹²⁸)² = 2²⁵⁶` is + // bigger than any uint256. + // + // By noticing that + // `2**(e-1) ≤ sqrt(a) < 2**e → (2**(e-1))² ≤ a < (2**e)² → 2**(2*e-2) ≤ a < + // 2**(2*e)` we can deduce that `e - 1` is `log2(a) / 2`. We can thus + // compute `x_n = 2**(e-1)` using a method similar to the msb function. let mut aa = a; let mut xn = one; @@ -44,32 +57,88 @@ pub fn sqrt(a: U256) -> U256 { xn <<= 1; } - xn = (uint!(3_U256) * xn) >> 1; + // We now have x_n such that `x_n = 2**(e-1) ≤ sqrt(a) < 2**e = 2 * x_n`. + // This implies ε_n ≤ 2**(e-1). + // + // We can refine our estimation by noticing that the middle of that interval + // minimizes the error. If we move x_n to equal 2**(e-1) + 2**(e-2), + // then we reduce the error to ε_n ≤ 2**(e-2). This is going to be our + // x_0 (and ε_0) + xn = (uint!(3_U256) * xn) >> 1; // ε_0 := | x_0 - sqrt(a) | ≤ 2**(e-2) - xn = (xn + a / xn) >> 1; - xn = (xn + a / xn) >> 1; - xn = (xn + a / xn) >> 1; - xn = (xn + a / xn) >> 1; - xn = (xn + a / xn) >> 1; - xn = (xn + a / xn) >> 1; + // From here, Newton's method give us: + // x_{n+1} = (x_n + a / x_n) / 2 + // + // One should note that: + // x_{n+1}² - a = ((x_n + a / x_n) / 2)² - a + // = ((x_n² + a) / (2 * x_n))² - a + // = (x_n⁴ + 2 * a * x_n² + a²) / (4 * x_n²) - a + // = (x_n⁴ + 2 * a * x_n² + a² - 4 * a * x_n²) / (4 * x_n²) + // = (x_n⁴ - 2 * a * x_n² + a²) / (4 * x_n²) + // = (x_n² - a)² / (2 * x_n)² + // = ((x_n² - a) / (2 * x_n))² + // ≥ 0 + // Which proves that for all n ≥ 1, sqrt(a) ≤ x_n + // + // This gives us the proof of quadratic convergence of the sequence: + // ε_{n+1} = | x_{n+1} - sqrt(a) | + // = | (x_n + a / x_n) / 2 - sqrt(a) | + // = | (x_n² + a - 2*x_n*sqrt(a)) / (2 * x_n) | + // = | (x_n - sqrt(a))² / (2 * x_n) | + // = | ε_n² / (2 * x_n) | + // = ε_n² / | (2 * x_n) | + // + // For the first iteration, we have a special case where x_0 is known: + // ε_1 = ε_0² / | (2 * x_0) | + // ≤ (2**(e-2))² / (2 * (2**(e-1) + 2**(e-2))) + // ≤ 2**(2*e-4) / (3 * 2**(e-1)) + // ≤ 2**(e-3) / 3 + // ≤ 2**(e-3-log2(3)) + // ≤ 2**(e-4.5) + // + // For the following iterations, we use the fact that, 2**(e-1) ≤ sqrt(a) ≤ + // x_n: ε_{n+1} = ε_n² / | (2 * x_n) | + // ≤ (2**(e-k))² / (2 * 2**(e-1)) + // ≤ 2**(2*e-2*k) / 2**e + // ≤ 2**(e-2*k) + xn = (xn + a / xn) >> 1; // ε_1 := | x_1 - sqrt(a) | ≤ 2**(e-4.5) -- special case, see above + xn = (xn + a / xn) >> 1; // ε_2 := | x_2 - sqrt(a) | ≤ 2**(e-9) -- general case with k = 4.5 + xn = (xn + a / xn) >> 1; // ε_3 := | x_3 - sqrt(a) | ≤ 2**(e-18) -- general case with k = 9 + xn = (xn + a / xn) >> 1; // ε_4 := | x_4 - sqrt(a) | ≤ 2**(e-36) -- general case with k = 18 + xn = (xn + a / xn) >> 1; // ε_5 := | x_5 - sqrt(a) | ≤ 2**(e-72) -- general case with k = 36 + xn = (xn + a / xn) >> 1; // ε_6 := | x_6 - sqrt(a) | ≤ 2**(e-144) -- general case with k = 72 + // Because e ≤ 128 (as discussed during the first estimation phase), we know + // have reached a precision ε_6 ≤ 2**(e-144) < 1. Given we're operating + // on integers, then we can ensure that xn is now either sqrt(a) or + // sqrt(a) + 1. xn - U256::from(xn > a / xn) } /// Returns the average of two numbers. The result is rounded towards zero. pub fn average(a: U256, b: U256) -> U256 { + // (a + b) / 2 can overflow. (a & b) + (a ^ b) / uint!(2_U256) } #[cfg(all(test, feature = "std"))] mod tests { - use alloy_primitives::uint; + use alloy_primitives::{private::proptest::proptest, uint, U256, U512}; - use crate::utils::math::sqrt; + use crate::utils::math::{average, sqrt}; #[test] fn check_sqrt() { - // TODO#q: use proptest - assert_eq!(sqrt(uint!(27_U256)), uint!(5_U256)); + proptest!(|(value: U256)| { + assert_eq!(sqrt(value), value.root(2)); + }); + } + + #[test] + fn check_average() { + proptest!(|(a: U256, b: U256)| { + let expected = (U512::from(a) + U512::from(b)) / uint!(2_U512); + assert_eq!(average(a, b), U256::from(expected)); + }); } } From 474ea9647b0251ec2aeccb249136ca37132787be Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Fri, 21 Jun 2024 11:46:24 +0400 Subject: [PATCH 07/95] ++ --- contracts/src/utils/math.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/contracts/src/utils/math.rs b/contracts/src/utils/math.rs index 537886cb..e5efbc0e 100644 --- a/contracts/src/utils/math.rs +++ b/contracts/src/utils/math.rs @@ -130,15 +130,16 @@ mod tests { #[test] fn check_sqrt() { proptest!(|(value: U256)| { + // NOTE: U256::root(..) method requires std. Can be used just in test assert_eq!(sqrt(value), value.root(2)); }); } #[test] fn check_average() { - proptest!(|(a: U256, b: U256)| { - let expected = (U512::from(a) + U512::from(b)) / uint!(2_U512); - assert_eq!(average(a, b), U256::from(expected)); + proptest!(|(left: U256, right: U256)| { + let expected = (U512::from(left) + U512::from(right)) / uint!(2_U512); + assert_eq!(average(left, right), U256::from(expected)); }); } } From e525ac18e4195b0884e076594080acdfbf9100a6 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Fri, 21 Jun 2024 13:05:47 +0400 Subject: [PATCH 08/95] ++ --- contracts/src/utils/math.rs | 241 +++++++++++---------- contracts/src/utils/structs/checkpoints.rs | 8 +- 2 files changed, 129 insertions(+), 120 deletions(-) diff --git a/contracts/src/utils/math.rs b/contracts/src/utils/math.rs index e5efbc0e..e2a08804 100644 --- a/contracts/src/utils/math.rs +++ b/contracts/src/utils/math.rs @@ -1,137 +1,146 @@ use alloy_primitives::{uint, U256}; -// TODO#q: add those helpers as an extension to U256 +pub trait Math{ + /// Returns the square root of a number. If the number is not a perfect + /// square, the value is rounded towards zero. + /// This method is based on Newton's method for computing square roots; the + /// algorithm is restricted to only using integer operations. + #[must_use] + fn sqrt(self) -> Self; -/// Returns the square root of a number. If the number is not a perfect -/// square, the value is rounded towards zero. -/// This method is based on Newton's method for computing square roots; the -/// algorithm is restricted to only using integer operations. -pub fn sqrt(a: U256) -> U256 { - let one = uint!(1_U256); - if a <= one { - return a; - } + /// Returns the average of two numbers. The result is rounded towards zero. + #[must_use] + fn average(self, rhs: Self) -> Self; +} - // In this function, we use Newton's method to get a root of `f(x) := x² - - // a`. It involves building a sequence x_n that converges toward - // sqrt(a). For each iteration x_n, we also define the error between the - // current value as `ε_n = | x_n - sqrt(a) |`. - // - // For our first estimation, we consider `e` the smallest power of 2 which - // is bigger than the square root of the target. (i.e. `2**(e-1) ≤ - // sqrt(a) < 2**e`). We know that `e ≤ 128` because `(2¹²⁸)² = 2²⁵⁶` is - // bigger than any uint256. - // - // By noticing that - // `2**(e-1) ≤ sqrt(a) < 2**e → (2**(e-1))² ≤ a < (2**e)² → 2**(2*e-2) ≤ a < - // 2**(2*e)` we can deduce that `e - 1` is `log2(a) / 2`. We can thus - // compute `x_n = 2**(e-1)` using a method similar to the msb function. - let mut aa = a; - let mut xn = one; +impl Math for U256 { + fn sqrt(self) -> Self { + let a = self; + let one = uint!(1_U256); + if a <= one { + return a; + } - if aa >= (one << 128) { - aa >>= 128; - xn <<= 64; - } - if aa >= (one << 64) { - aa >>= 64; - xn <<= 32; - } - if aa >= (one << 32) { - aa >>= 32; - xn <<= 16; - } - if aa >= (one << 16) { - aa >>= 16; - xn <<= 8; - } - if aa >= (one << 8) { - aa >>= 8; - xn <<= 4; - } - if aa >= (one << 4) { - aa >>= 4; - xn <<= 2; - } - if aa >= (one << 2) { - xn <<= 1; - } + // In this function, we use Newton's method to get a root of `f(x) := x² - + // a`. It involves building a sequence x_n that converges toward + // sqrt(a). For each iteration x_n, we also define the error between the + // current value as `ε_n = | x_n - sqrt(a) |`. + // + // For our first estimation, we consider `e` the smallest power of 2 which + // is bigger than the square root of the target. (i.e. `2**(e-1) ≤ + // sqrt(a) < 2**e`). We know that `e ≤ 128` because `(2¹²⁸)² = 2²⁵⁶` is + // bigger than any uint256. + // + // By noticing that + // `2**(e-1) ≤ sqrt(a) < 2**e → (2**(e-1))² ≤ a < (2**e)² → 2**(2*e-2) ≤ a < + // 2**(2*e)` we can deduce that `e - 1` is `log2(a) / 2`. We can thus + // compute `x_n = 2**(e-1)` using a method similar to the msb function. + let mut aa = a; + let mut xn = one; - // We now have x_n such that `x_n = 2**(e-1) ≤ sqrt(a) < 2**e = 2 * x_n`. - // This implies ε_n ≤ 2**(e-1). - // - // We can refine our estimation by noticing that the middle of that interval - // minimizes the error. If we move x_n to equal 2**(e-1) + 2**(e-2), - // then we reduce the error to ε_n ≤ 2**(e-2). This is going to be our - // x_0 (and ε_0) - xn = (uint!(3_U256) * xn) >> 1; // ε_0 := | x_0 - sqrt(a) | ≤ 2**(e-2) + if aa >= (one << 128) { + aa >>= 128; + xn <<= 64; + } + if aa >= (one << 64) { + aa >>= 64; + xn <<= 32; + } + if aa >= (one << 32) { + aa >>= 32; + xn <<= 16; + } + if aa >= (one << 16) { + aa >>= 16; + xn <<= 8; + } + if aa >= (one << 8) { + aa >>= 8; + xn <<= 4; + } + if aa >= (one << 4) { + aa >>= 4; + xn <<= 2; + } + if aa >= (one << 2) { + xn <<= 1; + } - // From here, Newton's method give us: - // x_{n+1} = (x_n + a / x_n) / 2 - // - // One should note that: - // x_{n+1}² - a = ((x_n + a / x_n) / 2)² - a - // = ((x_n² + a) / (2 * x_n))² - a - // = (x_n⁴ + 2 * a * x_n² + a²) / (4 * x_n²) - a - // = (x_n⁴ + 2 * a * x_n² + a² - 4 * a * x_n²) / (4 * x_n²) - // = (x_n⁴ - 2 * a * x_n² + a²) / (4 * x_n²) - // = (x_n² - a)² / (2 * x_n)² - // = ((x_n² - a) / (2 * x_n))² - // ≥ 0 - // Which proves that for all n ≥ 1, sqrt(a) ≤ x_n - // - // This gives us the proof of quadratic convergence of the sequence: - // ε_{n+1} = | x_{n+1} - sqrt(a) | - // = | (x_n + a / x_n) / 2 - sqrt(a) | - // = | (x_n² + a - 2*x_n*sqrt(a)) / (2 * x_n) | - // = | (x_n - sqrt(a))² / (2 * x_n) | - // = | ε_n² / (2 * x_n) | - // = ε_n² / | (2 * x_n) | - // - // For the first iteration, we have a special case where x_0 is known: - // ε_1 = ε_0² / | (2 * x_0) | - // ≤ (2**(e-2))² / (2 * (2**(e-1) + 2**(e-2))) - // ≤ 2**(2*e-4) / (3 * 2**(e-1)) - // ≤ 2**(e-3) / 3 - // ≤ 2**(e-3-log2(3)) - // ≤ 2**(e-4.5) - // - // For the following iterations, we use the fact that, 2**(e-1) ≤ sqrt(a) ≤ - // x_n: ε_{n+1} = ε_n² / | (2 * x_n) | - // ≤ (2**(e-k))² / (2 * 2**(e-1)) - // ≤ 2**(2*e-2*k) / 2**e - // ≤ 2**(e-2*k) - xn = (xn + a / xn) >> 1; // ε_1 := | x_1 - sqrt(a) | ≤ 2**(e-4.5) -- special case, see above - xn = (xn + a / xn) >> 1; // ε_2 := | x_2 - sqrt(a) | ≤ 2**(e-9) -- general case with k = 4.5 - xn = (xn + a / xn) >> 1; // ε_3 := | x_3 - sqrt(a) | ≤ 2**(e-18) -- general case with k = 9 - xn = (xn + a / xn) >> 1; // ε_4 := | x_4 - sqrt(a) | ≤ 2**(e-36) -- general case with k = 18 - xn = (xn + a / xn) >> 1; // ε_5 := | x_5 - sqrt(a) | ≤ 2**(e-72) -- general case with k = 36 - xn = (xn + a / xn) >> 1; // ε_6 := | x_6 - sqrt(a) | ≤ 2**(e-144) -- general case with k = 72 + // We now have x_n such that `x_n = 2**(e-1) ≤ sqrt(a) < 2**e = 2 * x_n`. + // This implies ε_n ≤ 2**(e-1). + // + // We can refine our estimation by noticing that the middle of that interval + // minimizes the error. If we move x_n to equal 2**(e-1) + 2**(e-2), + // then we reduce the error to ε_n ≤ 2**(e-2). This is going to be our + // x_0 (and ε_0) + xn = (uint!(3_U256) * xn) >> 1; // ε_0 := | x_0 - sqrt(a) | ≤ 2**(e-2) - // Because e ≤ 128 (as discussed during the first estimation phase), we know - // have reached a precision ε_6 ≤ 2**(e-144) < 1. Given we're operating - // on integers, then we can ensure that xn is now either sqrt(a) or - // sqrt(a) + 1. - xn - U256::from(xn > a / xn) -} + // From here, Newton's method give us: + // x_{n+1} = (x_n + a / x_n) / 2 + // + // One should note that: + // x_{n+1}² - a = ((x_n + a / x_n) / 2)² - a + // = ((x_n² + a) / (2 * x_n))² - a + // = (x_n⁴ + 2 * a * x_n² + a²) / (4 * x_n²) - a + // = (x_n⁴ + 2 * a * x_n² + a² - 4 * a * x_n²) / (4 * x_n²) + // = (x_n⁴ - 2 * a * x_n² + a²) / (4 * x_n²) + // = (x_n² - a)² / (2 * x_n)² + // = ((x_n² - a) / (2 * x_n))² + // ≥ 0 + // Which proves that for all n ≥ 1, sqrt(a) ≤ x_n + // + // This gives us the proof of quadratic convergence of the sequence: + // ε_{n+1} = | x_{n+1} - sqrt(a) | + // = | (x_n + a / x_n) / 2 - sqrt(a) | + // = | (x_n² + a - 2*x_n*sqrt(a)) / (2 * x_n) | + // = | (x_n - sqrt(a))² / (2 * x_n) | + // = | ε_n² / (2 * x_n) | + // = ε_n² / | (2 * x_n) | + // + // For the first iteration, we have a special case where x_0 is known: + // ε_1 = ε_0² / | (2 * x_0) | + // ≤ (2**(e-2))² / (2 * (2**(e-1) + 2**(e-2))) + // ≤ 2**(2*e-4) / (3 * 2**(e-1)) + // ≤ 2**(e-3) / 3 + // ≤ 2**(e-3-log2(3)) + // ≤ 2**(e-4.5) + // + // For the following iterations, we use the fact that, 2**(e-1) ≤ sqrt(a) ≤ + // x_n: ε_{n+1} = ε_n² / | (2 * x_n) | + // ≤ (2**(e-k))² / (2 * 2**(e-1)) + // ≤ 2**(2*e-2*k) / 2**e + // ≤ 2**(e-2*k) + xn = (xn + a / xn) >> 1; // ε_1 := | x_1 - sqrt(a) | ≤ 2**(e-4.5) -- special case, see above + xn = (xn + a / xn) >> 1; // ε_2 := | x_2 - sqrt(a) | ≤ 2**(e-9) -- general case with k = 4.5 + xn = (xn + a / xn) >> 1; // ε_3 := | x_3 - sqrt(a) | ≤ 2**(e-18) -- general case with k = 9 + xn = (xn + a / xn) >> 1; // ε_4 := | x_4 - sqrt(a) | ≤ 2**(e-36) -- general case with k = 18 + xn = (xn + a / xn) >> 1; // ε_5 := | x_5 - sqrt(a) | ≤ 2**(e-72) -- general case with k = 36 + xn = (xn + a / xn) >> 1; // ε_6 := | x_6 - sqrt(a) | ≤ 2**(e-144) -- general case with k = 72 -/// Returns the average of two numbers. The result is rounded towards zero. -pub fn average(a: U256, b: U256) -> U256 { - // (a + b) / 2 can overflow. - (a & b) + (a ^ b) / uint!(2_U256) + // Because e ≤ 128 (as discussed during the first estimation phase), we know + // have reached a precision ε_6 ≤ 2**(e-144) < 1. Given we're operating + // on integers, then we can ensure that xn is now either sqrt(a) or + // sqrt(a) + 1. + xn - U256::from(xn > a / xn) + } + + fn average(self, rhs: Self) -> Self { + // (self + rhs) / 2 can overflow. + (self & rhs) + (self ^ rhs) / uint!(2_U256) + } } #[cfg(all(test, feature = "std"))] mod tests { use alloy_primitives::{private::proptest::proptest, uint, U256, U512}; - use crate::utils::math::{average, sqrt}; + use crate::utils::math::Math; #[test] fn check_sqrt() { proptest!(|(value: U256)| { - // NOTE: U256::root(..) method requires std. Can be used just in test - assert_eq!(sqrt(value), value.root(2)); + // NOTE: U256::root(..) method requires std. Can be used just inside test. + assert_eq!(value.sqrt(), value.root(2)); }); } @@ -139,7 +148,7 @@ mod tests { fn check_average() { proptest!(|(left: U256, right: U256)| { let expected = (U512::from(left) + U512::from(right)) / uint!(2_U512); - assert_eq!(average(left, right), U256::from(expected)); + assert_eq!(left.average(right), U256::from(expected)); }); } } diff --git a/contracts/src/utils/structs/checkpoints.rs b/contracts/src/utils/structs/checkpoints.rs index 7e6bcb92..75c02f2f 100644 --- a/contracts/src/utils/structs/checkpoints.rs +++ b/contracts/src/utils/structs/checkpoints.rs @@ -8,7 +8,7 @@ use alloy_sol_types::sol; use stylus_proc::{sol_storage, SolidityError}; use stylus_sdk::prelude::StorageType; -use crate::utils::math::{average, sqrt}; +use crate::utils::math::{Math}; type U96 = Uint<96, 2>; type U160 = Uint<160, 3>; @@ -88,7 +88,7 @@ impl Trace160 { let mut high = len; if len > U256::from(5) { // NOTE#q: square root from `ruint` crate works just with std - let mid = len - sqrt(len); + let mid = len - len.sqrt(); if key < self._unsafe_access_key(mid) { high = mid; } else { @@ -189,7 +189,7 @@ impl Trace160 { mut high: U256, ) -> U256 { while low < high { - let mid = average(low, high); + let mid = low.average(high); if self._unsafe_access_key(mid) > key { high = mid; } else { @@ -212,7 +212,7 @@ impl Trace160 { mut high: U256, ) -> U256 { while low < high { - let mid = average(low, high); + let mid = low.average(high); if self._unsafe_access_key(mid) < key { low = mid + uint!(1_U256); } else { From 3379789ec6637b33c03e75cd81ae4b8ad294baeb Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Fri, 21 Jun 2024 13:12:26 +0400 Subject: [PATCH 09/95] ++ --- contracts/src/utils/math.rs | 62 +++++++++++++--------- contracts/src/utils/structs/checkpoints.rs | 2 +- 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/contracts/src/utils/math.rs b/contracts/src/utils/math.rs index e2a08804..2eb63f58 100644 --- a/contracts/src/utils/math.rs +++ b/contracts/src/utils/math.rs @@ -1,14 +1,24 @@ use alloy_primitives::{uint, U256}; -pub trait Math{ +/// Standard math utilities missing in `alloy_primitives`. +pub trait Math { /// Returns the square root of a number. If the number is not a perfect /// square, the value is rounded towards zero. /// This method is based on Newton's method for computing square roots; the /// algorithm is restricted to only using integer operations. + /// + /// # Arguments + /// + /// * `self` - value to perform square root operation onto. #[must_use] fn sqrt(self) -> Self; /// Returns the average of two numbers. The result is rounded towards zero. + /// + /// # Arguments + /// + /// * `self` - first value to compute average. + /// * `rhs` - second value to compute average. #[must_use] fn average(self, rhs: Self) -> Self; } @@ -21,20 +31,21 @@ impl Math for U256 { return a; } - // In this function, we use Newton's method to get a root of `f(x) := x² - - // a`. It involves building a sequence x_n that converges toward - // sqrt(a). For each iteration x_n, we also define the error between the - // current value as `ε_n = | x_n - sqrt(a) |`. + // In this function, we use Newton's method to get a root of `f(x) := x² + // - a`. It involves building a sequence x_n that converges + // toward sqrt(a). For each iteration x_n, we also define the + // error between the current value as `ε_n = | x_n - sqrt(a) |`. // - // For our first estimation, we consider `e` the smallest power of 2 which - // is bigger than the square root of the target. (i.e. `2**(e-1) ≤ - // sqrt(a) < 2**e`). We know that `e ≤ 128` because `(2¹²⁸)² = 2²⁵⁶` is - // bigger than any uint256. + // For our first estimation, we consider `e` the smallest power of 2 + // which is bigger than the square root of the target. (i.e. + // `2**(e-1) ≤ sqrt(a) < 2**e`). We know that `e ≤ 128` because + // `(2¹²⁸)² = 2²⁵⁶` is bigger than any uint256. // // By noticing that - // `2**(e-1) ≤ sqrt(a) < 2**e → (2**(e-1))² ≤ a < (2**e)² → 2**(2*e-2) ≤ a < - // 2**(2*e)` we can deduce that `e - 1` is `log2(a) / 2`. We can thus - // compute `x_n = 2**(e-1)` using a method similar to the msb function. + // `2**(e-1) ≤ sqrt(a) < 2**e → (2**(e-1))² ≤ a < (2**e)² → 2**(2*e-2) ≤ + // a < 2**(2*e)` we can deduce that `e - 1` is `log2(a) / 2`. We + // can thus compute `x_n = 2**(e-1)` using a method similar to + // the msb function. let mut aa = a; let mut xn = one; @@ -66,13 +77,13 @@ impl Math for U256 { xn <<= 1; } - // We now have x_n such that `x_n = 2**(e-1) ≤ sqrt(a) < 2**e = 2 * x_n`. - // This implies ε_n ≤ 2**(e-1). + // We now have x_n such that `x_n = 2**(e-1) ≤ sqrt(a) < 2**e = 2 * + // x_n`. This implies ε_n ≤ 2**(e-1). // - // We can refine our estimation by noticing that the middle of that interval - // minimizes the error. If we move x_n to equal 2**(e-1) + 2**(e-2), - // then we reduce the error to ε_n ≤ 2**(e-2). This is going to be our - // x_0 (and ε_0) + // We can refine our estimation by noticing that the middle of that + // interval minimizes the error. If we move x_n to equal + // 2**(e-1) + 2**(e-2), then we reduce the error to ε_n ≤ + // 2**(e-2). This is going to be our x_0 (and ε_0) xn = (uint!(3_U256) * xn) >> 1; // ε_0 := | x_0 - sqrt(a) | ≤ 2**(e-2) // From here, Newton's method give us: @@ -105,8 +116,8 @@ impl Math for U256 { // ≤ 2**(e-3-log2(3)) // ≤ 2**(e-4.5) // - // For the following iterations, we use the fact that, 2**(e-1) ≤ sqrt(a) ≤ - // x_n: ε_{n+1} = ε_n² / | (2 * x_n) | + // For the following iterations, we use the fact that, 2**(e-1) ≤ + // sqrt(a) ≤ x_n: ε_{n+1} = ε_n² / | (2 * x_n) | // ≤ (2**(e-k))² / (2 * 2**(e-1)) // ≤ 2**(2*e-2*k) / 2**e // ≤ 2**(e-2*k) @@ -117,10 +128,10 @@ impl Math for U256 { xn = (xn + a / xn) >> 1; // ε_5 := | x_5 - sqrt(a) | ≤ 2**(e-72) -- general case with k = 36 xn = (xn + a / xn) >> 1; // ε_6 := | x_6 - sqrt(a) | ≤ 2**(e-144) -- general case with k = 72 - // Because e ≤ 128 (as discussed during the first estimation phase), we know - // have reached a precision ε_6 ≤ 2**(e-144) < 1. Given we're operating - // on integers, then we can ensure that xn is now either sqrt(a) or - // sqrt(a) + 1. + // Because e ≤ 128 (as discussed during the first estimation phase), we + // know have reached a precision ε_6 ≤ 2**(e-144) < 1. Given + // we're operating on integers, then we can ensure that xn is + // now either sqrt(a) or sqrt(a) + 1. xn - U256::from(xn > a / xn) } @@ -139,7 +150,7 @@ mod tests { #[test] fn check_sqrt() { proptest!(|(value: U256)| { - // NOTE: U256::root(..) method requires std. Can be used just inside test. + // U256::root(..) method requires std. Can be used just inside test. assert_eq!(value.sqrt(), value.root(2)); }); } @@ -147,6 +158,7 @@ mod tests { #[test] fn check_average() { proptest!(|(left: U256, right: U256)| { + // compute average in straight forward way with overflow and downcast. let expected = (U512::from(left) + U512::from(right)) / uint!(2_U512); assert_eq!(left.average(right), U256::from(expected)); }); diff --git a/contracts/src/utils/structs/checkpoints.rs b/contracts/src/utils/structs/checkpoints.rs index 75c02f2f..674102bb 100644 --- a/contracts/src/utils/structs/checkpoints.rs +++ b/contracts/src/utils/structs/checkpoints.rs @@ -8,7 +8,7 @@ use alloy_sol_types::sol; use stylus_proc::{sol_storage, SolidityError}; use stylus_sdk::prelude::StorageType; -use crate::utils::math::{Math}; +use crate::utils::math::Math; type U96 = Uint<96, 2>; type U160 = Uint<160, 3>; From 50171d87fdc19e00bea1602a7f55c9a5cef96290 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Fri, 21 Jun 2024 15:59:48 +0400 Subject: [PATCH 10/95] ++ --- contracts/src/lib.rs | 1 - contracts/src/token/erc721/mod.rs | 2 +- contracts/src/utils/{math.rs => math/alloy.rs} | 2 +- contracts/src/utils/math/mod.rs | 3 +++ contracts/src/{arithmetic.rs => utils/math/storage.rs} | 0 contracts/src/utils/structs/checkpoints.rs | 2 +- 6 files changed, 6 insertions(+), 4 deletions(-) rename contracts/src/utils/{math.rs => math/alloy.rs} (99%) create mode 100644 contracts/src/utils/math/mod.rs rename contracts/src/{arithmetic.rs => utils/math/storage.rs} (100%) diff --git a/contracts/src/lib.rs b/contracts/src/lib.rs index 1a2ba325..9ef4814e 100644 --- a/contracts/src/lib.rs +++ b/contracts/src/lib.rs @@ -10,7 +10,6 @@ static ALLOC: mini_alloc::MiniAlloc = mini_alloc::MiniAlloc::INIT; #[cfg(any(feature = "std", feature = "access"))] pub mod access; -mod arithmetic; pub mod token; diff --git a/contracts/src/token/erc721/mod.rs b/contracts/src/token/erc721/mod.rs index fcea565e..7b7d9441 100644 --- a/contracts/src/token/erc721/mod.rs +++ b/contracts/src/token/erc721/mod.rs @@ -6,7 +6,7 @@ use stylus_sdk::{ abi::Bytes, alloy_sol_types::sol, call::Call, evm, msg, prelude::*, }; -use crate::arithmetic::{AddAssignUnchecked, SubAssignUnchecked}; +use crate::utils::math::storage::{AddAssignUnchecked, SubAssignUnchecked}; pub mod extensions; diff --git a/contracts/src/utils/math.rs b/contracts/src/utils/math/alloy.rs similarity index 99% rename from contracts/src/utils/math.rs rename to contracts/src/utils/math/alloy.rs index 2eb63f58..9525619a 100644 --- a/contracts/src/utils/math.rs +++ b/contracts/src/utils/math/alloy.rs @@ -145,7 +145,7 @@ impl Math for U256 { mod tests { use alloy_primitives::{private::proptest::proptest, uint, U256, U512}; - use crate::utils::math::Math; + use crate::utils::math::alloy::Math; #[test] fn check_sqrt() { diff --git a/contracts/src/utils/math/mod.rs b/contracts/src/utils/math/mod.rs new file mode 100644 index 00000000..8676ff91 --- /dev/null +++ b/contracts/src/utils/math/mod.rs @@ -0,0 +1,3 @@ +//! Math helpers for e.g. alloy and solidity storage types. +pub mod alloy; +pub mod storage; diff --git a/contracts/src/arithmetic.rs b/contracts/src/utils/math/storage.rs similarity index 100% rename from contracts/src/arithmetic.rs rename to contracts/src/utils/math/storage.rs diff --git a/contracts/src/utils/structs/checkpoints.rs b/contracts/src/utils/structs/checkpoints.rs index 674102bb..081b710f 100644 --- a/contracts/src/utils/structs/checkpoints.rs +++ b/contracts/src/utils/structs/checkpoints.rs @@ -8,7 +8,7 @@ use alloy_sol_types::sol; use stylus_proc::{sol_storage, SolidityError}; use stylus_sdk::prelude::StorageType; -use crate::utils::math::Math; +use crate::utils::math::alloy::Math; type U96 = Uint<96, 2>; type U160 = Uint<160, 3>; From d84eddb8cb137b0c9156e7dd6fa02961c9f874c2 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Fri, 21 Jun 2024 16:08:34 +0400 Subject: [PATCH 11/95] ++ --- contracts/src/utils/math/storage.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/contracts/src/utils/math/storage.rs b/contracts/src/utils/math/storage.rs index e4cd10dc..47f7e063 100644 --- a/contracts/src/utils/math/storage.rs +++ b/contracts/src/utils/math/storage.rs @@ -1,12 +1,14 @@ -use alloy_primitives::U256; +use alloy_primitives::Uint; use stylus_sdk::storage::{StorageGuardMut, StorageUint}; pub(crate) trait AddAssignUnchecked { fn add_assign_unchecked(&mut self, rhs: T); } -impl<'a> AddAssignUnchecked for StorageGuardMut<'a, StorageUint<256, 4>> { - fn add_assign_unchecked(&mut self, rhs: U256) { +impl<'a, const B: usize, const L: usize> AddAssignUnchecked> + for StorageGuardMut<'a, StorageUint> +{ + fn add_assign_unchecked(&mut self, rhs: Uint) { let new_balance = self.get() + rhs; self.set(new_balance); } @@ -16,8 +18,10 @@ pub(crate) trait SubAssignUnchecked { fn sub_assign_unchecked(&mut self, rhs: T); } -impl<'a> SubAssignUnchecked for StorageGuardMut<'a, StorageUint<256, 4>> { - fn sub_assign_unchecked(&mut self, rhs: U256) { +impl<'a, const B: usize, const L: usize> SubAssignUnchecked> + for StorageGuardMut<'a, StorageUint> +{ + fn sub_assign_unchecked(&mut self, rhs: Uint) { let new_balance = self.get() - rhs; self.set(new_balance); } From 3df4f82bedb63266d5acf1e130ef77ee3d733fd4 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Fri, 21 Jun 2024 16:52:14 +0400 Subject: [PATCH 12/95] ++ --- contracts/src/utils/math/alloy.rs | 3 ++- contracts/src/utils/math/storage.rs | 1 + contracts/src/utils/structs/checkpoints.rs | 2 -- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/src/utils/math/alloy.rs b/contracts/src/utils/math/alloy.rs index 9525619a..c270fb5c 100644 --- a/contracts/src/utils/math/alloy.rs +++ b/contracts/src/utils/math/alloy.rs @@ -1,6 +1,7 @@ +//! Standard math utilities missing in `alloy_primitives`. use alloy_primitives::{uint, U256}; -/// Standard math utilities missing in `alloy_primitives`. +/// Trait for standard math utilities missing in `alloy_primitives`. pub trait Math { /// Returns the square root of a number. If the number is not a perfect /// square, the value is rounded towards zero. diff --git a/contracts/src/utils/math/storage.rs b/contracts/src/utils/math/storage.rs index 47f7e063..31193c47 100644 --- a/contracts/src/utils/math/storage.rs +++ b/contracts/src/utils/math/storage.rs @@ -1,3 +1,4 @@ +//! Simple math operations missing in `stylus_sdk::storage`. use alloy_primitives::Uint; use stylus_sdk::storage::{StorageGuardMut, StorageUint}; diff --git a/contracts/src/utils/structs/checkpoints.rs b/contracts/src/utils/structs/checkpoints.rs index 081b710f..eea820c5 100644 --- a/contracts/src/utils/structs/checkpoints.rs +++ b/contracts/src/utils/structs/checkpoints.rs @@ -82,12 +82,10 @@ impl Trace160 { /// "recent" checkpoint (checkpoints with high keys). pub fn upper_lookup_recent(&mut self, key: U96) -> U160 { let len = self.length(); - // TODO#q: use uint!(1_U256); let mut low = U256::ZERO; let mut high = len; if len > U256::from(5) { - // NOTE#q: square root from `ruint` crate works just with std let mid = len - len.sqrt(); if key < self._unsafe_access_key(mid) { high = mid; From 0fe00dca25a464adfe2232c9fef364c7be0df002 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Fri, 21 Jun 2024 20:31:30 +0400 Subject: [PATCH 13/95] ++ --- contracts/src/utils/structs/Checkpoints.sol | 606 -------------------- contracts/src/utils/structs/checkpoints.rs | 14 +- 2 files changed, 7 insertions(+), 613 deletions(-) delete mode 100644 contracts/src/utils/structs/Checkpoints.sol diff --git a/contracts/src/utils/structs/Checkpoints.sol b/contracts/src/utils/structs/Checkpoints.sol deleted file mode 100644 index 5ba6ad92..00000000 --- a/contracts/src/utils/structs/Checkpoints.sol +++ /dev/null @@ -1,606 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (utils/structs/Checkpoints.sol) -// This file was procedurally generated from scripts/generate/templates/Checkpoints.js. - -pragma solidity ^0.8.20; - -import {Math} from "../math/Math.sol"; - -/** - * @dev This library defines the `Trace*` struct, for checkpointing values as they change at different points in - * time, and later looking up past values by block number. See {Votes} as an example. - * - * To create a history of checkpoints define a variable type `Checkpoints.Trace*` in your contract, and store a new - * checkpoint for the current transaction block using the {push} function. - */ -library Checkpoints { - /** - * @dev A value was attempted to be inserted on a past checkpoint. - */ - error CheckpointUnorderedInsertion(); - - struct Trace224 { - Checkpoint224[] _checkpoints; - } - - struct Checkpoint224 { - uint32 _key; - uint224 _value; - } - - /** - * @dev Pushes a (`key`, `value`) pair into a Trace224 so that it is stored as the checkpoint. - * - * Returns previous value and new value. - * - * IMPORTANT: Never accept `key` as a user input, since an arbitrary `type(uint32).max` key set will disable the - * library. - */ - function push(Trace224 storage self, uint32 key, uint224 value) internal returns (uint224, uint224) { - return _insert(self._checkpoints, key, value); - } - - /** - * @dev Returns the value in the first (oldest) checkpoint with key greater or equal than the search key, or zero if - * there is none. - */ - function lowerLookup(Trace224 storage self, uint32 key) internal view returns (uint224) { - uint256 len = self._checkpoints.length; - uint256 pos = _lowerBinaryLookup(self._checkpoints, key, 0, len); - return pos == len ? 0 : _unsafeAccess(self._checkpoints, pos)._value; - } - - /** - * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero - * if there is none. - */ - function upperLookup(Trace224 storage self, uint32 key) internal view returns (uint224) { - uint256 len = self._checkpoints.length; - uint256 pos = _upperBinaryLookup(self._checkpoints, key, 0, len); - return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; - } - - /** - * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero - * if there is none. - * - * NOTE: This is a variant of {upperLookup} that is optimised to find "recent" checkpoint (checkpoints with high - * keys). - */ - function upperLookupRecent(Trace224 storage self, uint32 key) internal view returns (uint224) { - uint256 len = self._checkpoints.length; - - uint256 low = 0; - uint256 high = len; - - if (len > 5) { - uint256 mid = len - Math.sqrt(len); - if (key < _unsafeAccess(self._checkpoints, mid)._key) { - high = mid; - } else { - low = mid + 1; - } - } - - uint256 pos = _upperBinaryLookup(self._checkpoints, key, low, high); - - return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; - } - - /** - * @dev Returns the value in the most recent checkpoint, or zero if there are no checkpoints. - */ - function latest(Trace224 storage self) internal view returns (uint224) { - uint256 pos = self._checkpoints.length; - return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; - } - - /** - * @dev Returns whether there is a checkpoint in the structure (i.e. it is not empty), and if so the key and value - * in the most recent checkpoint. - */ - function latestCheckpoint(Trace224 storage self) internal view returns (bool exists, uint32 _key, uint224 _value) { - uint256 pos = self._checkpoints.length; - if (pos == 0) { - return (false, 0, 0); - } else { - Checkpoint224 storage ckpt = _unsafeAccess(self._checkpoints, pos - 1); - return (true, ckpt._key, ckpt._value); - } - } - - /** - * @dev Returns the number of checkpoint. - */ - function length(Trace224 storage self) internal view returns (uint256) { - return self._checkpoints.length; - } - - /** - * @dev Returns checkpoint at given position. - */ - function at(Trace224 storage self, uint32 pos) internal view returns (Checkpoint224 memory) { - return self._checkpoints[pos]; - } - - /** - * @dev Pushes a (`key`, `value`) pair into an ordered list of checkpoints, either by inserting a new checkpoint, - * or by updating the last one. - */ - function _insert(Checkpoint224[] storage self, uint32 key, uint224 value) private returns (uint224, uint224) { - uint256 pos = self.length; - - if (pos > 0) { - Checkpoint224 storage last = _unsafeAccess(self, pos - 1); - uint32 lastKey = last._key; - uint224 lastValue = last._value; - - // Checkpoint keys must be non-decreasing. - if (lastKey > key) { - revert CheckpointUnorderedInsertion(); - } - - // Update or push new checkpoint - if (lastKey == key) { - _unsafeAccess(self, pos - 1)._value = value; - } else { - self.push(Checkpoint224({_key: key, _value: value})); - } - return (lastValue, value); - } else { - self.push(Checkpoint224({_key: key, _value: value})); - return (0, value); - } - } - - /** - * @dev Return the index of the last (most recent) checkpoint with key lower or equal than the search key, or `high` - * if there is none. `low` and `high` define a section where to do the search, with inclusive `low` and exclusive - * `high`. - * - * WARNING: `high` should not be greater than the array's length. - */ - function _upperBinaryLookup( - Checkpoint224[] storage self, - uint32 key, - uint256 low, - uint256 high - ) private view returns (uint256) { - while (low < high) { - uint256 mid = Math.average(low, high); - if (_unsafeAccess(self, mid)._key > key) { - high = mid; - } else { - low = mid + 1; - } - } - return high; - } - - /** - * @dev Return the index of the first (oldest) checkpoint with key is greater or equal than the search key, or - * `high` if there is none. `low` and `high` define a section where to do the search, with inclusive `low` and - * exclusive `high`. - * - * WARNING: `high` should not be greater than the array's length. - */ - function _lowerBinaryLookup( - Checkpoint224[] storage self, - uint32 key, - uint256 low, - uint256 high - ) private view returns (uint256) { - while (low < high) { - uint256 mid = Math.average(low, high); - if (_unsafeAccess(self, mid)._key < key) { - low = mid + 1; - } else { - high = mid; - } - } - return high; - } - - /** - * @dev Access an element of the array without performing bounds check. The position is assumed to be within bounds. - */ - function _unsafeAccess( - Checkpoint224[] storage self, - uint256 pos - ) private pure returns (Checkpoint224 storage result) { - assembly { - mstore(0, self.slot) - result.slot := add(keccak256(0, 0x20), pos) - } - } - - struct Trace208 { - Checkpoint208[] _checkpoints; - } - - struct Checkpoint208 { - uint48 _key; - uint208 _value; - } - - /** - * @dev Pushes a (`key`, `value`) pair into a Trace208 so that it is stored as the checkpoint. - * - * Returns previous value and new value. - * - * IMPORTANT: Never accept `key` as a user input, since an arbitrary `type(uint48).max` key set will disable the - * library. - */ - function push(Trace208 storage self, uint48 key, uint208 value) internal returns (uint208, uint208) { - return _insert(self._checkpoints, key, value); - } - - /** - * @dev Returns the value in the first (oldest) checkpoint with key greater or equal than the search key, or zero if - * there is none. - */ - function lowerLookup(Trace208 storage self, uint48 key) internal view returns (uint208) { - uint256 len = self._checkpoints.length; - uint256 pos = _lowerBinaryLookup(self._checkpoints, key, 0, len); - return pos == len ? 0 : _unsafeAccess(self._checkpoints, pos)._value; - } - - /** - * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero - * if there is none. - */ - function upperLookup(Trace208 storage self, uint48 key) internal view returns (uint208) { - uint256 len = self._checkpoints.length; - uint256 pos = _upperBinaryLookup(self._checkpoints, key, 0, len); - return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; - } - - /** - * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero - * if there is none. - * - * NOTE: This is a variant of {upperLookup} that is optimised to find "recent" checkpoint (checkpoints with high - * keys). - */ - function upperLookupRecent(Trace208 storage self, uint48 key) internal view returns (uint208) { - uint256 len = self._checkpoints.length; - - uint256 low = 0; - uint256 high = len; - - if (len > 5) { - uint256 mid = len - Math.sqrt(len); - if (key < _unsafeAccess(self._checkpoints, mid)._key) { - high = mid; - } else { - low = mid + 1; - } - } - - uint256 pos = _upperBinaryLookup(self._checkpoints, key, low, high); - - return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; - } - - /** - * @dev Returns the value in the most recent checkpoint, or zero if there are no checkpoints. - */ - function latest(Trace208 storage self) internal view returns (uint208) { - uint256 pos = self._checkpoints.length; - return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; - } - - /** - * @dev Returns whether there is a checkpoint in the structure (i.e. it is not empty), and if so the key and value - * in the most recent checkpoint. - */ - function latestCheckpoint(Trace208 storage self) internal view returns (bool exists, uint48 _key, uint208 _value) { - uint256 pos = self._checkpoints.length; - if (pos == 0) { - return (false, 0, 0); - } else { - Checkpoint208 storage ckpt = _unsafeAccess(self._checkpoints, pos - 1); - return (true, ckpt._key, ckpt._value); - } - } - - /** - * @dev Returns the number of checkpoint. - */ - function length(Trace208 storage self) internal view returns (uint256) { - return self._checkpoints.length; - } - - /** - * @dev Returns checkpoint at given position. - */ - function at(Trace208 storage self, uint32 pos) internal view returns (Checkpoint208 memory) { - return self._checkpoints[pos]; - } - - /** - * @dev Pushes a (`key`, `value`) pair into an ordered list of checkpoints, either by inserting a new checkpoint, - * or by updating the last one. - */ - function _insert(Checkpoint208[] storage self, uint48 key, uint208 value) private returns (uint208, uint208) { - uint256 pos = self.length; - - if (pos > 0) { - Checkpoint208 storage last = _unsafeAccess(self, pos - 1); - uint48 lastKey = last._key; - uint208 lastValue = last._value; - - // Checkpoint keys must be non-decreasing. - if (lastKey > key) { - revert CheckpointUnorderedInsertion(); - } - - // Update or push new checkpoint - if (lastKey == key) { - _unsafeAccess(self, pos - 1)._value = value; - } else { - self.push(Checkpoint208({_key: key, _value: value})); - } - return (lastValue, value); - } else { - self.push(Checkpoint208({_key: key, _value: value})); - return (0, value); - } - } - - /** - * @dev Return the index of the last (most recent) checkpoint with key lower or equal than the search key, or `high` - * if there is none. `low` and `high` define a section where to do the search, with inclusive `low` and exclusive - * `high`. - * - * WARNING: `high` should not be greater than the array's length. - */ - function _upperBinaryLookup( - Checkpoint208[] storage self, - uint48 key, - uint256 low, - uint256 high - ) private view returns (uint256) { - while (low < high) { - uint256 mid = Math.average(low, high); - if (_unsafeAccess(self, mid)._key > key) { - high = mid; - } else { - low = mid + 1; - } - } - return high; - } - - /** - * @dev Return the index of the first (oldest) checkpoint with key is greater or equal than the search key, or - * `high` if there is none. `low` and `high` define a section where to do the search, with inclusive `low` and - * exclusive `high`. - * - * WARNING: `high` should not be greater than the array's length. - */ - function _lowerBinaryLookup( - Checkpoint208[] storage self, - uint48 key, - uint256 low, - uint256 high - ) private view returns (uint256) { - while (low < high) { - uint256 mid = Math.average(low, high); - if (_unsafeAccess(self, mid)._key < key) { - low = mid + 1; - } else { - high = mid; - } - } - return high; - } - - /** - * @dev Access an element of the array without performing bounds check. The position is assumed to be within bounds. - */ - function _unsafeAccess( - Checkpoint208[] storage self, - uint256 pos - ) private pure returns (Checkpoint208 storage result) { - assembly { - mstore(0, self.slot) - result.slot := add(keccak256(0, 0x20), pos) - } - } - - struct Trace160 { - Checkpoint160[] _checkpoints; - } - - struct Checkpoint160 { - uint96 _key; - uint160 _value; - } - - /** - * @dev Pushes a (`key`, `value`) pair into a Trace160 so that it is stored as the checkpoint. - * - * Returns previous value and new value. - * - * IMPORTANT: Never accept `key` as a user input, since an arbitrary `type(uint96).max` key set will disable the - * library. - */ - function push(Trace160 storage self, uint96 key, uint160 value) internal returns (uint160, uint160) { - return _insert(self._checkpoints, key, value); - } - - /** - * @dev Returns the value in the first (oldest) checkpoint with key greater or equal than the search key, or zero if - * there is none. - */ - function lowerLookup(Trace160 storage self, uint96 key) internal view returns (uint160) { - uint256 len = self._checkpoints.length; - uint256 pos = _lowerBinaryLookup(self._checkpoints, key, 0, len); - return pos == len ? 0 : _unsafeAccess(self._checkpoints, pos)._value; - } - - /** - * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero - * if there is none. - */ - function upperLookup(Trace160 storage self, uint96 key) internal view returns (uint160) { - uint256 len = self._checkpoints.length; - uint256 pos = _upperBinaryLookup(self._checkpoints, key, 0, len); - return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; - } - - /** - * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero - * if there is none. - * - * NOTE: This is a variant of {upperLookup} that is optimised to find "recent" checkpoint (checkpoints with high - * keys). - */ - function upperLookupRecent(Trace160 storage self, uint96 key) internal view returns (uint160) { - uint256 len = self._checkpoints.length; - - uint256 low = 0; - uint256 high = len; - - if (len > 5) { - uint256 mid = len - Math.sqrt(len); - if (key < _unsafeAccess(self._checkpoints, mid)._key) { - high = mid; - } else { - low = mid + 1; - } - } - - uint256 pos = _upperBinaryLookup(self._checkpoints, key, low, high); - - return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; - } - - /** - * @dev Returns the value in the most recent checkpoint, or zero if there are no checkpoints. - */ - function latest(Trace160 storage self) internal view returns (uint160) { - uint256 pos = self._checkpoints.length; - return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; - } - - /** - * @dev Returns whether there is a checkpoint in the structure (i.e. it is not empty), and if so the key and value - * in the most recent checkpoint. - */ - function latestCheckpoint(Trace160 storage self) internal view returns (bool exists, uint96 _key, uint160 _value) { - uint256 pos = self._checkpoints.length; - if (pos == 0) { - return (false, 0, 0); - } else { - Checkpoint160 storage ckpt = _unsafeAccess(self._checkpoints, pos - 1); - return (true, ckpt._key, ckpt._value); - } - } - - /** - * @dev Returns the number of checkpoint. - */ - function length(Trace160 storage self) internal view returns (uint256) { - return self._checkpoints.length; - } - - /** - * @dev Returns checkpoint at given position. - */ - function at(Trace160 storage self, uint32 pos) internal view returns (Checkpoint160 memory) { - return self._checkpoints[pos]; - } - - /** - * @dev Pushes a (`key`, `value`) pair into an ordered list of checkpoints, either by inserting a new checkpoint, - * or by updating the last one. - */ - function _insert(Checkpoint160[] storage self, uint96 key, uint160 value) private returns (uint160, uint160) { - uint256 pos = self.length; - - if (pos > 0) { - Checkpoint160 storage last = _unsafeAccess(self, 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) { - _unsafeAccess(self, pos - 1)._value = value; - } else { - self.push(Checkpoint160({_key: key, _value: value})); - } - return (lastValue, value); - } else { - self.push(Checkpoint160({_key: key, _value: value})); - return (0, value); - } - } - - /** - * @dev Return the index of the last (most recent) checkpoint with key lower or equal than the search key, or `high` - * if there is none. `low` and `high` define a section where to do the search, with inclusive `low` and exclusive - * `high`. - * - * WARNING: `high` should not be greater than the array's length. - */ - function _upperBinaryLookup( - Checkpoint160[] storage self, - uint96 key, - uint256 low, - uint256 high - ) private view returns (uint256) { - while (low < high) { - uint256 mid = Math.average(low, high); - if (_unsafeAccess(self, mid)._key > key) { - high = mid; - } else { - low = mid + 1; - } - } - return high; - } - - /** - * @dev Return the index of the first (oldest) checkpoint with key is greater or equal than the search key, or - * `high` if there is none. `low` and `high` define a section where to do the search, with inclusive `low` and - * exclusive `high`. - * - * WARNING: `high` should not be greater than the array's length. - */ - function _lowerBinaryLookup( - Checkpoint160[] storage self, - uint96 key, - uint256 low, - uint256 high - ) private view returns (uint256) { - while (low < high) { - uint256 mid = Math.average(low, high); - if (_unsafeAccess(self, mid)._key < key) { - low = mid + 1; - } else { - high = mid; - } - } - return high; - } - - /** - * @dev Access an element of the array without performing bounds check. The position is assumed to be within bounds. - */ - function _unsafeAccess( - Checkpoint160[] storage self, - uint256 pos - ) private pure returns (Checkpoint160 storage result) { - assembly { - mstore(0, self.slot) - result.slot := add(keccak256(0, 0x20), pos) - } - } -} diff --git a/contracts/src/utils/structs/checkpoints.rs b/contracts/src/utils/structs/checkpoints.rs index eea820c5..cd272031 100644 --- a/contracts/src/utils/structs/checkpoints.rs +++ b/contracts/src/utils/structs/checkpoints.rs @@ -25,11 +25,11 @@ pub enum Error { } sol_storage! { - struct Trace160 { + pub struct Trace160 { Checkpoint160[] _checkpoints; } - struct Checkpoint160 { + pub struct Checkpoint160 { uint96 _key; uint160 _value; } @@ -85,12 +85,12 @@ impl Trace160 { let mut low = U256::ZERO; let mut high = len; - if len > U256::from(5) { + if len > uint!(5_U256) { let mid = len - len.sqrt(); if key < self._unsafe_access_key(mid) { high = mid; } else { - low = mid + U256::from(1); + low = mid + uint!(1_U256); } } @@ -99,7 +99,7 @@ impl Trace160 { if pos == U256::ZERO { U160::ZERO } else { - self._unsafe_access_value(pos - U256::from(1)) + self._unsafe_access_value(pos - uint!(1_U256)) } } @@ -110,7 +110,7 @@ impl Trace160 { if pos == U256::ZERO { U160::ZERO } else { - self._unsafe_access_value(pos - U256::from(1)) + self._unsafe_access_value(pos - uint!(1_U256)) } } @@ -122,7 +122,7 @@ impl Trace160 { if pos == U256::ZERO { (false, U96::ZERO, U160::ZERO) } else { - let checkpoint = self._unsafe_access(pos - U256::from(1)); + let checkpoint = self._unsafe_access(pos - uint!(1_U256)); (true, checkpoint._key.load(), checkpoint._value.load()) } } From 7fbaaea59ea7196ff3845cdc9a5d77e823b22c0c Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Fri, 21 Jun 2024 20:32:58 +0400 Subject: [PATCH 14/95] ++ --- contracts/src/utils/structs/checkpoints.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/utils/structs/checkpoints.rs b/contracts/src/utils/structs/checkpoints.rs index cd272031..21a16b00 100644 --- a/contracts/src/utils/structs/checkpoints.rs +++ b/contracts/src/utils/structs/checkpoints.rs @@ -220,7 +220,7 @@ impl Trace160 { high } - /// Access an element of the array without performing bounds check. + /// Access on an element of the array without performing bounds check. /// The position is assumed to be within bounds. fn _unsafe_access(&self, pos: U256) -> Checkpoint160 { // TODO#q: think how access it without bounds check From 4ba38872d969604f5cca330adfc118a61c74958e Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Wed, 26 Jun 2024 13:09:58 +0400 Subject: [PATCH 15/95] checkpoints library contract with tests and docs --- contracts/src/utils/math/alloy.rs | 8 + contracts/src/utils/structs/checkpoints.rs | 322 +++++++++++++++++---- contracts/src/utils/structs/mod.rs | 2 +- 3 files changed, 279 insertions(+), 53 deletions(-) diff --git a/contracts/src/utils/math/alloy.rs b/contracts/src/utils/math/alloy.rs index c3f38552..3ad3a2aa 100644 --- a/contracts/src/utils/math/alloy.rs +++ b/contracts/src/utils/math/alloy.rs @@ -169,4 +169,12 @@ mod tests { assert_eq!(left.average(right), U256::from(expected)); }); } + + #[test] + fn check_average_test() { + let left = uint!(0_U256); + let right = uint!(1_U256); + + assert_eq!(left.average(right), uint!(0_U256)); + } } diff --git a/contracts/src/utils/structs/checkpoints.rs b/contracts/src/utils/structs/checkpoints.rs index 21a16b00..8651924f 100644 --- a/contracts/src/utils/structs/checkpoints.rs +++ b/contracts/src/utils/structs/checkpoints.rs @@ -1,15 +1,21 @@ //! Contract module for checkpointing values as they -//! change at different points in time, and later looking up past values by -//! block number. See {Votes} as an example. To create a history of checkpoints -//! define a variable type `Checkpoints.Trace*` in your contract, and store a -//! new checkpoint for the current transaction block using the {push} function. +//! change at different points in time. +//! Lets to look up past values by +//! block number. +//! See {Votes} as an example. +//! To create a history of checkpoints, +//! define a variable type [`Trace160`] in your contract, and store a +//! new checkpoint for the current transaction block using the +//! [`Trace160::push`] function. use alloy_primitives::{uint, Uint, U256, U32}; use alloy_sol_types::sol; use stylus_proc::{sol_storage, SolidityError}; -use stylus_sdk::prelude::StorageType; +use stylus_sdk::storage::{StorageGuard, StorageGuardMut}; use crate::utils::math::alloy::Math; +// TODO#q: add generics for other pairs (uint32, uint224) and (uint48, uint208). +// Logic should be same. type U96 = Uint<96, 2>; type U160 = Uint<160, 3>; @@ -19,18 +25,26 @@ sol! { error CheckpointUnorderedInsertion(); } +/// An error that occurred while calling [`Trace160`] checkpoint contract. #[derive(SolidityError, Debug)] pub enum Error { + /// A value was attempted to be inserted on a past checkpoint. CheckpointUnorderedInsertion(CheckpointUnorderedInsertion), } sol_storage! { + /// State of checkpoint library contract. + #[cfg_attr(all(test, feature = "std"), derive(motsu::DefaultStorageLayout))] pub struct Trace160 { + /// Stores checkpoints in dynamic array sorted by key. Checkpoint160[] _checkpoints; } + /// State of a single checkpoint. pub struct Checkpoint160 { + /// Key of checkpoint. Used as a sorting key. uint96 _key; + /// Value corresponding to the key. uint160 _value; } } @@ -42,7 +56,19 @@ impl Trace160 { /// Returns previous value and new value. /// /// IMPORTANT: Never accept `key` as a user input, since an arbitrary - /// `type(uint96).max` key set will disable the library. + /// `U96::MAX` key set will disable the library. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the checkpoint's state. + /// * `key` - Last checkpoint key to insert. + /// * `value` - Checkpoint value corresponding to insertion `key`. + /// + /// # Errors + /// + /// To maintain sorted order if the `key` is lower than + /// previously inserted error [`Error::CheckpointUnorderedInsertion`] is + /// returned. pub fn push( &mut self, key: U96, @@ -53,41 +79,57 @@ impl Trace160 { /// Returns the value in the first (oldest) checkpoint with key /// greater or equal than the search key, or zero if there is none. - pub fn lower_lookup(&mut self, key: U96) -> U160 { + /// + /// # Arguments + /// + /// * `&self` - read access to the checkpoint's state. + /// * `key` - Checkpoint's key to lookup. + pub fn lower_lookup(&self, key: U96) -> U160 { let len = self.length(); let pos = self._lower_binary_lookup(key, U256::ZERO, len); if pos == len { U160::ZERO } else { - self._unsafe_access_value(pos) + self._access(pos)._value.get() } } /// Returns the value in the last (most recent) checkpoint with key /// lower or equal than the search key, or zero if there is none. - pub fn upper_lookup(&mut self, key: U96) -> U160 { + /// + /// # Arguments + /// + /// * `&self` - read access to the checkpoint's state. + /// * `key` - Checkpoint's key to lookup. + pub fn upper_lookup(&self, key: U96) -> U160 { let len = self.length(); - let pos = self._lower_binary_lookup(key, U256::ZERO, len); - if pos == len { + let pos = self._upper_binary_lookup(key, U256::ZERO, len); + if pos == U256::ZERO { U160::ZERO } else { - self._unsafe_access_value(pos) + self._access(pos - uint!(1_U256))._value.get() } } /// Returns the value in the last (most recent) checkpoint with key /// lower or equal than the search key, or zero if there is none. /// - /// NOTE: This is a variant of {upperLookup} that is optimised to find + /// This is a variant of [`Self::upper_lookup`] that is optimized to find /// "recent" checkpoint (checkpoints with high keys). - pub fn upper_lookup_recent(&mut self, key: U96) -> U160 { + /// + /// # Arguments + /// + /// * `&self` - read access to the checkpoint's state. + /// * `key` - Checkpoint's key to query. + pub fn upper_lookup_recent(&self, key: U96) -> U160 { let len = self.length(); let mut low = U256::ZERO; let mut high = len; + if len > uint!(5_U256) { let mid = len - len.sqrt(); - if key < self._unsafe_access_key(mid) { + if key < self._access(mid)._key.get() { high = mid; } else { low = mid + uint!(1_U256); @@ -99,48 +141,77 @@ impl Trace160 { if pos == U256::ZERO { U160::ZERO } else { - self._unsafe_access_value(pos - uint!(1_U256)) + self._access(pos - uint!(1_U256))._value.get() } } /// Returns the value in the most recent checkpoint, or zero if /// there are no checkpoints. - pub fn latest(&mut self) -> U160 { + /// + /// # Arguments + /// + /// * `&self` - read access to the checkpoint's state. + pub fn latest(&self) -> U160 { let pos = self.length(); if pos == U256::ZERO { U160::ZERO } else { - self._unsafe_access_value(pos - uint!(1_U256)) + self._access(pos - uint!(1_U256))._value.get() } } /// Returns whether there is a checkpoint in the structure (i.e. it - /// is not empty), and if so the key and value in the most recent + /// is not empty), and if so, the key and value in the most recent /// checkpoint. + /// + /// # Arguments + /// + /// * `&self` - read access to the checkpoint's state. pub fn latest_checkpoint(&self) -> (bool, U96, U160) { let pos = self.length(); if pos == U256::ZERO { (false, U96::ZERO, U160::ZERO) } else { - let checkpoint = self._unsafe_access(pos - uint!(1_U256)); - (true, checkpoint._key.load(), checkpoint._value.load()) + let checkpoint = self._access(pos - uint!(1_U256)); + (true, checkpoint._key.get(), checkpoint._value.get()) } } - /// Returns the number of checkpoint. + /// Returns the number of checkpoints. + /// + /// # Arguments + /// + /// * `&self` - read access to the checkpoint's state. pub fn length(&self) -> U256 { // TODO#q: think how to retrieve U256 without conversion U256::from(self._checkpoints.len()) } /// Returns checkpoint at given position. + /// + /// # Arguments + /// + /// * `&self` - read access to the checkpoint's state. + /// * `pos` - index of the checkpoint. pub fn at(&self, pos: U32) -> Checkpoint160 { - unsafe { self._checkpoints.getter(pos).unwrap().into_raw() } + unsafe { self._checkpoints.get(pos).unwrap().into_raw() } } /// Pushes a (`key`, `value`) pair into an ordered list of /// checkpoints, either by inserting a new checkpoint, or by updating /// the last one. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the checkpoint's state. + /// * `key` - Last checkpoint key to insert. + /// * `value` - Checkpoint value corresponding to insertion `key`. + /// + /// # Errors + /// + /// To maintain sorted order if the `key` is lower than + /// previously inserted error [`Error::CheckpointUnorderedInsertion`] is + /// returned. fn _insert( &mut self, key: U96, @@ -148,7 +219,7 @@ impl Trace160 { ) -> Result<(U160, U160), Error> { let pos = self.length(); if pos > U256::ZERO { - let last = self._unsafe_access(pos - uint!(1_U256)); + let last = self._access(pos - uint!(1_U256)); let last_key = last._key.get(); let last_value = last._value.get(); @@ -158,28 +229,31 @@ impl Trace160 { } // Update or push new checkpoint - if last_key > key { - self._checkpoints - .setter(pos - uint!(1_U256)) - .unwrap() - ._value - .set(value); + if last_key == key { + self._access_mut(pos - uint!(1_U256))._value.set(value); } else { - self.push(key, value)?; + self._unchecked_push(key, value); } Ok((last_value, value)) } else { - self.push(key, value)?; + self._unchecked_push(key, value); Ok((U160::ZERO, value)) } } /// Return the index of the last (most recent) checkpoint with key /// lower or equal than the search key, or `high` if there is none. - /// `low` and `high` define a section where to do the search, with + /// Indexes `low` and `high` define a section where to do the search, with /// inclusive `low` and exclusive `high`. /// /// WARNING: `high` should not be greater than the array's length. + /// + /// # Arguments + /// + /// * `&self` - Read access to the checkpoint's state. + /// * `key` - Checkpoint key to lookup. + /// * `low` - inclusive index where search begins. + /// * `high` - exclusive index where search ends. fn _upper_binary_lookup( &self, key: U96, @@ -188,7 +262,7 @@ impl Trace160 { ) -> U256 { while low < high { let mid = low.average(high); - if self._unsafe_access_key(mid) > key { + if self._access(mid)._key.get() > key { high = mid; } else { low = mid + uint!(1_U256); @@ -199,10 +273,17 @@ impl Trace160 { /// Return the index of the first (oldest) checkpoint with key is /// greater or equal than the search key, or `high` if there is none. - /// `low` and `high` define a section where to do the search, with + /// Indexes `low` and `high` define a section where to do the search, with /// inclusive `low` and exclusive `high`. /// /// WARNING: `high` should not be greater than the array's length. + /// + /// # Arguments + /// + /// * `&self` - Read access to the checkpoint's state. + /// * `key` - Checkpoint key to lookup. + /// * `low` - inclusive index where search begins. + /// * `high` - exclusive index where search ends. fn _lower_binary_lookup( &self, key: U96, @@ -211,7 +292,7 @@ impl Trace160 { ) -> U256 { while low < high { let mid = low.average(high); - if self._unsafe_access_key(mid) < key { + if self._access(mid)._key.get() < key { low = mid + uint!(1_U256); } else { high = mid; @@ -220,26 +301,163 @@ impl Trace160 { high } - /// Access on an element of the array without performing bounds check. + /// Immutable access on an element of the checkpoint's array. + /// The position is assumed to be within bounds. + /// Panic when out of bounds. + /// + /// # Arguments + /// + /// * `&self` - Read access to the checkpoint's state. + /// * `pos` - index of the checkpoint. + fn _access(&self, pos: U256) -> StorageGuard { + self._checkpoints + .get(pos) + .unwrap_or_else(|| panic!("should get checkpoint at index `{pos}`")) + } + + /// Mutable access on an element of the checkpoint's array. /// The position is assumed to be within bounds. - fn _unsafe_access(&self, pos: U256) -> Checkpoint160 { - // TODO#q: think how access it without bounds check - unsafe { self._checkpoints.getter(pos).unwrap().into_raw() } + /// Panic when out of bounds. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the checkpoint's state. + /// * `pos` - index of the checkpoint. + fn _access_mut(&mut self, pos: U256) -> StorageGuardMut { + self._checkpoints + .setter(pos) + .unwrap_or_else(|| panic!("should get checkpoint at index `{pos}`")) + } + + /// Append checkpoint not checking if sorted order pertains after. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the checkpoint's state. + /// * `key` - Checkpoint key to insert. + /// * `value` - Checkpoint value corresponding to insertion `key`. + fn _unchecked_push(&mut self, key: U96, value: U160) { + let mut new_checkpoint = self._checkpoints.grow(); + new_checkpoint._key.set(key); + new_checkpoint._value.set(value); + } +} + +#[cfg(all(test, feature = "std"))] +mod tests { + use alloy_primitives::uint; + + use crate::utils::structs::checkpoints::{ + CheckpointUnorderedInsertion, Error, Trace160, + }; + + #[motsu::test] + fn push(checkpoint: Trace160) { + let first_key = uint!(1_U96); + let first_value = uint!(11_U160); + + let second_key = uint!(2_U96); + let second_value = uint!(22_U160); + + let third_key = uint!(3_U96); + let third_value = uint!(33_U160); + + checkpoint.push(first_key, first_value).expect("push first"); + checkpoint.push(second_key, second_value).expect("push second"); + checkpoint.push(third_key, third_value).expect("push third"); + + assert_eq!(checkpoint.length(), uint!(3_U256)); + + assert_eq!(checkpoint.at(uint!(0_U32))._key.get(), first_key); + assert_eq!(checkpoint.at(uint!(0_U32))._value.get(), first_value); + + assert_eq!(checkpoint.at(uint!(1_U32))._key.get(), second_key); + assert_eq!(checkpoint.at(uint!(1_U32))._value.get(), second_value); + + assert_eq!(checkpoint.at(uint!(2_U32))._key.get(), third_key); + assert_eq!(checkpoint.at(uint!(2_U32))._value.get(), third_value); + } + + #[motsu::test] + fn lower_lookup(checkpoint: Trace160) { + checkpoint.push(uint!(1_U96), uint!(11_U160)).expect("push first"); + checkpoint.push(uint!(3_U96), uint!(33_U160)).expect("push second"); + checkpoint.push(uint!(5_U96), uint!(55_U160)).expect("push third"); + + assert_eq!(checkpoint.lower_lookup(uint!(2_U96)), uint!(33_U160)); + assert_eq!(checkpoint.lower_lookup(uint!(3_U96)), uint!(33_U160)); + assert_eq!(checkpoint.lower_lookup(uint!(4_U96)), uint!(55_U160)); + assert_eq!(checkpoint.lower_lookup(uint!(6_U96)), uint!(0_U160)); + } + + #[motsu::test] + fn upper_lookup(checkpoint: Trace160) { + checkpoint.push(uint!(1_U96), uint!(11_U160)).expect("push first"); + checkpoint.push(uint!(3_U96), uint!(33_U160)).expect("push second"); + checkpoint.push(uint!(5_U96), uint!(55_U160)).expect("push third"); + + assert_eq!(checkpoint.upper_lookup(uint!(2_U96)), uint!(11_U160)); + assert_eq!(checkpoint.upper_lookup(uint!(1_U96)), uint!(11_U160)); + assert_eq!(checkpoint.upper_lookup(uint!(4_U96)), uint!(33_U160)); + assert_eq!(checkpoint.upper_lookup(uint!(0_U96)), uint!(0_U160)); + } + + #[motsu::test] + fn upper_lookup_recent(checkpoint: Trace160) { + // Since `upper_lookup_recent` optimized for higher keys (>5) compare to + // `upper_lookup`. All test key values will be higher then 5. + checkpoint.push(uint!(11_U96), uint!(111_U160)).expect("push first"); + checkpoint.push(uint!(33_U96), uint!(333_U160)).expect("push second"); + checkpoint.push(uint!(55_U96), uint!(555_U160)).expect("push third"); + + assert_eq!( + checkpoint.upper_lookup_recent(uint!(22_U96)), + uint!(111_U160) + ); + assert_eq!( + checkpoint.upper_lookup_recent(uint!(11_U96)), + uint!(111_U160) + ); + assert_eq!( + checkpoint.upper_lookup_recent(uint!(44_U96)), + uint!(333_U160) + ); + assert_eq!(checkpoint.upper_lookup_recent(uint!(0_U96)), uint!(0_U160)); + } + + #[motsu::test] + fn latest(checkpoint: Trace160) { + assert_eq!(checkpoint.latest(), uint!(0_U160)); + checkpoint.push(uint!(1_U96), uint!(11_U160)).expect("push first"); + checkpoint.push(uint!(3_U96), uint!(33_U160)).expect("push second"); + checkpoint.push(uint!(5_U96), uint!(55_U160)).expect("push third"); + assert_eq!(checkpoint.latest(), uint!(55_U160)); } - /// Access on a key - fn _unsafe_access_key(&self, pos: U256) -> U96 { - // TODO#q: think how access it without bounds check - let check_point = - self._checkpoints.get(pos).expect("get checkpoint by index"); - check_point._key.get() + #[motsu::test] + fn latest_checkpoint(checkpoint: Trace160) { + assert_eq!(checkpoint.latest_checkpoint().0, false); + checkpoint.push(uint!(1_U96), uint!(11_U160)).expect("push first"); + checkpoint.push(uint!(3_U96), uint!(33_U160)).expect("push second"); + checkpoint.push(uint!(5_U96), uint!(55_U160)).expect("push third"); + assert_eq!( + checkpoint.latest_checkpoint(), + (true, uint!(5_U96), uint!(55_U160)) + ); } - /// Access on a value - fn _unsafe_access_value(&self, pos: U256) -> U160 { - // TODO#q: think how access it without bounds check - let check_point = - self._checkpoints.get(pos).expect("get checkpoint by index"); - check_point._value.get() + #[motsu::test] + fn error_when_unordered_insertion(checkpoint: Trace160) { + checkpoint.push(uint!(1_U96), uint!(11_U160)).expect("push first"); + checkpoint.push(uint!(3_U96), uint!(33_U160)).expect("push second"); + let err = checkpoint + .push(uint!(2_U96), uint!(22_U160)) + .expect_err("should not push value lower then last one"); + assert!(matches!( + err, + Error::CheckpointUnorderedInsertion( + CheckpointUnorderedInsertion {} + ) + )); } } diff --git a/contracts/src/utils/structs/mod.rs b/contracts/src/utils/structs/mod.rs index b6a94061..a7daf8ae 100644 --- a/contracts/src/utils/structs/mod.rs +++ b/contracts/src/utils/structs/mod.rs @@ -1,3 +1,3 @@ //! Solidity storage types used by other contracts. pub mod bitmap; -mod checkpoints; +pub mod checkpoints; From 2de0e0e5cf4a223dc70db6170e33ad428234c16c Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Thu, 27 Jun 2024 14:17:39 +0400 Subject: [PATCH 16/95] ++ --- contracts/src/utils/math/alloy.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/contracts/src/utils/math/alloy.rs b/contracts/src/utils/math/alloy.rs index 3ad3a2aa..c3f38552 100644 --- a/contracts/src/utils/math/alloy.rs +++ b/contracts/src/utils/math/alloy.rs @@ -169,12 +169,4 @@ mod tests { assert_eq!(left.average(right), U256::from(expected)); }); } - - #[test] - fn check_average_test() { - let left = uint!(0_U256); - let right = uint!(1_U256); - - assert_eq!(left.average(right), uint!(0_U256)); - } } From c597ec72030887322399baf4c28d7453e996105e Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Thu, 27 Jun 2024 14:18:51 +0400 Subject: [PATCH 17/95] ++ --- contracts/src/utils/structs/checkpoints.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/src/utils/structs/checkpoints.rs b/contracts/src/utils/structs/checkpoints.rs index 8651924f..fd5455bb 100644 --- a/contracts/src/utils/structs/checkpoints.rs +++ b/contracts/src/utils/structs/checkpoints.rs @@ -183,7 +183,6 @@ impl Trace160 { /// /// * `&self` - read access to the checkpoint's state. pub fn length(&self) -> U256 { - // TODO#q: think how to retrieve U256 without conversion U256::from(self._checkpoints.len()) } From d50a360c85112faa24d01b400e2b05b9976ccf37 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Thu, 27 Jun 2024 15:24:03 +0400 Subject: [PATCH 18/95] ++ --- contracts/src/utils/structs/checkpoints.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/utils/structs/checkpoints.rs b/contracts/src/utils/structs/checkpoints.rs index fd5455bb..5c5b650b 100644 --- a/contracts/src/utils/structs/checkpoints.rs +++ b/contracts/src/utils/structs/checkpoints.rs @@ -328,7 +328,7 @@ impl Trace160 { .unwrap_or_else(|| panic!("should get checkpoint at index `{pos}`")) } - /// Append checkpoint not checking if sorted order pertains after. + /// Append checkpoint without checking if sorted order pertains after. /// /// # Arguments /// From a58d7e003fca314c93fe5baf4ed56cb0627508dd Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Thu, 27 Jun 2024 15:26:03 +0400 Subject: [PATCH 19/95] ++ --- contracts/src/utils/structs/checkpoints.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/src/utils/structs/checkpoints.rs b/contracts/src/utils/structs/checkpoints.rs index 5c5b650b..3f0c982e 100644 --- a/contracts/src/utils/structs/checkpoints.rs +++ b/contracts/src/utils/structs/checkpoints.rs @@ -36,7 +36,7 @@ sol_storage! { /// State of checkpoint library contract. #[cfg_attr(all(test, feature = "std"), derive(motsu::DefaultStorageLayout))] pub struct Trace160 { - /// Stores checkpoints in dynamic array sorted by key. + /// Stores checkpoints in a dynamic array sorted by key. Checkpoint160[] _checkpoints; } @@ -160,7 +160,7 @@ impl Trace160 { } } - /// Returns whether there is a checkpoint in the structure (i.e. it + /// Returns whether there is a checkpoint in the structure (i.g. it /// is not empty), and if so, the key and value in the most recent /// checkpoint. /// From e11f67f5161bf16563db28b0dca93ffefe9c6751 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Thu, 27 Jun 2024 21:00:35 +0400 Subject: [PATCH 20/95] ++ --- contracts/src/utils/structs/checkpoints.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/contracts/src/utils/structs/checkpoints.rs b/contracts/src/utils/structs/checkpoints.rs index 3f0c982e..caaeb554 100644 --- a/contracts/src/utils/structs/checkpoints.rs +++ b/contracts/src/utils/structs/checkpoints.rs @@ -2,7 +2,6 @@ //! change at different points in time. //! Lets to look up past values by //! block number. -//! See {Votes} as an example. //! To create a history of checkpoints, //! define a variable type [`Trace160`] in your contract, and store a //! new checkpoint for the current transaction block using the @@ -14,8 +13,8 @@ use stylus_sdk::storage::{StorageGuard, StorageGuardMut}; use crate::utils::math::alloy::Math; -// TODO#q: add generics for other pairs (uint32, uint224) and (uint48, uint208). -// Logic should be same. +// 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>; From 78777d42953e824f008fb2e917ac50bbc3d37d55 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Thu, 27 Jun 2024 21:04:40 +0400 Subject: [PATCH 21/95] ++ --- contracts/src/utils/structs/checkpoints.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/src/utils/structs/checkpoints.rs b/contracts/src/utils/structs/checkpoints.rs index caaeb554..041e9029 100644 --- a/contracts/src/utils/structs/checkpoints.rs +++ b/contracts/src/utils/structs/checkpoints.rs @@ -65,9 +65,9 @@ impl Trace160 { /// /// # Errors /// - /// To maintain sorted order if the `key` is lower than - /// previously inserted error [`Error::CheckpointUnorderedInsertion`] is - /// returned. + /// If the `key` is lower than previously pushed checkpoint's key error + /// [`Error::CheckpointUnorderedInsertion`] is returned (necessary to + /// maintain sorted order). pub fn push( &mut self, key: U96, From 8fdcc3a2873b73e686db735e408296e6a755580b Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Thu, 27 Jun 2024 21:28:59 +0400 Subject: [PATCH 22/95] ++ --- contracts/src/utils/structs/checkpoints.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/contracts/src/utils/structs/checkpoints.rs b/contracts/src/utils/structs/checkpoints.rs index 041e9029..27e75c9e 100644 --- a/contracts/src/utils/structs/checkpoints.rs +++ b/contracts/src/utils/structs/checkpoints.rs @@ -162,17 +162,18 @@ impl Trace160 { /// Returns whether there is a checkpoint in the structure (i.g. it /// is not empty), and if so, the key and value in the most recent /// checkpoint. + /// Otherwise, [`None`] will be returned. /// /// # Arguments /// /// * `&self` - read access to the checkpoint's state. - pub fn latest_checkpoint(&self) -> (bool, U96, U160) { + pub fn latest_checkpoint(&self) -> Option<(U96, U160)> { let pos = self.length(); if pos == U256::ZERO { - (false, U96::ZERO, U160::ZERO) + None } else { let checkpoint = self._access(pos - uint!(1_U256)); - (true, checkpoint._key.get(), checkpoint._value.get()) + Some((checkpoint._key.get(), checkpoint._value.get())) } } @@ -198,6 +199,7 @@ impl Trace160 { /// Pushes a (`key`, `value`) pair into an ordered list of /// checkpoints, either by inserting a new checkpoint, or by updating /// the last one. + /// Returns previous value and new value. /// /// # Arguments /// @@ -434,13 +436,13 @@ mod tests { #[motsu::test] fn latest_checkpoint(checkpoint: Trace160) { - assert_eq!(checkpoint.latest_checkpoint().0, false); + assert_eq!(checkpoint.latest_checkpoint(), None); checkpoint.push(uint!(1_U96), uint!(11_U160)).expect("push first"); checkpoint.push(uint!(3_U96), uint!(33_U160)).expect("push second"); checkpoint.push(uint!(5_U96), uint!(55_U160)).expect("push third"); assert_eq!( checkpoint.latest_checkpoint(), - (true, uint!(5_U96), uint!(55_U160)) + Some((uint!(5_U96), uint!(55_U160))) ); } From 7c0ff94a8bfae7b5ceb32b29453df2353bc7cb91 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Mon, 1 Jul 2024 22:12:34 +0400 Subject: [PATCH 23/95] ++ --- contracts/src/utils/structs/checkpoints.rs | 38 +++++++++++++--------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/contracts/src/utils/structs/checkpoints.rs b/contracts/src/utils/structs/checkpoints.rs index 27e75c9e..54ed4c3e 100644 --- a/contracts/src/utils/structs/checkpoints.rs +++ b/contracts/src/utils/structs/checkpoints.rs @@ -49,7 +49,7 @@ sol_storage! { } impl Trace160 { - /// Pushes a (`key`, `value`) pair into a Trace160 so that it is + /// Pushes a (`key`, `value`) pair into a `Trace160` so that it is /// stored as the checkpoint. /// /// Returns previous value and new value. @@ -77,7 +77,7 @@ impl Trace160 { } /// Returns the value in the first (oldest) checkpoint with key - /// greater or equal than the search key, or zero if there is none. + /// greater or equal than the search key, or `U160::ZERO` if there is none. /// /// # Arguments /// @@ -89,7 +89,7 @@ impl Trace160 { if pos == len { U160::ZERO } else { - self._access(pos)._value.get() + self._index(pos)._value.get() } } @@ -106,7 +106,7 @@ impl Trace160 { if pos == U256::ZERO { U160::ZERO } else { - self._access(pos - uint!(1_U256))._value.get() + self._index(pos - uint!(1_U256))._value.get() } } @@ -128,7 +128,7 @@ impl Trace160 { if len > uint!(5_U256) { let mid = len - len.sqrt(); - if key < self._access(mid)._key.get() { + if key < self._index(mid)._key.get() { high = mid; } else { low = mid + uint!(1_U256); @@ -140,7 +140,7 @@ impl Trace160 { if pos == U256::ZERO { U160::ZERO } else { - self._access(pos - uint!(1_U256))._value.get() + self._index(pos - uint!(1_U256))._value.get() } } @@ -155,7 +155,7 @@ impl Trace160 { if pos == U256::ZERO { U160::ZERO } else { - self._access(pos - uint!(1_U256))._value.get() + self._index(pos - uint!(1_U256))._value.get() } } @@ -172,7 +172,7 @@ impl Trace160 { if pos == U256::ZERO { None } else { - let checkpoint = self._access(pos - uint!(1_U256)); + let checkpoint = self._index(pos - uint!(1_U256)); Some((checkpoint._key.get(), checkpoint._value.get())) } } @@ -219,7 +219,7 @@ impl Trace160 { ) -> Result<(U160, U160), Error> { let pos = self.length(); if pos > U256::ZERO { - let last = self._access(pos - uint!(1_U256)); + let last = self._index(pos - uint!(1_U256)); let last_key = last._key.get(); let last_value = last._value.get(); @@ -230,7 +230,7 @@ impl Trace160 { // Update or push new checkpoint if last_key == key { - self._access_mut(pos - uint!(1_U256))._value.set(value); + self._index_mut(pos - uint!(1_U256))._value.set(value); } else { self._unchecked_push(key, value); } @@ -262,7 +262,7 @@ impl Trace160 { ) -> U256 { while low < high { let mid = low.average(high); - if self._access(mid)._key.get() > key { + if self._index(mid)._key.get() > key { high = mid; } else { low = mid + uint!(1_U256); @@ -292,7 +292,7 @@ impl Trace160 { ) -> U256 { while low < high { let mid = low.average(high); - if self._access(mid)._key.get() < key { + if self._index(mid)._key.get() < key { low = mid + uint!(1_U256); } else { high = mid; @@ -303,13 +303,16 @@ impl Trace160 { /// Immutable access on an element of the checkpoint's array. /// The position is assumed to be within bounds. - /// Panic when out of bounds. + /// + /// # Panics + /// + /// If `pos` exceeds `U256::max`. /// /// # Arguments /// /// * `&self` - Read access to the checkpoint's state. /// * `pos` - index of the checkpoint. - fn _access(&self, pos: U256) -> StorageGuard { + fn _index(&self, pos: U256) -> StorageGuard { self._checkpoints .get(pos) .unwrap_or_else(|| panic!("should get checkpoint at index `{pos}`")) @@ -317,13 +320,16 @@ impl Trace160 { /// Mutable access on an element of the checkpoint's array. /// The position is assumed to be within bounds. - /// Panic when out of bounds. + /// + /// # Panics + /// + /// If `pos` exceeds `U256::max`. /// /// # Arguments /// /// * `&mut self` - Write access to the checkpoint's state. /// * `pos` - index of the checkpoint. - fn _access_mut(&mut self, pos: U256) -> StorageGuardMut { + fn _index_mut(&mut self, pos: U256) -> StorageGuardMut { self._checkpoints .setter(pos) .unwrap_or_else(|| panic!("should get checkpoint at index `{pos}`")) From a213b157e1cb94039013c69dd5df392692b2d0f3 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Tue, 2 Jul 2024 18:06:51 +0400 Subject: [PATCH 24/95] add more test cases for push and upper_lookup_recent --- contracts/src/utils/structs/checkpoints.rs | 78 ++++++++++++++++++---- 1 file changed, 64 insertions(+), 14 deletions(-) diff --git a/contracts/src/utils/structs/checkpoints.rs b/contracts/src/utils/structs/checkpoints.rs index 54ed4c3e..f581142b 100644 --- a/contracts/src/utils/structs/checkpoints.rs +++ b/contracts/src/utils/structs/checkpoints.rs @@ -188,12 +188,19 @@ impl Trace160 { /// Returns checkpoint at given position. /// + /// # Panics + /// + /// If `pos` exceeds [`Self::length`]. + /// /// # Arguments /// /// * `&self` - read access to the checkpoint's state. /// * `pos` - index of the checkpoint. - pub fn at(&self, pos: U32) -> Checkpoint160 { - unsafe { self._checkpoints.get(pos).unwrap().into_raw() } + pub fn at(&self, pos: U32) -> (U96, U160) { + let guard = self._checkpoints.get(pos).unwrap_or_else(|| { + panic!("should get checkpoint at index `{pos}`") + }); + (guard._key.get(), guard._value.get()) } /// Pushes a (`key`, `value`) pair into an ordered list of @@ -305,8 +312,8 @@ impl Trace160 { /// The position is assumed to be within bounds. /// /// # Panics - /// - /// If `pos` exceeds `U256::max`. + /// + /// If `pos` exceeds [`Self::length`]. /// /// # Arguments /// @@ -323,7 +330,7 @@ impl Trace160 { /// /// # Panics /// - /// If `pos` exceeds `U256::max`. + /// If `pos` exceeds [`Self::length`]. /// /// # Arguments /// @@ -374,14 +381,34 @@ mod tests { assert_eq!(checkpoint.length(), uint!(3_U256)); - assert_eq!(checkpoint.at(uint!(0_U32))._key.get(), first_key); - assert_eq!(checkpoint.at(uint!(0_U32))._value.get(), first_value); + assert_eq!(checkpoint.at(uint!(0_U32)), (first_key, first_value)); + assert_eq!(checkpoint.at(uint!(1_U32)), (second_key, second_value)); + assert_eq!(checkpoint.at(uint!(2_U32)), (third_key, third_value)); + } - assert_eq!(checkpoint.at(uint!(1_U32))._key.get(), second_key); - assert_eq!(checkpoint.at(uint!(1_U32))._value.get(), second_value); + #[motsu::test] + fn push_same_value(checkpoint: Trace160) { + let first_key = uint!(1_U96); + let first_value = uint!(11_U160); - assert_eq!(checkpoint.at(uint!(2_U32))._key.get(), third_key); - assert_eq!(checkpoint.at(uint!(2_U32))._value.get(), third_value); + let second_key = uint!(2_U96); + let second_value = uint!(22_U160); + + let third_key = uint!(2_U96); + let third_value = uint!(222_U160); + + checkpoint.push(first_key, first_value).expect("push first"); + checkpoint.push(second_key, second_value).expect("push second"); + checkpoint.push(third_key, third_value).expect("push third"); + + assert_eq!( + checkpoint.length(), + uint!(2_U256), + "two checkpoints should be stored since third_value overrides second_value" + ); + + assert_eq!(checkpoint.at(uint!(0_U32)), (first_key, first_value)); + assert_eq!(checkpoint.at(uint!(1_U32)), (third_key, third_value)); } #[motsu::test] @@ -409,9 +436,32 @@ mod tests { } #[motsu::test] - fn upper_lookup_recent(checkpoint: Trace160) { - // Since `upper_lookup_recent` optimized for higher keys (>5) compare to - // `upper_lookup`. All test key values will be higher then 5. + fn upper_lookup_recent_low_values(checkpoint: Trace160) { + // upper_lookup_recent has different optimisation for low (<=5) and high + // (>5) values. Validate the first approach for low values. + checkpoint.push(uint!(1_U96), uint!(11_U160)).expect("push first"); + checkpoint.push(uint!(3_U96), uint!(33_U160)).expect("push second"); + checkpoint.push(uint!(5_U96), uint!(55_U160)).expect("push third"); + + assert_eq!( + checkpoint.upper_lookup_recent(uint!(2_U96)), + uint!(11_U160) + ); + assert_eq!( + checkpoint.upper_lookup_recent(uint!(1_U96)), + uint!(11_U160) + ); + assert_eq!( + checkpoint.upper_lookup_recent(uint!(4_U96)), + uint!(33_U160) + ); + assert_eq!(checkpoint.upper_lookup_recent(uint!(0_U96)), uint!(0_U160)); + } + + #[motsu::test] + fn upper_lookup_recent_high_values(checkpoint: Trace160) { + // upper_lookup_recent has different optimisation for low (<=5) and high + // (>5) values. Validate the second approach for high values. checkpoint.push(uint!(11_U96), uint!(111_U160)).expect("push first"); checkpoint.push(uint!(33_U96), uint!(333_U160)).expect("push second"); checkpoint.push(uint!(55_U96), uint!(555_U160)).expect("push third"); From 8523f1e2e2e488400935c1edd0520d763165f700 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Tue, 2 Jul 2024 22:32:10 +0400 Subject: [PATCH 25/95] ++ --- contracts/src/utils/structs/checkpoints.rs | 25 ++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/contracts/src/utils/structs/checkpoints.rs b/contracts/src/utils/structs/checkpoints.rs index f581142b..d2fedf0f 100644 --- a/contracts/src/utils/structs/checkpoints.rs +++ b/contracts/src/utils/structs/checkpoints.rs @@ -437,8 +437,10 @@ mod tests { #[motsu::test] fn upper_lookup_recent_low_values(checkpoint: Trace160) { - // upper_lookup_recent has different optimisation for low (<=5) and high - // (>5) values. Validate the first approach for low values. + // upper_lookup_recent has different optimizations for "short" (<=5) and + // "long" (>5) checkpoint arrays. + // + // Validate the first approach for a short checkpoint array. checkpoint.push(uint!(1_U96), uint!(11_U160)).expect("push first"); checkpoint.push(uint!(3_U96), uint!(33_U160)).expect("push second"); checkpoint.push(uint!(5_U96), uint!(55_U160)).expect("push third"); @@ -455,6 +457,25 @@ mod tests { checkpoint.upper_lookup_recent(uint!(4_U96)), uint!(33_U160) ); + + // Validate the second approach for a long checkpoint array. + checkpoint.push(uint!(7_U96), uint!(77_U160)).expect("push fourth"); + checkpoint.push(uint!(9_U96), uint!(99_U160)).expect("push fifth"); + checkpoint.push(uint!(11_U96), uint!(111_U160)).expect("push sixth"); + + assert_eq!( + checkpoint.upper_lookup_recent(uint!(7_U96)), + uint!(77_U160) + ); + assert_eq!( + checkpoint.upper_lookup_recent(uint!(9_U96)), + uint!(99_U160) + ); + assert_eq!( + checkpoint.upper_lookup_recent(uint!(11_U96)), + uint!(111_U160) + ); + assert_eq!(checkpoint.upper_lookup_recent(uint!(0_U96)), uint!(0_U160)); } From bcce1a0f552f145aa8d92864e801f3a5eb91b99f Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Wed, 3 Jul 2024 14:42:24 +0400 Subject: [PATCH 26/95] ++ --- contracts/src/utils/structs/checkpoints.rs | 25 +--------------------- 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/contracts/src/utils/structs/checkpoints.rs b/contracts/src/utils/structs/checkpoints.rs index d2fedf0f..1b58a745 100644 --- a/contracts/src/utils/structs/checkpoints.rs +++ b/contracts/src/utils/structs/checkpoints.rs @@ -436,7 +436,7 @@ mod tests { } #[motsu::test] - fn upper_lookup_recent_low_values(checkpoint: Trace160) { + fn upper_lookup_recent(checkpoint: Trace160) { // upper_lookup_recent has different optimizations for "short" (<=5) and // "long" (>5) checkpoint arrays. // @@ -479,29 +479,6 @@ mod tests { assert_eq!(checkpoint.upper_lookup_recent(uint!(0_U96)), uint!(0_U160)); } - #[motsu::test] - fn upper_lookup_recent_high_values(checkpoint: Trace160) { - // upper_lookup_recent has different optimisation for low (<=5) and high - // (>5) values. Validate the second approach for high values. - checkpoint.push(uint!(11_U96), uint!(111_U160)).expect("push first"); - checkpoint.push(uint!(33_U96), uint!(333_U160)).expect("push second"); - checkpoint.push(uint!(55_U96), uint!(555_U160)).expect("push third"); - - assert_eq!( - checkpoint.upper_lookup_recent(uint!(22_U96)), - uint!(111_U160) - ); - assert_eq!( - checkpoint.upper_lookup_recent(uint!(11_U96)), - uint!(111_U160) - ); - assert_eq!( - checkpoint.upper_lookup_recent(uint!(44_U96)), - uint!(333_U160) - ); - assert_eq!(checkpoint.upper_lookup_recent(uint!(0_U96)), uint!(0_U160)); - } - #[motsu::test] fn latest(checkpoint: Trace160) { assert_eq!(checkpoint.latest(), uint!(0_U160)); From 06d0b3e6d2440270d5b928096a1304d106a9c033 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Wed, 3 Jul 2024 17:46:04 +0400 Subject: [PATCH 27/95] ++ --- contracts/src/utils/structs/checkpoints.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/src/utils/structs/checkpoints.rs b/contracts/src/utils/structs/checkpoints.rs index 1b58a745..2ff4ccbc 100644 --- a/contracts/src/utils/structs/checkpoints.rs +++ b/contracts/src/utils/structs/checkpoints.rs @@ -94,7 +94,7 @@ impl Trace160 { } /// Returns the value in the last (most recent) checkpoint with key - /// lower or equal than the search key, or zero if there is none. + /// lower or equal than the search key, or `U160::ZERO` if there is none. /// /// # Arguments /// @@ -111,7 +111,7 @@ impl Trace160 { } /// Returns the value in the last (most recent) checkpoint with key - /// lower or equal than the search key, or zero if there is none. + /// lower or equal than the search key, or `U160::ZERO` if there is none. /// /// This is a variant of [`Self::upper_lookup`] that is optimized to find /// "recent" checkpoint (checkpoints with high keys). @@ -144,7 +144,7 @@ impl Trace160 { } } - /// Returns the value in the most recent checkpoint, or zero if + /// Returns the value in the most recent checkpoint, or `U160::ZERO` if /// there are no checkpoints. /// /// # Arguments From 9408fdd08ff1f747d361c8914b6cbb63de24a9b4 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Thu, 27 Jun 2024 14:19:03 +0400 Subject: [PATCH 28/95] add erc721consecutive scratch --- Cargo.lock | 16 ++++++++++++ Cargo.toml | 2 ++ examples/erc721-consecutive/Cargo.toml | 27 ++++++++++++++++++++ examples/erc721-consecutive/src/lib.rs | 35 ++++++++++++++++++++++++++ 4 files changed, 80 insertions(+) create mode 100644 examples/erc721-consecutive/Cargo.toml create mode 100644 examples/erc721-consecutive/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 0c130474..e4ae9b95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1561,6 +1561,22 @@ dependencies = [ "tokio", ] +[[package]] +name = "erc721-consecutive-example" +version = "0.0.0" +dependencies = [ + "alloy", + "alloy-primitives 0.3.3", + "e2e", + "eyre", + "mini-alloc", + "openzeppelin-stylus", + "rand", + "stylus-proc", + "stylus-sdk", + "tokio", +] + [[package]] name = "erc721-example" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index 40ac0191..7829a98d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "lib/e2e-proc", "examples/erc20", "examples/erc721", + "examples/erc721-consecutive", "examples/merkle-proofs", "examples/ownable", "examples/access-control", @@ -23,6 +24,7 @@ default-members = [ "lib/e2e-proc", "examples/erc20", "examples/erc721", + "examples/erc721-consecutive", "examples/merkle-proofs", "examples/ownable", "examples/access-control", diff --git a/examples/erc721-consecutive/Cargo.toml b/examples/erc721-consecutive/Cargo.toml new file mode 100644 index 00000000..5ffc27a6 --- /dev/null +++ b/examples/erc721-consecutive/Cargo.toml @@ -0,0 +1,27 @@ +[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 +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/lib.rs b/examples/erc721-consecutive/src/lib.rs new file mode 100644 index 00000000..10506b47 --- /dev/null +++ b/examples/erc721-consecutive/src/lib.rs @@ -0,0 +1,35 @@ +#![cfg_attr(not(test), no_main, no_std)] +extern crate alloc; + +use alloc::vec::Vec; + +use alloy_primitives::{Address, U256}; +use openzeppelin_stylus::token::erc721::{extensions::IErc721Burnable, Erc721}; +use stylus_sdk::prelude::{entrypoint, external, sol_storage}; + +sol_storage! { + #[entrypoint] + struct Erc721ConsecutiveExample { + #[borrow] + Erc721 erc721; + } +} + +// TODO#q: add consecutive errors + +#[external] +#[inherit(Erc721)] +impl Erc721ConsecutiveExample { + pub fn burn(&mut self, token_id: U256) -> Result<(), Vec> { + self.erc721.burn(token_id)?; + Ok(()) + } + + pub fn mint(&mut self, to: Address, token_id: U256) -> Result<(), Vec> { + self.erc721._mint(to, token_id)?; + + Ok(()) + } + + // TODO#q: add consecutive implementation +} From 7a3ca4971ea3bb00d0805f899738bf35c4fc89b6 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Mon, 1 Jul 2024 15:28:09 +0400 Subject: [PATCH 29/95] add mint consecutive and burn implementation --- Cargo.lock | 1 + contracts/src/utils/structs/checkpoints.rs | 4 +- examples/erc721-consecutive/Cargo.toml | 1 + examples/erc721-consecutive/src/lib.rs | 137 +++++++++++++++++++-- 4 files changed, 133 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e4ae9b95..5c97a8e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1567,6 +1567,7 @@ version = "0.0.0" dependencies = [ "alloy", "alloy-primitives 0.3.3", + "alloy-sol-types 0.3.1", "e2e", "eyre", "mini-alloc", diff --git a/contracts/src/utils/structs/checkpoints.rs b/contracts/src/utils/structs/checkpoints.rs index 2ff4ccbc..af198237 100644 --- a/contracts/src/utils/structs/checkpoints.rs +++ b/contracts/src/utils/structs/checkpoints.rs @@ -15,8 +15,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 on a past checkpoint. diff --git a/examples/erc721-consecutive/Cargo.toml b/examples/erc721-consecutive/Cargo.toml index 5ffc27a6..2c6eef27 100644 --- a/examples/erc721-consecutive/Cargo.toml +++ b/examples/erc721-consecutive/Cargo.toml @@ -9,6 +9,7 @@ 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 diff --git a/examples/erc721-consecutive/src/lib.rs b/examples/erc721-consecutive/src/lib.rs index 10506b47..e800575c 100644 --- a/examples/erc721-consecutive/src/lib.rs +++ b/examples/erc721-consecutive/src/lib.rs @@ -3,33 +3,154 @@ extern crate alloc; use alloc::vec::Vec; -use alloy_primitives::{Address, U256}; -use openzeppelin_stylus::token::erc721::{extensions::IErc721Burnable, Erc721}; -use stylus_sdk::prelude::{entrypoint, external, sol_storage}; +use alloy_primitives::{uint, Address, U128, U256}; +use alloy_sol_types::SolError; +use openzeppelin_stylus::{ + token::erc721::{ + extensions::IErc721Burnable, ERC721InvalidReceiver, Erc721, + }, + utils::structs::{ + bitmap::BitMap, + checkpoints::{Trace160, U160, U96}, + }, +}; +use stylus_sdk::{alloy_sol_types::sol, evm, prelude::*}; sol_storage! { #[entrypoint] struct Erc721ConsecutiveExample { #[borrow] Erc721 erc721; + Trace160 sequential_ownership; + BitMap sequentian_burn; } } -// TODO#q: add consecutive errors +sol! { + /// Emitted when the tokens from `fromTokenId` to `toTokenId` are transferred from `fromAddress` to `toAddress`. + event ConsecutiveTransfer( + uint256 indexed fromTokenId, + uint256 toTokenId, + address indexed fromAddress, + address indexed toAddress + ); +} + +sol! { + /// Batch mint is restricted to the constructor. + /// Any batch mint not emitting the {IERC721-Transfer} event outside of the constructor + /// is non ERC-721 compliant. + #[derive(Debug)] + #[allow(missing_docs)] + error ERC721ForbiddenBatchMint(); + + /// Exceeds the max amount of mints per batch. + #[derive(Debug)] + #[allow(missing_docs)] + error ERC721ExceededMaxBatchMint(uint256 batchSize, uint256 maxBatch); + + /// Individual minting is not allowed. + #[derive(Debug)] + #[allow(missing_docs)] + error ERC721ForbiddenMint(); + + /// Batch burn is not supported. + #[derive(Debug)] + #[allow(missing_docs)] + error ERC721ForbiddenBatchBurn(); +} + +// 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. +const MAX_BATCH_SIZE: U96 = uint!(5000_U96); + +// Used to offset the first token id in {_nextConsecutiveId} +const FIRST_CONSECUTIVE_ID: U96 = uint!(0_U96); #[external] #[inherit(Erc721)] impl Erc721ConsecutiveExample { + // Burn token with `token_id` and record it at [`Self::sequentian_burn`] + // storage. pub fn burn(&mut self, token_id: U256) -> Result<(), Vec> { self.erc721.burn(token_id)?; + + // record burn + if token_id < self.next_consecutive_id().to() // the tokenId was minted in a batch + && !self.sequentian_burn.get(token_id) + // and the token was never marked as burnt + { + self.sequentian_burn.set(token_id) + } Ok(()) } - pub fn mint(&mut self, to: Address, token_id: U256) -> Result<(), Vec> { - self.erc721._mint(to, token_id)?; + // Mint a batch of tokens of length `batchSize` for `to`. Returns the token + // id of the first token minted in the batch; if `batchSize` is 0, + // returns the number of consecutive ids minted so far. + // + // Requirements: + // + // - `batchSize` must not be greater than [`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 `onERC721Received` on the receiver. + // + // Emits a [`ConsecutiveTransfer`] event. + pub fn mint_consecutive( + &mut self, + to: Address, + batch_size: u128, // TODO: how to use U96 type + ) -> Result> { + let batch_size = U96::from(batch_size); + let next = self.next_consecutive_id(); - Ok(()) + if batch_size > U96::ZERO { + if to.is_zero() { + return Err( + ERC721InvalidReceiver { receiver: Address::ZERO }.encode() + ); + } + + if batch_size > MAX_BATCH_SIZE.to() { + return Err(ERC721ExceededMaxBatchMint { + batchSize: U256::from(batch_size), + maxBatch: U256::from(MAX_BATCH_SIZE), + } + .encode()); + } + + let last = next + batch_size - uint!(1_U96); + self.sequential_ownership + .push(last, U160::from_be_bytes(to.into_array())) + .map_err(Vec::::from)?; + + self.erc721._increase_balance(to, U128::from(batch_size)); + evm::log(ConsecutiveTransfer { + fromTokenId: next.to::(), + toTokenId: last.to::(), + fromAddress: Address::ZERO, + toAddress: to, + }); + }; + Ok(next.to()) } +} - // TODO#q: add consecutive implementation +impl Erc721ConsecutiveExample { + /// Returns the next tokenId to mint using {_mintConsecutive}. It will + /// return [`FIRST_CONSECUTIVE_ID`] if no consecutive tokenId has been + /// minted before. + fn next_consecutive_id(&self) -> U96 { + match self.sequential_ownership.latest_checkpoint() { + None => FIRST_CONSECUTIVE_ID, + Some((latest_id, _)) => latest_id + uint!(1_U96), + } + } } From a243066db574c07865be2f5745fd37ae7a0f1924 Mon Sep 17 00:00:00 2001 From: alexfertel Date: Wed, 3 Jul 2024 17:07:52 +0200 Subject: [PATCH 30/95] feat(access): add AccessControl e2e tests (#178) Resolves #175 --- Cargo.lock | 4 + examples/access-control/Cargo.toml | 9 + examples/access-control/src/lib.rs | 19 +- examples/access-control/tests/abi.rs | 27 ++ .../access-control/tests/access_control.rs | 419 ++++++++++++++++++ 5 files changed, 472 insertions(+), 6 deletions(-) create mode 100644 examples/access-control/tests/abi.rs create mode 100644 examples/access-control/tests/access_control.rs diff --git a/Cargo.lock b/Cargo.lock index 0c130474..e5574bde 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6,11 +6,15 @@ version = 3 name = "access-control-example" version = "0.0.0" dependencies = [ + "alloy", "alloy-primitives 0.3.3", + "e2e", + "eyre", "mini-alloc", "openzeppelin-stylus", "stylus-proc", "stylus-sdk", + "tokio", ] [[package]] diff --git a/examples/access-control/Cargo.toml b/examples/access-control/Cargo.toml index 51dd3427..d3fdeef2 100644 --- a/examples/access-control/Cargo.toml +++ b/examples/access-control/Cargo.toml @@ -13,5 +13,14 @@ 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 + [lib] crate-type = ["lib", "cdylib"] + +[features] +e2e = [] diff --git a/examples/access-control/src/lib.rs b/examples/access-control/src/lib.rs index e4f530ae..43078d1c 100644 --- a/examples/access-control/src/lib.rs +++ b/examples/access-control/src/lib.rs @@ -3,7 +3,7 @@ extern crate alloc; use alloc::vec::Vec; -use alloy_primitives::{Address, U256}; +use alloy_primitives::{Address, B256, U256}; use openzeppelin_stylus::{ access::control::AccessControl, token::erc20::{Erc20, IErc20}, @@ -20,15 +20,16 @@ sol_storage! { } } +pub const TRANSFER_ROLE: [u8; 32] = [ + 133, 2, 35, 48, 150, 217, 9, 190, 251, 218, 9, 153, 187, 142, 162, 243, + 166, 190, 60, 19, 139, 159, 191, 0, 55, 82, 164, 200, 188, 232, 111, 108, +]; + #[external] #[inherit(Erc20, AccessControl)] impl AccessControlExample { // `keccak256("TRANSFER_ROLE")` - pub const TRANSFER_ROLE: [u8; 32] = [ - 133, 2, 35, 48, 150, 217, 9, 190, 251, 218, 9, 153, 187, 142, 162, 243, - 166, 190, 60, 19, 139, 159, 191, 0, 55, 82, 164, 200, 188, 232, 111, - 108, - ]; + pub const TRANSFER_ROLE: [u8; 32] = TRANSFER_ROLE; pub fn make_admin(&mut self, account: Address) -> Result<(), Vec> { self.access.only_role(AccessControl::DEFAULT_ADMIN_ROLE.into())?; @@ -47,4 +48,10 @@ impl AccessControlExample { let transfer_result = self.erc20.transfer_from(from, to, value)?; Ok(transfer_result) } + + // WARNING: This should not be part of the public API, it's here for testing + // purposes only. + pub fn set_role_admin(&mut self, role: B256, new_admin_role: B256) { + self.access._set_role_admin(role, new_admin_role) + } } diff --git a/examples/access-control/tests/abi.rs b/examples/access-control/tests/abi.rs new file mode 100644 index 00000000..c75c3713 --- /dev/null +++ b/examples/access-control/tests/abi.rs @@ -0,0 +1,27 @@ +#![allow(dead_code)] +use alloy::sol; + +sol!( + #[sol(rpc)] + contract AccessControl { + constructor(); + + function hasRole(bytes32 role, address account) public view virtual returns (bool hasRole); + function getRoleAdmin(bytes32 role) public view virtual returns (bytes32 role); + function grantRole(bytes32 role, address account) public virtual; + function revokeRole(bytes32 role, address account) public virtual; + function renounceRole(bytes32 role, address callerConfirmation) public virtual; + + function setRoleAdmin(bytes32 role, bytes32 adminRole) public virtual; + + error AccessControlUnauthorizedAccount(address account, bytes32 neededRole); + error AccessControlBadConfirmation(); + + #[derive(Debug, PartialEq)] + event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole); + #[derive(Debug, PartialEq)] + event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); + #[derive(Debug, PartialEq)] + event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender); + } +); diff --git a/examples/access-control/tests/access_control.rs b/examples/access-control/tests/access_control.rs new file mode 100644 index 00000000..e33f9f60 --- /dev/null +++ b/examples/access-control/tests/access_control.rs @@ -0,0 +1,419 @@ +#![cfg(feature = "e2e")] + +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; + +mod abi; + +const DEFAULT_ADMIN_ROLE: [u8; 32] = + openzeppelin_stylus::access::control::AccessControl::DEFAULT_ADMIN_ROLE; +const ROLE: [u8; 32] = access_control_example::TRANSFER_ROLE; +const NEW_ADMIN_ROLE: [u8; 32] = + hex!("879ce0d4bfd332649ca3552efe772a38d64a315eb70ab69689fd309c735946b5"); + +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 +} + +// ============================================================================ +// Integration Tests: AccessControl +// ============================================================================ + +#[e2e::test] +async fn constructs(alice: Account) -> Result<()> { + let alice_addr = alice.address(); + let contract_addr = deploy(&alice).await?; + let contract = AccessControl::new(contract_addr, &alice.wallet); + + let AccessControl::hasRoleReturn { hasRole } = + contract.hasRole(DEFAULT_ADMIN_ROLE.into(), alice_addr).call().await?; + assert_eq!(hasRole, true); + + Ok(()) +} + +#[e2e::test] +async fn other_roles_admin_is_the_default_admin_role( + alice: Account, +) -> Result<()> { + let contract_addr = deploy(&alice).await?; + let contract = AccessControl::new(contract_addr, &alice.wallet); + + let AccessControl::getRoleAdminReturn { role } = + contract.getRoleAdmin(ROLE.into()).call().await?; + assert_eq!(*role, DEFAULT_ADMIN_ROLE); + + Ok(()) +} + +#[e2e::test] +async fn default_role_is_default_admin(alice: Account) -> Result<()> { + let contract_addr = deploy(&alice).await?; + let contract = AccessControl::new(contract_addr, &alice.wallet); + + let AccessControl::getRoleAdminReturn { role } = + contract.getRoleAdmin(ROLE.into()).call().await?; + assert_eq!(*role, DEFAULT_ADMIN_ROLE); + + let AccessControl::getRoleAdminReturn { role } = + contract.getRoleAdmin(DEFAULT_ADMIN_ROLE.into()).call().await?; + assert_eq!(*role, DEFAULT_ADMIN_ROLE); + + Ok(()) +} + +#[e2e::test] +async fn error_when_non_admin_grants_role( + alice: Account, + bob: Account, +) -> Result<()> { + let contract_addr = deploy(&alice).await?; + let contract = AccessControl::new(contract_addr, &bob.wallet); + + let err = send!(contract.grantRole(ROLE.into(), alice.address())) + .expect_err("should not have permission to grant roles"); + assert!(err.reverted_with( + AccessControl::AccessControlUnauthorizedAccount { + account: bob.address(), + neededRole: DEFAULT_ADMIN_ROLE.into() + } + )); + + Ok(()) +} + +#[e2e::test] +async fn accounts_can_be_granted_roles_multiple_times( + alice: Account, + bob: Account, +) -> Result<()> { + let contract_addr = deploy(&alice).await?; + let contract = AccessControl::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + + let receipt = receipt!(contract.grantRole(ROLE.into(), bob_addr))?; + assert!(receipt.emits(RoleGranted { + role: ROLE.into(), + account: bob_addr, + sender: alice_addr + })); + let receipt = receipt!(contract.grantRole(ROLE.into(), bob_addr))?; + assert!(!receipt.emits(RoleGranted { + role: ROLE.into(), + account: bob_addr, + sender: alice_addr + })); + + Ok(()) +} + +#[e2e::test] +async fn not_granted_roles_can_be_revoked(alice: Account) -> Result<()> { + let alice_addr = alice.address(); + let contract_addr = deploy(&alice).await?; + let contract = AccessControl::new(contract_addr, &alice.wallet); + + let AccessControl::hasRoleReturn { hasRole } = + contract.hasRole(ROLE.into(), alice_addr).call().await?; + assert_eq!(hasRole, false); + + let receipt = receipt!(contract.revokeRole(ROLE.into(), alice_addr))?; + assert!(!receipt.emits(RoleRevoked { + role: ROLE.into(), + account: alice_addr, + sender: alice_addr + })); + + Ok(()) +} + +#[e2e::test] +async fn admin_can_revoke_role(alice: Account, bob: Account) -> Result<()> { + let contract_addr = deploy(&alice).await?; + let contract = AccessControl::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + + let _ = watch!(contract.grantRole(ROLE.into(), bob_addr))?; + + let receipt = receipt!(contract.revokeRole(ROLE.into(), bob_addr))?; + assert!(receipt.emits(RoleRevoked { + role: ROLE.into(), + account: bob_addr, + sender: alice_addr + })); + + Ok(()) +} + +#[e2e::test] +async fn error_when_non_admin_revokes_role( + alice: Account, + bob: Account, +) -> Result<()> { + let contract_addr = deploy(&alice).await?; + let contract = AccessControl::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + + let _ = watch!(contract.grantRole(ROLE.into(), alice_addr))?; + + let contract = AccessControl::new(contract_addr, &bob.wallet); + let err = send!(contract.revokeRole(ROLE.into(), alice_addr)) + .expect_err("non-admin should not be able to revoke role"); + assert!(err.reverted_with(AccessControlUnauthorizedAccount { + account: bob_addr, + neededRole: DEFAULT_ADMIN_ROLE.into() + })); + + Ok(()) +} + +#[e2e::test] +async fn roles_can_be_revoked_multiple_times( + alice: Account, + bob: Account, +) -> Result<()> { + let contract_addr = deploy(&alice).await?; + let contract = AccessControl::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + + let _ = watch!(contract.revokeRole(ROLE.into(), bob_addr))?; + let receipt = receipt!(contract.revokeRole(ROLE.into(), bob_addr))?; + assert!(!receipt.emits(RoleRevoked { + role: ROLE.into(), + account: bob_addr, + sender: alice_addr + })); + + Ok(()) +} + +#[e2e::test] +async fn not_granted_roles_can_be_renounced(alice: Account) -> Result<()> { + let alice_addr = alice.address(); + let contract_addr = deploy(&alice).await?; + let contract = AccessControl::new(contract_addr, &alice.wallet); + + let receipt = receipt!(contract.renounceRole(ROLE.into(), alice_addr))?; + assert!(!receipt.emits(RoleRevoked { + role: ROLE.into(), + account: alice_addr, + sender: alice_addr + })); + + Ok(()) +} + +#[e2e::test] +async fn bearer_can_renounce_role(alice: Account, bob: Account) -> Result<()> { + let bob_addr = bob.address(); + let contract_addr = deploy(&alice).await?; + let contract = AccessControl::new(contract_addr, &alice.wallet); + + let _ = watch!(contract.grantRole(ROLE.into(), bob_addr))?; + + let contract = AccessControl::new(contract_addr, &bob.wallet); + let receipt = receipt!(contract.renounceRole(ROLE.into(), bob_addr))?; + assert!(receipt.emits(RoleRevoked { + role: ROLE.into(), + account: bob_addr, + sender: bob_addr + })); + + Ok(()) +} + +#[e2e::test] +async fn error_when_the_one_renouncing_is_not_the_sender( + alice: Account, + bob: Account, +) -> Result<()> { + let contract_addr = deploy(&alice).await?; + let contract = AccessControl::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + + let _ = watch!(contract.grantRole(ROLE.into(), bob_addr))?; + + let contract = AccessControl::new(contract_addr, &bob.wallet); + let err = send!(contract.renounceRole(ROLE.into(), alice_addr)) + .expect_err("only sender should be able to renounce"); + assert!(err.reverted_with(AccessControlBadConfirmation {})); + + Ok(()) +} + +#[e2e::test] +async fn roles_can_be_renounced_multiple_times(alice: Account) -> Result<()> { + let alice_addr = alice.address(); + let contract_addr = deploy(&alice).await?; + let contract = AccessControl::new(contract_addr, &alice.wallet); + + let _ = watch!(contract.renounceRole(ROLE.into(), alice_addr))?; + let receipt = receipt!(contract.renounceRole(ROLE.into(), alice_addr))?; + assert!(!receipt.emits(RoleRevoked { + role: ROLE.into(), + account: alice_addr, + sender: alice_addr + })); + + Ok(()) +} + +#[e2e::test] +async fn a_roles_admin_role_can_change(alice: Account) -> Result<()> { + let contract_addr = deploy(&alice).await?; + let contract = AccessControl::new(contract_addr, &alice.wallet); + + let receipt = + receipt!(contract.setRoleAdmin(ROLE.into(), NEW_ADMIN_ROLE.into()))?; + assert!(receipt.emits(RoleAdminChanged { + role: ROLE.into(), + previousAdminRole: DEFAULT_ADMIN_ROLE.into(), + newAdminRole: NEW_ADMIN_ROLE.into() + })); + + let AccessControl::getRoleAdminReturn { role } = + contract.getRoleAdmin(ROLE.into()).call().await?; + assert_eq!(*role, NEW_ADMIN_ROLE); + + Ok(()) +} + +#[e2e::test] +async fn the_new_admin_can_grant_roles( + alice: Account, + bob: Account, +) -> Result<()> { + let contract_addr = deploy(&alice).await?; + let contract = AccessControl::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + + let receipt = + receipt!(contract.setRoleAdmin(ROLE.into(), NEW_ADMIN_ROLE.into()))?; + assert!(receipt.emits(RoleAdminChanged { + role: ROLE.into(), + previousAdminRole: DEFAULT_ADMIN_ROLE.into(), + newAdminRole: NEW_ADMIN_ROLE.into() + })); + + let _ = watch!(contract.grantRole(NEW_ADMIN_ROLE.into(), bob_addr))?; + + let contract = AccessControl::new(contract_addr, &bob.wallet); + let receipt = receipt!(contract.grantRole(ROLE.into(), alice_addr))?; + assert!(receipt.emits(RoleGranted { + role: ROLE.into(), + account: alice_addr, + sender: bob_addr + })); + + Ok(()) +} + +#[e2e::test] +async fn the_new_admin_can_revoke_roles( + alice: Account, + bob: Account, +) -> Result<()> { + let contract_addr = deploy(&alice).await?; + let contract = AccessControl::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + + let receipt = + receipt!(contract.setRoleAdmin(ROLE.into(), NEW_ADMIN_ROLE.into()))?; + assert!(receipt.emits(RoleAdminChanged { + role: ROLE.into(), + previousAdminRole: DEFAULT_ADMIN_ROLE.into(), + newAdminRole: NEW_ADMIN_ROLE.into() + })); + + let _ = watch!(contract.grantRole(NEW_ADMIN_ROLE.into(), bob_addr))?; + + let contract = AccessControl::new(contract_addr, &bob.wallet); + let _ = watch!(contract.grantRole(ROLE.into(), alice_addr))?; + let receipt = receipt!(contract.revokeRole(ROLE.into(), alice_addr))?; + assert!(receipt.emits(RoleRevoked { + role: ROLE.into(), + account: alice_addr, + sender: bob_addr + })); + + Ok(()) +} + +#[e2e::test] +async fn error_when_previous_admin_grants_roles( + alice: Account, + bob: Account, +) -> Result<()> { + let contract_addr = deploy(&alice).await?; + let contract = AccessControl::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + + let receipt = + receipt!(contract.setRoleAdmin(ROLE.into(), NEW_ADMIN_ROLE.into()))?; + assert!(receipt.emits(RoleAdminChanged { + role: ROLE.into(), + previousAdminRole: DEFAULT_ADMIN_ROLE.into(), + newAdminRole: NEW_ADMIN_ROLE.into() + })); + + let err = send!(contract.grantRole(ROLE.into(), bob_addr)) + .expect_err("previous admins can't grant roles after admin change"); + assert!(err.reverted_with(AccessControlUnauthorizedAccount { + account: alice_addr, + neededRole: NEW_ADMIN_ROLE.into() + })); + + Ok(()) +} + +#[e2e::test] +async fn error_when_previous_admin_revokes_roles( + alice: Account, + bob: Account, +) -> Result<()> { + let contract_addr = deploy(&alice).await?; + let contract = AccessControl::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + + let receipt = + receipt!(contract.setRoleAdmin(ROLE.into(), NEW_ADMIN_ROLE.into()))?; + assert!(receipt.emits(RoleAdminChanged { + role: ROLE.into(), + previousAdminRole: DEFAULT_ADMIN_ROLE.into(), + newAdminRole: NEW_ADMIN_ROLE.into() + })); + + let err = send!(contract.revokeRole(ROLE.into(), bob_addr)) + .expect_err("previous admins can't revoke roles after admin change"); + assert!(err.reverted_with(AccessControlUnauthorizedAccount { + account: alice_addr, + neededRole: NEW_ADMIN_ROLE.into() + })); + + Ok(()) +} From b5990447b60abffd5bc553376ac3c7c28ff227a1 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Wed, 3 Jul 2024 20:11:52 +0400 Subject: [PATCH 31/95] add test scratch for consecutive --- examples/erc721-consecutive/tests/abi.rs | 38 +++++++++++++++++++++ examples/erc721-consecutive/tests/erc721.rs | 29 ++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 examples/erc721-consecutive/tests/abi.rs create mode 100644 examples/erc721-consecutive/tests/erc721.rs diff --git a/examples/erc721-consecutive/tests/abi.rs b/examples/erc721-consecutive/tests/abi.rs new file mode 100644 index 00000000..268ef73f --- /dev/null +++ b/examples/erc721-consecutive/tests/abi.rs @@ -0,0 +1,38 @@ +#![allow(dead_code)] +use alloy::sol; + +sol!( + #[sol(rpc)] + contract Erc721 { + function balanceOf(address owner) external view returns (uint256 balance); + 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 burn(uint256 tokenId) external; + function mintConsecutive(address to, uint128 batchSize) 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); + + #[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); + } +); diff --git a/examples/erc721-consecutive/tests/erc721.rs b/examples/erc721-consecutive/tests/erc721.rs new file mode 100644 index 00000000..9ee6db18 --- /dev/null +++ b/examples/erc721-consecutive/tests/erc721.rs @@ -0,0 +1,29 @@ +#![cfg(feature = "e2e")] + +use alloy::primitives::{Address, U256}; +use e2e::{Account, EventExt, Revert}; + +use crate::abi::Erc721; + +mod abi; + +fn random_token_id() -> U256 { + let num: u32 = rand::random(); + U256::from(num) +} + +async fn deploy(rpc_url: &str, private_key: &str) -> eyre::Result
{ + e2e::deploy(rpc_url, private_key, None).await +} + +#[e2e::test] +async fn constructs(alice: Account) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let res = contract.mintConsecutive(alice_addr, 10_u128).call().await?; + + todo!(); + Ok(()) +} From 1636d95c9e76be6406f9a3459cb15f72ed43140a Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Fri, 5 Jul 2024 09:39:21 +0400 Subject: [PATCH 32/95] add consecutive as a separate extension --- .../token/erc721/extensions/consecutive.rs | 701 ++++++++++++++++++ contracts/src/token/erc721/extensions/mod.rs | 1 + 2 files changed, 702 insertions(+) create mode 100644 contracts/src/token/erc721/extensions/consecutive.rs diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs new file mode 100644 index 00000000..365beeb9 --- /dev/null +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -0,0 +1,701 @@ +use alloy_primitives::{fixed_bytes, uint, Address, FixedBytes, U128, U256}; +use stylus_proc::{external, sol_storage}; +use stylus_sdk::{ + abi::Bytes, + call::Call, + evm, msg, + prelude::{AddressVM, TopLevelStorage}, +}; + +use crate::{ + token::erc721::{ + Approval, ApprovalForAll, ERC721IncorrectOwner, + ERC721InsufficientApproval, ERC721InvalidApprover, + ERC721InvalidOperator, ERC721InvalidOwner, ERC721InvalidReceiver, + ERC721InvalidSender, ERC721NonexistentToken, Error, IERC721Receiver, + IErc721, Transfer, + }, + utils::math::storage::{AddAssignUnchecked, SubAssignUnchecked}, +}; + +sol_storage! { + /// State of an [`Erc721`] token. + #[cfg_attr(all(test, feature = "std"), derive(motsu::DefaultStorageLayout))] + pub struct Erc721Consecutive { + /// Maps tokens to owners. + mapping(uint256 => address) _owners; + /// Maps users to balances. + mapping(address => uint256) _balances; + /// Maps tokens to approvals. + mapping(uint256 => address) _token_approvals; + /// Maps owners to a mapping of operator approvals. + mapping(address => mapping(address => bool)) _operator_approvals; + } +} + +unsafe impl TopLevelStorage for Erc721Consecutive {} + +#[external] +impl IErc721 for Erc721Consecutive { + fn balance_of(&self, owner: Address) -> Result { + if owner.is_zero() { + return Err(ERC721InvalidOwner { owner: Address::ZERO }.into()); + } + Ok(self._balances.get(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)?; + self._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( + ERC721InvalidReceiver { receiver: Address::ZERO }.into() + ); + } + + // Setting an "auth" argument enables the `_is_authorized` check which + // verifies that the token exists (`from != 0`). 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(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> { + self._set_approval_for_all(msg::sender(), operator, approved) + } + + fn get_approved(&self, token_id: U256) -> Result { + self._require_owned(token_id)?; + Ok(self._get_approved_inner(token_id)) + } + + fn is_approved_for_all(&self, owner: Address, operator: Address) -> bool { + self._operator_approvals.get(owner).get(operator) + } +} + +impl Erc721Consecutive { + /// Returns the owner of the `token_id`. Does NOT revert if the token + /// doesn't exist. + /// + /// IMPORTANT: Any overrides to this function that add ownership of tokens + /// 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`. + /// + /// # 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 { + self._owners.get(token_id) + } + + /// Returns the approved address for `token_id`. + /// Returns 0 if `token_id` is not minted. + /// + /// # Arguments + /// + /// * `&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 { + self._token_approvals.get(token_id) + } + + /// Returns whether `spender` is allowed to manage `owner`'s tokens, or + /// `token_id` in particular (ignoring whether it is owned by `owner`). + /// + /// WARNING: This function assumes that `owner` is the actual owner of + /// `token_id` and does not verify this assumption. + /// + /// # Arguments + /// + /// * `&self` - Read access to the contract's state. + /// * `owner` - Account of the token's owner. + /// * `spender` - Account that will spend token. + /// * `token_id` - Token id as a number. + #[must_use] + pub fn _is_authorized( + &self, + owner: Address, + spender: Address, + token_id: U256, + ) -> bool { + !spender.is_zero() + && (owner == spender + || self.is_approved_for_all(owner, spender) + || self._get_approved_inner(token_id) == spender) + } + + /// Checks if `operator` can operate on `token_id`, assuming the provided + /// `owner` is the actual owner. Reverts if: + /// - `operator` does not have approval from `owner` for `token_id`. + /// - `operator` does not have approval to manage all of `owner`'s assets. + /// + /// WARNING: This function assumes that `owner` is the actual owner of + /// `token_id` and does not verify this assumption. + /// + /// # Arguments + /// + /// * `&self` - Read access to the contract's state. + /// * `owner` - Account of the token's owner. + /// * `operator` - Account that will spend token. + /// * `token_id` - Token id as a number. + /// + /// # Errors + /// + /// If the token does not exist, then the error + /// [`Error::NonexistentToken`] is returned. + /// If `spender` does not have the right to approve, then the error + /// [`Error::InsufficientApproval`] is returned. + pub fn _check_authorized( + &self, + owner: Address, + operator: Address, + token_id: U256, + ) -> Result<(), Error> { + if self._is_authorized(owner, operator, token_id) { + return Ok(()); + } + + if owner.is_zero() { + Err(ERC721NonexistentToken { token_id }.into()) + } else { + Err(ERC721InsufficientApproval { operator, token_id }.into()) + } + } + + /// Unsafe write access to the balances, used by extensions that "mint" + /// tokens using an [`Self::owner_of`] override. + /// + /// NOTE: the value is limited to type(uint128).max. This protects against + /// _balance overflow. It is unrealistic that a `U256` would ever + /// overflow from increments when these increments are bounded to `u128` + /// 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 + /// resolve the ownership of the corresponding tokens so that balances and + /// ownership remain consistent with one another. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `account` - Account to increase balance. + /// * `value` - The number of tokens to increase balance. + // TODO: Right now this function is pointless since it is not used. + // But once we will be able to override internal functions, + // it will make a difference. + pub fn _increase_balance(&mut self, account: Address, value: U128) { + self._balances.setter(account).add_assign_unchecked(U256::from(value)); + } + + /// 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 [`Self::_increase_balance`]. + /// + /// # 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 [`Error::NonexistentToken`] is returned. + /// If `auth` is not `Address::ZERO` and `auth` does not have a right to + /// approve this token, then the error + /// [`Error::InsufficientApproval`] is returned. + /// + /// # Events + /// + /// Emits a [`Transfer`] event. + pub fn _update( + &mut self, + to: Address, + token_id: U256, + auth: Address, + ) -> Result { + let from = self._owner_of_inner(token_id); + + // Perform (optional) operator check. + if !auth.is_zero() { + self._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._balances.setter(from).sub_assign_unchecked(uint!(1_U256)); + } + + if !to.is_zero() { + self._balances.setter(to).add_assign_unchecked(uint!(1_U256)); + } + + self._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 + /// [`Error::InvalidSender`] is returned. + /// If `to` is `Address::ZERO`, then the error + /// [`Error::InvalidReceiver`] is returned. + /// + /// # Requirements: + /// + /// * `token_id` must not exist. + /// * `to` cannot be the zero address. + /// + /// # Events + /// + /// Emits a [`Transfer`] event. + pub fn _mint(&mut self, to: Address, token_id: U256) -> Result<(), Error> { + if to.is_zero() { + return Err( + ERC721InvalidReceiver { receiver: Address::ZERO }.into() + ); + } + + let previous_owner = self._update(to, token_id, Address::ZERO)?; + if !previous_owner.is_zero() { + return Err(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 + /// [`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 + /// [`Self::_check_on_erc721_received`]. + /// + /// # Errors + /// + /// If `token_id` already exists, then the error + /// [`Error::InvalidSender`] is returned. + /// If `to` is `Address::ZERO`, then the error + /// [`Error::InvalidReceiver`] is returned. + /// If [`IERC721Receiver::on_erc_721_received`] hasn't returned its + /// interface id or returned with error, then the error + /// [`Error::InvalidReceiver`] is returned. + /// + /// # Requirements: + /// + /// * `token_id` must not exist. + /// * If `to` refers to a smart contract, it must implement + /// [`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)?; + self._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 + /// [`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(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 + /// [`Error::InvalidReceiver`] is returned. + /// If `token_id` does not exist, then the error + /// [`Error::ERC721NonexistentToken`] is returned. + /// If the previous owner is not `from`, then the error + /// [`Error::IncorrectOwner`] is returned. + /// + /// # Requirements: + /// + /// * `to` cannot be the zero address. + /// * 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( + ERC721InvalidReceiver { receiver: Address::ZERO }.into() + ); + } + + let previous_owner = self._update(to, token_id, Address::ZERO)?; + if previous_owner.is_zero() { + return Err(ERC721NonexistentToken { token_id }.into()); + } else if previous_owner != from { + return Err(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 [`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 + /// [`Self::_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 + /// [`Error::ERC721NonexistentToken`] is returned. + /// If the previous owner is not `from`, then the error + /// [`Error::IncorrectOwner`] is returned. + /// + /// # Requirements: + /// + /// * The `token_id` token must exist and be owned by `from`. + /// * `to` cannot be the zero address. + /// * `from` cannot be the zero address. + /// * If `to` refers to a smart contract, it must implement + /// [`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)?; + 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. + /// + /// # 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 + /// [`Error::NonexistentToken`] is returned. + /// If `auth` does not have a right to approve this token, then the error + /// [`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(ERC721InvalidApprover { approver: auth }.into()); + } + + if emit_event { + evm::log(Approval { owner, approved: to, token_id }); + } + } + + self._token_approvals.setter(token_id).set(to); + Ok(()) + } + + /// Approve `operator` to operate on all of `owner`'s tokens. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `owner` - Account the token's owner. + /// * `operator` - Account to add to the set of authorized operators. + /// * `approved` - Whether permission will be granted. If true, this means. + /// + /// # Errors + /// + /// If `operator` is `Address::ZERO`, then the error + /// [`Error::InvalidOperator`] is returned. + /// + /// # Requirements: + /// + /// * `operator` can't be the address zero. + /// + /// # Events + /// + /// Emits an [`ApprovalForAll`] event. + pub fn _set_approval_for_all( + &mut self, + owner: Address, + operator: Address, + approved: bool, + ) -> Result<(), Error> { + if operator.is_zero() { + return Err(ERC721InvalidOperator { operator }.into()); + } + + self._operator_approvals.setter(owner).setter(operator).set(approved); + evm::log(ApprovalForAll { owner, operator, approved }); + 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_inner`]. + /// + /// # Errors + /// + /// If token does not exist, then the error + /// [`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_inner(token_id); + if owner.is_zero() { + return Err(ERC721NonexistentToken { token_id }.into()); + } + Ok(owner) + } + + /// Performs an acceptance check for the provided `operator` by calling + /// [`IERC721Receiver::on_erc_721_received`] on the `to` address. The + /// `operator` is generally the address that initiated the token transfer + /// (i.e. `msg::sender()`). + /// + /// The acceptance call is not executed and treated as a no-op + /// if the target address doesn't contain code (i.e. an EOA). + /// Otherwise, the recipient must implement + /// [`IERC721Receiver::on_erc_721_received`] and return the + /// acceptance magic value to accept the transfer. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `operator` - Account to add to the set of authorized operators. + /// * `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 call to + /// `to`. + /// + /// # Errors + /// + /// If [`IERC721Receiver::on_erc_721_received`] hasn't returned its + /// interface id or returned with error, then the error + /// [`Error::InvalidReceiver`] is returned. + pub fn _check_on_erc721_received( + &mut self, + operator: Address, + from: Address, + to: Address, + token_id: U256, + data: &Bytes, + ) -> Result<(), Error> { + const IERC721RECEIVER_INTERFACE_ID: FixedBytes<4> = + fixed_bytes!("150b7a02"); + + // FIXME: Cleanup this code once it's covered in the test suite. + if to.has_code() { + let call = Call::new_in(self); + return match IERC721Receiver::new(to).on_erc_721_received( + call, + operator, + from, + token_id, + data.to_vec(), + ) { + Ok(result) => { + if result == IERC721RECEIVER_INTERFACE_ID { + Ok(()) + } else { + Err(ERC721InvalidReceiver { receiver: to }.into()) + } + } + Err(_) => Err(ERC721InvalidReceiver { receiver: to }.into()), + }; + } + Ok(()) + } +} 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; From 863e415ed93685bd4731914d009dfee8c53b0e4d Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Fri, 5 Jul 2024 10:11:08 +0400 Subject: [PATCH 33/95] ++ --- .../token/erc721/extensions/consecutive.rs | 269 +++--------------- 1 file changed, 37 insertions(+), 232 deletions(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index 365beeb9..5a5bd705 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -1,19 +1,12 @@ -use alloy_primitives::{fixed_bytes, uint, Address, FixedBytes, U128, U256}; +use alloy_primitives::{uint, Address, U256}; use stylus_proc::{external, sol_storage}; -use stylus_sdk::{ - abi::Bytes, - call::Call, - evm, msg, - prelude::{AddressVM, TopLevelStorage}, -}; +use stylus_sdk::{abi::Bytes, evm, msg, prelude::TopLevelStorage}; use crate::{ token::erc721::{ - Approval, ApprovalForAll, ERC721IncorrectOwner, - ERC721InsufficientApproval, ERC721InvalidApprover, - ERC721InvalidOperator, ERC721InvalidOwner, ERC721InvalidReceiver, - ERC721InvalidSender, ERC721NonexistentToken, Error, IERC721Receiver, - IErc721, Transfer, + Approval, ERC721IncorrectOwner, ERC721InvalidApprover, + ERC721InvalidReceiver, ERC721InvalidSender, ERC721NonexistentToken, + Erc721, Error, IERC721Receiver, IErc721, Transfer, }, utils::math::storage::{AddAssignUnchecked, SubAssignUnchecked}, }; @@ -22,14 +15,7 @@ sol_storage! { /// State of an [`Erc721`] token. #[cfg_attr(all(test, feature = "std"), derive(motsu::DefaultStorageLayout))] pub struct Erc721Consecutive { - /// Maps tokens to owners. - mapping(uint256 => address) _owners; - /// Maps users to balances. - mapping(address => uint256) _balances; - /// Maps tokens to approvals. - mapping(uint256 => address) _token_approvals; - /// Maps owners to a mapping of operator approvals. - mapping(address => mapping(address => bool)) _operator_approvals; + Erc721 erc721; } } @@ -38,10 +24,7 @@ unsafe impl TopLevelStorage for Erc721Consecutive {} #[external] impl IErc721 for Erc721Consecutive { fn balance_of(&self, owner: Address) -> Result { - if owner.is_zero() { - return Err(ERC721InvalidOwner { owner: Address::ZERO }.into()); - } - Ok(self._balances.get(owner)) + self.erc721.balance_of(owner) } fn owner_of(&self, token_id: U256) -> Result { @@ -68,7 +51,13 @@ impl IErc721 for Erc721Consecutive { data: Bytes, ) -> Result<(), Error> { self.transfer_from(from, to, token_id)?; - self._check_on_erc721_received(msg::sender(), from, to, token_id, &data) + self.erc721._check_on_erc721_received( + msg::sender(), + from, + to, + token_id, + &data, + ) } fn transfer_from( @@ -107,16 +96,16 @@ impl IErc721 for Erc721Consecutive { operator: Address, approved: bool, ) -> Result<(), Error> { - self._set_approval_for_all(msg::sender(), operator, approved) + self.erc721.set_approval_for_all(operator, approved) } fn get_approved(&self, token_id: U256) -> Result { self._require_owned(token_id)?; - Ok(self._get_approved_inner(token_id)) + Ok(self.erc721._get_approved_inner(token_id)) } fn is_approved_for_all(&self, owner: Address, operator: Address) -> bool { - self._operator_approvals.get(owner).get(operator) + self.erc721.is_approved_for_all(owner, operator) } } @@ -137,107 +126,7 @@ impl Erc721Consecutive { /// * `token_id` - Token id as a number. #[must_use] pub fn _owner_of_inner(&self, token_id: U256) -> Address { - self._owners.get(token_id) - } - - /// Returns the approved address for `token_id`. - /// Returns 0 if `token_id` is not minted. - /// - /// # Arguments - /// - /// * `&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 { - self._token_approvals.get(token_id) - } - - /// Returns whether `spender` is allowed to manage `owner`'s tokens, or - /// `token_id` in particular (ignoring whether it is owned by `owner`). - /// - /// WARNING: This function assumes that `owner` is the actual owner of - /// `token_id` and does not verify this assumption. - /// - /// # Arguments - /// - /// * `&self` - Read access to the contract's state. - /// * `owner` - Account of the token's owner. - /// * `spender` - Account that will spend token. - /// * `token_id` - Token id as a number. - #[must_use] - pub fn _is_authorized( - &self, - owner: Address, - spender: Address, - token_id: U256, - ) -> bool { - !spender.is_zero() - && (owner == spender - || self.is_approved_for_all(owner, spender) - || self._get_approved_inner(token_id) == spender) - } - - /// Checks if `operator` can operate on `token_id`, assuming the provided - /// `owner` is the actual owner. Reverts if: - /// - `operator` does not have approval from `owner` for `token_id`. - /// - `operator` does not have approval to manage all of `owner`'s assets. - /// - /// WARNING: This function assumes that `owner` is the actual owner of - /// `token_id` and does not verify this assumption. - /// - /// # Arguments - /// - /// * `&self` - Read access to the contract's state. - /// * `owner` - Account of the token's owner. - /// * `operator` - Account that will spend token. - /// * `token_id` - Token id as a number. - /// - /// # Errors - /// - /// If the token does not exist, then the error - /// [`Error::NonexistentToken`] is returned. - /// If `spender` does not have the right to approve, then the error - /// [`Error::InsufficientApproval`] is returned. - pub fn _check_authorized( - &self, - owner: Address, - operator: Address, - token_id: U256, - ) -> Result<(), Error> { - if self._is_authorized(owner, operator, token_id) { - return Ok(()); - } - - if owner.is_zero() { - Err(ERC721NonexistentToken { token_id }.into()) - } else { - Err(ERC721InsufficientApproval { operator, token_id }.into()) - } - } - - /// Unsafe write access to the balances, used by extensions that "mint" - /// tokens using an [`Self::owner_of`] override. - /// - /// NOTE: the value is limited to type(uint128).max. This protects against - /// _balance overflow. It is unrealistic that a `U256` would ever - /// overflow from increments when these increments are bounded to `u128` - /// 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 - /// resolve the ownership of the corresponding tokens so that balances and - /// ownership remain consistent with one another. - /// - /// # Arguments - /// - /// * `&mut self` - Write access to the contract's state. - /// * `account` - Account to increase balance. - /// * `value` - The number of tokens to increase balance. - // TODO: Right now this function is pointless since it is not used. - // But once we will be able to override internal functions, - // it will make a difference. - pub fn _increase_balance(&mut self, account: Address, value: U128) { - self._balances.setter(account).add_assign_unchecked(U256::from(value)); + self.erc721._owners.get(token_id) } /// Transfers `token_id` from its current owner to `to`, or alternatively @@ -279,7 +168,7 @@ impl Erc721Consecutive { // Perform (optional) operator check. if !auth.is_zero() { - self._check_authorized(from, auth, token_id)?; + self.erc721._check_authorized(from, auth, token_id)?; } // Execute the update. @@ -287,14 +176,20 @@ impl Erc721Consecutive { // Clear approval. No need to re-authorize or emit the `Approval` // event. self._approve(Address::ZERO, token_id, Address::ZERO, false)?; - self._balances.setter(from).sub_assign_unchecked(uint!(1_U256)); + self.erc721 + ._balances + .setter(from) + .sub_assign_unchecked(uint!(1_U256)); } if !to.is_zero() { - self._balances.setter(to).add_assign_unchecked(uint!(1_U256)); + self.erc721 + ._balances + .setter(to) + .add_assign_unchecked(uint!(1_U256)); } - self._owners.setter(token_id).set(to); + self.erc721._owners.setter(token_id).set(to); evm::log(Transfer { from, to, token_id }); Ok(from) } @@ -380,7 +275,7 @@ impl Erc721Consecutive { data: Bytes, ) -> Result<(), Error> { self._mint(to, token_id)?; - self._check_on_erc721_received( + self.erc721._check_on_erc721_received( msg::sender(), Address::ZERO, to, @@ -526,7 +421,13 @@ impl Erc721Consecutive { data: Bytes, ) -> Result<(), Error> { self._transfer(from, to, token_id)?; - self._check_on_erc721_received(msg::sender(), from, to, token_id, &data) + self.erc721._check_on_erc721_received( + msg::sender(), + from, + to, + token_id, + &data, + ) } /// Variant of `approve_inner` with an optional flag to enable or disable @@ -576,43 +477,7 @@ impl Erc721Consecutive { } } - self._token_approvals.setter(token_id).set(to); - Ok(()) - } - - /// Approve `operator` to operate on all of `owner`'s tokens. - /// - /// # Arguments - /// - /// * `&mut self` - Write access to the contract's state. - /// * `owner` - Account the token's owner. - /// * `operator` - Account to add to the set of authorized operators. - /// * `approved` - Whether permission will be granted. If true, this means. - /// - /// # Errors - /// - /// If `operator` is `Address::ZERO`, then the error - /// [`Error::InvalidOperator`] is returned. - /// - /// # Requirements: - /// - /// * `operator` can't be the address zero. - /// - /// # Events - /// - /// Emits an [`ApprovalForAll`] event. - pub fn _set_approval_for_all( - &mut self, - owner: Address, - operator: Address, - approved: bool, - ) -> Result<(), Error> { - if operator.is_zero() { - return Err(ERC721InvalidOperator { operator }.into()); - } - - self._operator_approvals.setter(owner).setter(operator).set(approved); - evm::log(ApprovalForAll { owner, operator, approved }); + self.erc721._token_approvals.setter(token_id).set(to); Ok(()) } @@ -638,64 +503,4 @@ impl Erc721Consecutive { } Ok(owner) } - - /// Performs an acceptance check for the provided `operator` by calling - /// [`IERC721Receiver::on_erc_721_received`] on the `to` address. The - /// `operator` is generally the address that initiated the token transfer - /// (i.e. `msg::sender()`). - /// - /// The acceptance call is not executed and treated as a no-op - /// if the target address doesn't contain code (i.e. an EOA). - /// Otherwise, the recipient must implement - /// [`IERC721Receiver::on_erc_721_received`] and return the - /// acceptance magic value to accept the transfer. - /// - /// # Arguments - /// - /// * `&mut self` - Write access to the contract's state. - /// * `operator` - Account to add to the set of authorized operators. - /// * `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 call to - /// `to`. - /// - /// # Errors - /// - /// If [`IERC721Receiver::on_erc_721_received`] hasn't returned its - /// interface id or returned with error, then the error - /// [`Error::InvalidReceiver`] is returned. - pub fn _check_on_erc721_received( - &mut self, - operator: Address, - from: Address, - to: Address, - token_id: U256, - data: &Bytes, - ) -> Result<(), Error> { - const IERC721RECEIVER_INTERFACE_ID: FixedBytes<4> = - fixed_bytes!("150b7a02"); - - // FIXME: Cleanup this code once it's covered in the test suite. - if to.has_code() { - let call = Call::new_in(self); - return match IERC721Receiver::new(to).on_erc_721_received( - call, - operator, - from, - token_id, - data.to_vec(), - ) { - Ok(result) => { - if result == IERC721RECEIVER_INTERFACE_ID { - Ok(()) - } else { - Err(ERC721InvalidReceiver { receiver: to }.into()) - } - } - Err(_) => Err(ERC721InvalidReceiver { receiver: to }.into()), - }; - } - Ok(()) - } } From 6f5b83e33a461e9386168cb469f55874da6344b9 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Fri, 5 Jul 2024 10:11:08 +0400 Subject: [PATCH 34/95] ++ --- .../token/erc721/extensions/consecutive.rs | 157 +++++++++++++++++- 1 file changed, 151 insertions(+), 6 deletions(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index 5a5bd705..f9580753 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -1,6 +1,9 @@ -use alloy_primitives::{uint, Address, U256}; +use alloy_primitives::{uint, Address, U128, U256}; +use alloy_sol_types::sol; use stylus_proc::{external, sol_storage}; -use stylus_sdk::{abi::Bytes, evm, msg, prelude::TopLevelStorage}; +use stylus_sdk::{ + abi::Bytes, call::MethodError, evm, msg, prelude::TopLevelStorage, +}; use crate::{ token::erc721::{ @@ -8,14 +11,155 @@ use crate::{ ERC721InvalidReceiver, ERC721InvalidSender, ERC721NonexistentToken, Erc721, Error, IERC721Receiver, IErc721, Transfer, }, - utils::math::storage::{AddAssignUnchecked, SubAssignUnchecked}, + utils::{ + math::storage::{AddAssignUnchecked, SubAssignUnchecked}, + structs::{ + bitmap::BitMap, + checkpoints::{Trace160, U160, U96}, + }, + }, }; sol_storage! { - /// State of an [`Erc721`] token. + /// State of an [`Erc72Erc721Consecutive`] token. #[cfg_attr(all(test, feature = "std"), derive(motsu::DefaultStorageLayout))] pub struct Erc721Consecutive { Erc721 erc721; + Trace160 sequential_ownership; + BitMap sequentian_burn; + } +} + +sol! { + /// Emitted when the tokens from `fromTokenId` to `toTokenId` are transferred from `fromAddress` to `toAddress`. + event ConsecutiveTransfer( + uint256 indexed fromTokenId, + uint256 toTokenId, + address indexed fromAddress, + address indexed toAddress + ); +} + +sol! { + /// Batch mint is restricted to the constructor. + /// Any batch mint not emitting the {IERC721-Transfer} event outside of the constructor + /// is non ERC-721 compliant. + #[derive(Debug)] + #[allow(missing_docs)] + error ERC721ForbiddenBatchMint(); + + /// Exceeds the max amount of mints per batch. + #[derive(Debug)] + #[allow(missing_docs)] + error ERC721ExceededMaxBatchMint(uint256 batchSize, uint256 maxBatch); + + /// Individual minting is not allowed. + #[derive(Debug)] + #[allow(missing_docs)] + error ERC721ForbiddenMint(); + + /// Batch burn is not supported. + #[derive(Debug)] + #[allow(missing_docs)] + error ERC721ForbiddenBatchBurn(); +} + +// 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. +const MAX_BATCH_SIZE: U96 = uint!(5000_U96); + +// Used to offset the first token id in {_nextConsecutiveId} +const FIRST_CONSECUTIVE_ID: U96 = uint!(0_U96); + +/// Consecutive extension related implementation: +impl Erc721Consecutive { + /// Override that checks the sequential ownership structure for tokens that + /// have been minted as part of a batch, and not yet transferred. + pub fn _owner_of_inner(&self, token_id: U256) -> Address { + self.__owner_of_inner(token_id) + } + + // Mint a batch of tokens of length `batchSize` for `to`. Returns the token + // id of the first token minted in the batch; if `batchSize` is 0, + // returns the number of consecutive ids minted so far. + // + // Requirements: + // + // - `batchSize` must not be greater than [`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 `onERC721Received` on the receiver. + // + // Emits a [`ConsecutiveTransfer`] event. + pub fn mint_consecutive( + &mut self, + to: Address, + batch_size: u128, // TODO: how to use U96 type + ) -> Result> { + let batch_size = U96::from(batch_size); + let next = self.next_consecutive_id(); + + if batch_size > U96::ZERO { + if to.is_zero() { + return Err( + ERC721InvalidReceiver { receiver: Address::ZERO }.encode() + ); + } + + if batch_size > MAX_BATCH_SIZE.to() { + return Err(ERC721ExceededMaxBatchMint { + batchSize: U256::from(batch_size), + maxBatch: U256::from(MAX_BATCH_SIZE), + } + .encode()); + } + + let last = next + batch_size - uint!(1_U96); + self.sequential_ownership + .push(last, U160::from_be_bytes(to.into_array())) + .map_err(Vec::::from)?; + + self.erc721._increase_balance(to, U128::from(batch_size)); + evm::log(ConsecutiveTransfer { + fromTokenId: next.to::(), + toTokenId: last.to::(), + fromAddress: Address::ZERO, + toAddress: to, + }); + }; + Ok(next.to()) + } + + /// Override version that restricts normal minting to after construction. + /// + /// WARNING: Using [`Erc721Consecutive`] prevents minting during + /// construction in favor of [`Erc721Consecutive::mint_consecutive`]. + /// After construction,[`Erc721Consecutive::mint_consecutive`] is no + /// longer available and minting through [`Erc721Consecutive::_update`] + /// becomes possible. + pub fn _update( + &mut self, + to: Address, + token_id: U256, + auth: Address, + ) -> Result { + self.__update(to, token_id, auth) + } + + /// Returns the next tokenId to mint using {_mintConsecutive}. It will + /// return [`FIRST_CONSECUTIVE_ID`] if no consecutive tokenId has been + /// minted before. + fn next_consecutive_id(&self) -> U96 { + match self.sequential_ownership.latest_checkpoint() { + None => FIRST_CONSECUTIVE_ID, + Some((latest_id, _)) => latest_id + uint!(1_U96), + } } } @@ -109,6 +253,7 @@ impl IErc721 for Erc721Consecutive { } } +// erc721 related implementation: impl Erc721Consecutive { /// Returns the owner of the `token_id`. Does NOT revert if the token /// doesn't exist. @@ -125,7 +270,7 @@ impl Erc721Consecutive { /// * `&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_inner(&self, token_id: U256) -> Address { self.erc721._owners.get(token_id) } @@ -158,7 +303,7 @@ impl Erc721Consecutive { /// # Events /// /// Emits a [`Transfer`] event. - pub fn _update( + pub fn __update( &mut self, to: Address, token_id: U256, From 9a9aeb72d8c01c6eb490725ccc8ac605abd2b836 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Jul 2024 11:40:22 +0200 Subject: [PATCH 35/95] build(deps): bump crate-ci/typos from 1.22.9 to 1.23.0 (#180) Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.22.9 to 1.23.0. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 7227b4d5..69f2b2f7 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -110,4 +110,4 @@ jobs: - name: Checkout Actions Repository uses: actions/checkout@v4 - name: Check spelling of files in the workspace - uses: crate-ci/typos@v1.22.9 + uses: crate-ci/typos@v1.23.0 From c1aaec83e5cc319536daf2458fd2ae75d94743fe Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh <37006439+qalisander@users.noreply.github.com> Date: Fri, 5 Jul 2024 14:29:58 +0400 Subject: [PATCH 36/95] feat: add the Trace160 contract to keep checkpoints (#162) Checkpoint library contract that reflects [solidity counterpart](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/c3f8b760ad9d0431f33e8303658ccbdcc1610f24/contracts/utils/structs/Checkpoints.sol#L16). Resolves #76 #### PR Checklist - [x] Tests - [x] Documentation --------- Co-authored-by: Alexander Gonzalez --- contracts/src/utils/structs/checkpoints.rs | 515 +++++++++++++++++++++ contracts/src/utils/structs/mod.rs | 1 + 2 files changed, 516 insertions(+) create mode 100644 contracts/src/utils/structs/checkpoints.rs diff --git a/contracts/src/utils/structs/checkpoints.rs b/contracts/src/utils/structs/checkpoints.rs new file mode 100644 index 00000000..bf9a03d2 --- /dev/null +++ b/contracts/src/utils/structs/checkpoints.rs @@ -0,0 +1,515 @@ +//! Contract for checkpointing values as they change at different points in +//! time, and later looking up and later looking up past values by block number. +//! +//! To create a history of checkpoints, define a variable type [`Trace160`] +//! in your contract, and store a new checkpoint for the current transaction +//! block using the [`Trace160::push`] function. +use alloy_primitives::{uint, Uint, U256, U32}; +use alloy_sol_types::sol; +use stylus_proc::{sol_storage, SolidityError}; +use stylus_sdk::storage::{StorageGuard, StorageGuardMut}; + +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>; + +sol! { + /// A value was attempted to be inserted into a past checkpoint. + #[derive(Debug)] + error CheckpointUnorderedInsertion(); +} + +/// An error that occurred while calling the [`Trace160`] checkpoint contract. +#[derive(SolidityError, Debug)] +pub enum Error { + /// A value was attempted to be inserted into a past checkpoint. + CheckpointUnorderedInsertion(CheckpointUnorderedInsertion), +} + +sol_storage! { + /// State of the checkpoint library contract. + #[cfg_attr(all(test, feature = "std"), derive(motsu::DefaultStorageLayout))] + pub struct Trace160 { + /// Stores checkpoints in a dynamic array sorted by key. + Checkpoint160[] _checkpoints; + } + + /// State of a single checkpoint. + pub struct Checkpoint160 { + /// The key of the checkpoint. Used as a sorting key. + uint96 _key; + /// The value corresponding to the key. + uint160 _value; + } +} + +impl Trace160 { + /// Pushes a (`key`, `value`) pair into a `Trace160` so that it is + /// stored as the checkpoint. + /// + /// Returns the previous value and the new value as an ordered pair. + /// + /// IMPORTANT: Never accept `key` as user input, since an arbitrary + /// `U96::MAX` key set will disable the library. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the checkpoint's state. + /// * `key` - Latest checkpoint key to insert. + /// * `value` - Checkpoint value corresponding to `key`. + /// + /// # Errors + /// + /// If the `key` is lower than previously pushed checkpoint's key, the error + /// [`Error::CheckpointUnorderedInsertion`] is returned (necessary to + /// maintain sorted order). + pub fn push( + &mut self, + key: U96, + value: U160, + ) -> Result<(U160, U160), Error> { + self._insert(key, value) + } + + /// Returns the value in the first (oldest) checkpoint with key greater or + /// equal than the search key, or `U160::ZERO` if there is none. + /// + /// # Arguments + /// + /// * `&self` - Read access to the checkpoint's state. + /// * `key` - Checkpoint's key to lookup. + pub fn lower_lookup(&self, key: U96) -> U160 { + let len = self.length(); + let pos = self._lower_binary_lookup(key, U256::ZERO, len); + if pos == len { + U160::ZERO + } else { + self._index(pos)._value.get() + } + } + + /// Returns the value in the last (most recent) checkpoint with key + /// lower or equal than the search key, or `U160::ZERO` if there is none. + /// + /// # Arguments + /// + /// * `&self` - Read access to the checkpoint's state. + /// * `key` - Checkpoint's key to lookup. + pub fn upper_lookup(&self, key: U96) -> U160 { + let len = self.length(); + let pos = self._upper_binary_lookup(key, U256::ZERO, len); + if pos == U256::ZERO { + U160::ZERO + } else { + self._index(pos - uint!(1_U256))._value.get() + } + } + + /// Returns the value in the last (most recent) checkpoint with key lower or + /// equal than the search key, or `U160::ZERO` if there is none. + /// + /// This is a variant of [`Self::upper_lookup`] that is optimized to find + /// "recent" checkpoints (checkpoints with high keys). + /// + /// # Arguments + /// + /// * `&self` - Read access to the checkpoint's state. + /// * `key` - Checkpoint's key to query. + pub fn upper_lookup_recent(&self, key: U96) -> U160 { + let len = self.length(); + + let mut low = U256::ZERO; + let mut high = len; + + if len > uint!(5_U256) { + let mid = len - len.sqrt(); + if key < self._index(mid)._key.get() { + high = mid; + } else { + low = mid + uint!(1_U256); + } + } + + let pos = self._upper_binary_lookup(key, low, high); + + if pos == U256::ZERO { + U160::ZERO + } else { + self._index(pos - uint!(1_U256))._value.get() + } + } + + /// Returns the value in the most recent checkpoint, or `U160::ZERO` if + /// there are no checkpoints. + /// + /// # Arguments + /// + /// * `&self` - Read access to the checkpoint's state. + pub fn latest(&self) -> U160 { + let pos = self.length(); + if pos == U256::ZERO { + U160::ZERO + } else { + self._index(pos - uint!(1_U256))._value.get() + } + } + + /// Returns whether there is a checkpoint in the structure (i.g. it is not + /// empty), and if so, the key and value in the most recent checkpoint. + /// Otherwise, [`None`] will be returned. + /// + /// # Arguments + /// + /// * `&self` - Read access to the checkpoint's state. + pub fn latest_checkpoint(&self) -> Option<(U96, U160)> { + let pos = self.length(); + if pos == U256::ZERO { + None + } else { + let checkpoint = self._index(pos - uint!(1_U256)); + Some((checkpoint._key.get(), checkpoint._value.get())) + } + } + + /// Returns the number of checkpoints. + /// + /// # Arguments + /// + /// * `&self` - Read access to the checkpoint's state. + pub fn length(&self) -> U256 { + U256::from(self._checkpoints.len()) + } + + /// Returns checkpoint at given position. + /// + /// # Panics + /// + /// If `pos` exceeds [`Self::length`]. + /// + /// # Arguments + /// + /// * `&self` - Read access to the checkpoint's state. + /// * `pos` - Index of the checkpoint. + pub fn at(&self, pos: U32) -> (U96, U160) { + let guard = self._checkpoints.get(pos).unwrap_or_else(|| { + panic!("should get checkpoint at index `{pos}`") + }); + (guard._key.get(), guard._value.get()) + } + + /// Pushes a (`key`, `value`) pair into an ordered list of checkpoints, + /// either by inserting a new checkpoint, or by updating the last one. + /// Returns the previous value and the new value as an ordered pair. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the checkpoint's state. + /// * `key` - The key of the checkpoint to insert. + /// * `value` - Checkpoint value corresponding to insertion `key`. + /// + /// # Errors + /// + /// To maintain the sorted order if the `key` is lower than the previously + /// inserted one, the error [`Error::CheckpointUnorderedInsertion`] is + /// returned. + fn _insert( + &mut self, + key: U96, + value: U160, + ) -> Result<(U160, U160), Error> { + let pos = self.length(); + if pos > U256::ZERO { + let last = self._index(pos - uint!(1_U256)); + let last_key = last._key.get(); + let last_value = last._value.get(); + + // Checkpoint keys must be non-decreasing. + if last_key > key { + return Err(CheckpointUnorderedInsertion {}.into()); + } + + // Update or push new checkpoint + if last_key == key { + self._index_mut(pos - uint!(1_U256))._value.set(value); + } else { + self._unchecked_push(key, value); + } + Ok((last_value, value)) + } else { + self._unchecked_push(key, value); + Ok((U160::ZERO, value)) + } + } + + /// Return the index of the last (most recent) checkpoint with key lower or + /// equal than the search key, or `high` if there is none. + /// + /// Indexes `low` and `high` define a section where to do the search, with + /// inclusive `low` and exclusive `high`. + /// + /// WARNING: `high` should not be greater than the array's length. + /// + /// # Arguments + /// + /// * `&self` - Read access to the checkpoint's state. + /// * `key` - Checkpoint key to lookup. + /// * `low` - Inclusive index where search begins. + /// * `high` - Exclusive index where search ends. + fn _upper_binary_lookup( + &self, + key: U96, + mut low: U256, + mut high: U256, + ) -> U256 { + while low < high { + let mid = low.average(high); + if self._index(mid)._key.get() > key { + high = mid; + } else { + low = mid + uint!(1_U256); + } + } + high + } + + /// Return the index of the first (oldest) checkpoint with key is greater or + /// equal than the search key, or `high` if there is none. + /// + /// Indexes `low` and `high` define a section where to do the search, with + /// inclusive `low` and exclusive `high`. + /// + /// WARNING: `high` should not be greater than the array's length. + /// + /// # Arguments + /// + /// * `&self` - Read access to the checkpoint's state. + /// * `key` - Checkpoint key to lookup. + /// * `low` - Inclusive index where search begins. + /// * `high` - Exclusive index where search ends. + fn _lower_binary_lookup( + &self, + key: U96, + mut low: U256, + mut high: U256, + ) -> U256 { + while low < high { + let mid = low.average(high); + if self._index(mid)._key.get() < key { + low = mid + uint!(1_U256); + } else { + high = mid; + } + } + high + } + + /// Immutable access on an element of the checkpoint's array. The position + /// is assumed to be within bounds. + /// + /// # Panics + /// + /// If `pos` exceeds [`Self::length`]. + /// + /// # Arguments + /// + /// * `&self` - Read access to the checkpoint's state. + /// * `pos` - Index of the checkpoint. + fn _index(&self, pos: U256) -> StorageGuard { + self._checkpoints + .get(pos) + .unwrap_or_else(|| panic!("should get checkpoint at index `{pos}`")) + } + + /// Mutable access on an element of the checkpoint's array. The position is + /// assumed to be within bounds. + /// + /// # Panics + /// + /// If `pos` exceeds [`Self::length`]. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the checkpoint's state. + /// * `pos` - Index of the checkpoint. + fn _index_mut(&mut self, pos: U256) -> StorageGuardMut { + self._checkpoints + .setter(pos) + .unwrap_or_else(|| panic!("should get checkpoint at index `{pos}`")) + } + + /// Append a checkpoint without checking if the sorted order is kept. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the checkpoint's state. + /// * `key` - Checkpoint key to insert. + /// * `value` - Checkpoint value corresponding to insertion `key`. + fn _unchecked_push(&mut self, key: U96, value: U160) { + let mut new_checkpoint = self._checkpoints.grow(); + new_checkpoint._key.set(key); + new_checkpoint._value.set(value); + } +} + +#[cfg(all(test, feature = "std"))] +mod tests { + use alloy_primitives::uint; + + use crate::utils::structs::checkpoints::{ + CheckpointUnorderedInsertion, Error, Trace160, + }; + + #[motsu::test] + fn push(checkpoint: Trace160) { + let first_key = uint!(1_U96); + let first_value = uint!(11_U160); + + let second_key = uint!(2_U96); + let second_value = uint!(22_U160); + + let third_key = uint!(3_U96); + let third_value = uint!(33_U160); + + checkpoint.push(first_key, first_value).expect("push first"); + checkpoint.push(second_key, second_value).expect("push second"); + checkpoint.push(third_key, third_value).expect("push third"); + + assert_eq!(checkpoint.length(), uint!(3_U256)); + + assert_eq!(checkpoint.at(uint!(0_U32)), (first_key, first_value)); + assert_eq!(checkpoint.at(uint!(1_U32)), (second_key, second_value)); + assert_eq!(checkpoint.at(uint!(2_U32)), (third_key, third_value)); + } + + #[motsu::test] + fn push_same_value(checkpoint: Trace160) { + let first_key = uint!(1_U96); + let first_value = uint!(11_U160); + + let second_key = uint!(2_U96); + let second_value = uint!(22_U160); + + let third_key = uint!(2_U96); + let third_value = uint!(222_U160); + + checkpoint.push(first_key, first_value).expect("push first"); + checkpoint.push(second_key, second_value).expect("push second"); + checkpoint.push(third_key, third_value).expect("push third"); + + assert_eq!( + checkpoint.length(), + uint!(2_U256), + "two checkpoints should be stored since third_value overrides second_value" + ); + + assert_eq!(checkpoint.at(uint!(0_U32)), (first_key, first_value)); + assert_eq!(checkpoint.at(uint!(1_U32)), (third_key, third_value)); + } + + #[motsu::test] + fn lower_lookup(checkpoint: Trace160) { + checkpoint.push(uint!(1_U96), uint!(11_U160)).expect("push first"); + checkpoint.push(uint!(3_U96), uint!(33_U160)).expect("push second"); + checkpoint.push(uint!(5_U96), uint!(55_U160)).expect("push third"); + + assert_eq!(checkpoint.lower_lookup(uint!(2_U96)), uint!(33_U160)); + assert_eq!(checkpoint.lower_lookup(uint!(3_U96)), uint!(33_U160)); + assert_eq!(checkpoint.lower_lookup(uint!(4_U96)), uint!(55_U160)); + assert_eq!(checkpoint.lower_lookup(uint!(6_U96)), uint!(0_U160)); + } + + #[motsu::test] + fn upper_lookup(checkpoint: Trace160) { + checkpoint.push(uint!(1_U96), uint!(11_U160)).expect("push first"); + checkpoint.push(uint!(3_U96), uint!(33_U160)).expect("push second"); + checkpoint.push(uint!(5_U96), uint!(55_U160)).expect("push third"); + + assert_eq!(checkpoint.upper_lookup(uint!(2_U96)), uint!(11_U160)); + assert_eq!(checkpoint.upper_lookup(uint!(1_U96)), uint!(11_U160)); + assert_eq!(checkpoint.upper_lookup(uint!(4_U96)), uint!(33_U160)); + assert_eq!(checkpoint.upper_lookup(uint!(0_U96)), uint!(0_U160)); + } + + #[motsu::test] + fn upper_lookup_recent(checkpoint: Trace160) { + // `upper_lookup_recent` has different optimizations for "short" (<=5) + // and "long" (>5) checkpoint arrays. + // + // Validate the first approach for a short checkpoint array. + checkpoint.push(uint!(1_U96), uint!(11_U160)).expect("push first"); + checkpoint.push(uint!(3_U96), uint!(33_U160)).expect("push second"); + checkpoint.push(uint!(5_U96), uint!(55_U160)).expect("push third"); + + assert_eq!( + checkpoint.upper_lookup_recent(uint!(2_U96)), + uint!(11_U160) + ); + assert_eq!( + checkpoint.upper_lookup_recent(uint!(1_U96)), + uint!(11_U160) + ); + assert_eq!( + checkpoint.upper_lookup_recent(uint!(4_U96)), + uint!(33_U160) + ); + + // Validate the second approach for a long checkpoint array. + checkpoint.push(uint!(7_U96), uint!(77_U160)).expect("push fourth"); + checkpoint.push(uint!(9_U96), uint!(99_U160)).expect("push fifth"); + checkpoint.push(uint!(11_U96), uint!(111_U160)).expect("push sixth"); + + assert_eq!( + checkpoint.upper_lookup_recent(uint!(7_U96)), + uint!(77_U160) + ); + assert_eq!( + checkpoint.upper_lookup_recent(uint!(9_U96)), + uint!(99_U160) + ); + assert_eq!( + checkpoint.upper_lookup_recent(uint!(11_U96)), + uint!(111_U160) + ); + + assert_eq!(checkpoint.upper_lookup_recent(uint!(0_U96)), uint!(0_U160)); + } + + #[motsu::test] + fn latest(checkpoint: Trace160) { + assert_eq!(checkpoint.latest(), uint!(0_U160)); + checkpoint.push(uint!(1_U96), uint!(11_U160)).expect("push first"); + checkpoint.push(uint!(3_U96), uint!(33_U160)).expect("push second"); + checkpoint.push(uint!(5_U96), uint!(55_U160)).expect("push third"); + assert_eq!(checkpoint.latest(), uint!(55_U160)); + } + + #[motsu::test] + fn latest_checkpoint(checkpoint: Trace160) { + assert_eq!(checkpoint.latest_checkpoint(), None); + checkpoint.push(uint!(1_U96), uint!(11_U160)).expect("push first"); + checkpoint.push(uint!(3_U96), uint!(33_U160)).expect("push second"); + checkpoint.push(uint!(5_U96), uint!(55_U160)).expect("push third"); + assert_eq!( + checkpoint.latest_checkpoint(), + Some((uint!(5_U96), uint!(55_U160))) + ); + } + + #[motsu::test] + fn error_when_unordered_insertion(checkpoint: Trace160) { + checkpoint.push(uint!(1_U96), uint!(11_U160)).expect("push first"); + checkpoint.push(uint!(3_U96), uint!(33_U160)).expect("push second"); + let err = checkpoint + .push(uint!(2_U96), uint!(22_U160)) + .expect_err("should not push value lower then last one"); + assert!(matches!( + err, + Error::CheckpointUnorderedInsertion( + CheckpointUnorderedInsertion {} + ) + )); + } +} diff --git a/contracts/src/utils/structs/mod.rs b/contracts/src/utils/structs/mod.rs index f5510ad5..a7daf8ae 100644 --- a/contracts/src/utils/structs/mod.rs +++ b/contracts/src/utils/structs/mod.rs @@ -1,2 +1,3 @@ //! Solidity storage types used by other contracts. pub mod bitmap; +pub mod checkpoints; From 997b3b76c5407b5af49562e1e9efce5f2b0c6375 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Sat, 6 Jul 2024 01:10:22 +0400 Subject: [PATCH 37/95] ++ --- contracts/src/token/erc721/extensions/consecutive.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index f9580753..8cfe222c 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -137,7 +137,7 @@ impl Erc721Consecutive { } /// Override version that restricts normal minting to after construction. - /// + /// /// WARNING: Using [`Erc721Consecutive`] prevents minting during /// construction in favor of [`Erc721Consecutive::mint_consecutive`]. /// After construction,[`Erc721Consecutive::mint_consecutive`] is no From 7de19f2111dbb698bfb7a7dd3402765aa266f05f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jul 2024 07:22:42 +0200 Subject: [PATCH 38/95] build(deps): bump crate-ci/typos from 1.23.0 to 1.23.1 (#188) Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.23.0 to 1.23.1. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 69f2b2f7..0e6d71b9 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -110,4 +110,4 @@ jobs: - name: Checkout Actions Repository uses: actions/checkout@v4 - name: Check spelling of files in the workspace - uses: crate-ci/typos@v1.23.0 + uses: crate-ci/typos@v1.23.1 From 2c46b83748f892175efc7d945d0e5f287de68da9 Mon Sep 17 00:00:00 2001 From: alexfertel Date: Mon, 8 Jul 2024 12:52:26 +0200 Subject: [PATCH 39/95] feat: bench AccessControl gas usage (#186) Resolves #173 --- Cargo.lock | 2 + benches/Cargo.toml | 2 + benches/src/access_control.rs | 133 +++++++++++++++++++++++++++++ benches/src/erc20.rs | 43 +--------- benches/src/lib.rs | 42 +++++++++ benches/src/main.rs | 4 +- examples/access-control/src/lib.rs | 1 + 7 files changed, 185 insertions(+), 42 deletions(-) create mode 100644 benches/src/access_control.rs diff --git a/Cargo.lock b/Cargo.lock index e5574bde..a8b6559e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -867,7 +867,9 @@ dependencies = [ "alloy-primitives 0.3.3", "e2e", "eyre", + "keccak-const", "koba", + "openzeppelin-stylus", "serde", "tokio", ] diff --git a/benches/Cargo.toml b/benches/Cargo.toml index cfcdd845..c6b5c745 100644 --- a/benches/Cargo.toml +++ b/benches/Cargo.toml @@ -7,6 +7,7 @@ publish = false version = "0.0.0" [dependencies] +openzeppelin-stylus = { path = "../contracts" } alloy-primitives.workspace = true alloy.workspace = true tokio.workspace = true @@ -14,3 +15,4 @@ eyre.workspace = true koba.workspace = true e2e = { path = "../lib/e2e" } serde = "1.0.203" +keccak-const = "0.2.0" diff --git a/benches/src/access_control.rs b/benches/src/access_control.rs new file mode 100644 index 00000000..b51be753 --- /dev/null +++ b/benches/src/access_control.rs @@ -0,0 +1,133 @@ +use alloy::{ + hex, + network::{AnyNetwork, EthereumWallet}, + primitives::Address, + providers::ProviderBuilder, + sol, + sol_types::SolConstructor, +}; +use e2e::{receipt, Account}; + +use crate::ArbOtherFields; + +sol!( + #[sol(rpc)] + contract AccessControl { + constructor(); + + function hasRole(bytes32 role, address account) public view virtual returns (bool hasRole); + function getRoleAdmin(bytes32 role) public view virtual returns (bytes32 role); + function grantRole(bytes32 role, address account) public virtual; + function revokeRole(bytes32 role, address account) public virtual; + function renounceRole(bytes32 role, address callerConfirmation) public virtual; + function setRoleAdmin(bytes32 role, bytes32 adminRole) public virtual; + } +); + +const DEFAULT_ADMIN_ROLE: [u8; 32] = + openzeppelin_stylus::access::control::AccessControl::DEFAULT_ADMIN_ROLE; +// There's no way to query constants of a Stylus contract, so this one is +// hard-coded :( +const ROLE: [u8; 32] = + keccak_const::Keccak256::new().update(b"TRANSFER_ROLE").finalize(); +const NEW_ADMIN_ROLE: [u8; 32] = + hex!("879ce0d4bfd332649ca3552efe772a38d64a315eb70ab69689fd309c735946b5"); + +pub async fn bench() -> eyre::Result<()> { + let alice = Account::new().await?; + let alice_addr = alice.address(); + let alice_wallet = ProviderBuilder::new() + .network::() + .with_recommended_fillers() + .wallet(EthereumWallet::from(alice.signer.clone())) + .on_http(alice.url().parse()?); + + let bob = Account::new().await?; + let bob_addr = bob.address(); + let bob_wallet = ProviderBuilder::new() + .network::() + .with_recommended_fillers() + .wallet(EthereumWallet::from(bob.signer.clone())) + .on_http(bob.url().parse()?); + + let contract_addr = deploy(&alice).await; + let contract = AccessControl::new(contract_addr, &alice_wallet); + let contract_bob = AccessControl::new(contract_addr, &bob_wallet); + + // IMPORTANT: Order matters! + let receipts = vec![ + ( + "hasRole(DEFAULT_ADMIN_ROLE, alice)", + receipt!(contract.hasRole(DEFAULT_ADMIN_ROLE.into(), alice_addr))?, + ), + ("getRoleAdmin(ROLE)", receipt!(contract.getRoleAdmin(ROLE.into()))?), + ( + "revokeRole(ROLE, alice)", + receipt!(contract.revokeRole(ROLE.into(), alice_addr))?, + ), + ( + "grantRole(ROLE, bob)", + receipt!(contract.grantRole(ROLE.into(), bob_addr))?, + ), + ( + "renounceRole(ROLE, bob)", + receipt!(contract_bob.renounceRole(ROLE.into(), bob_addr))?, + ), + ( + "setRoleAdmin(ROLE, NEW_ADMIN_ROLE)", + receipt!(contract.setRoleAdmin(ROLE.into(), NEW_ADMIN_ROLE.into()))?, + ), + ]; + + // Calculate the width of the longest function name. + let max_name_width = receipts + .iter() + .max_by_key(|x| x.0.len()) + .expect("should at least bench one function") + .0 + .len(); + let name_width = max_name_width.max("AccessControl".len()); + + // Calculate the total width of the table. + let total_width = name_width + 3 + 6 + 3 + 6 + 3 + 20 + 4; // 3 for padding, 4 for outer borders + + // Print the table header. + println!("+{}+", "-".repeat(total_width - 2)); + println!( + "| {:(); + let effective_gas = l2_gas - l1_gas; + + println!( + "| {:6} | {:>6} | {:>20} |", + func_name, + l2_gas, + l1_gas, + effective_gas, + width = name_width + ); + } + + // Print the table footer. + println!("+{}+", "-".repeat(total_width - 2)); + + Ok(()) +} + +async fn deploy(account: &Account) -> Address { + let args = AccessControl::constructorCall {}; + let args = alloy::hex::encode(args.abi_encode()); + crate::deploy(account, "access-control", &args).await +} diff --git a/benches/src/erc20.rs b/benches/src/erc20.rs index 419bf943..9a05dbbe 100644 --- a/benches/src/erc20.rs +++ b/benches/src/erc20.rs @@ -8,12 +8,9 @@ use alloy::{ }; use alloy_primitives::U256; use e2e::{receipt, Account}; -use koba::config::Deploy; use crate::ArbOtherFields; -const RPC_URL: &str = "http://localhost:8547"; - sol!( #[sol(rpc)] contract Erc20 { @@ -63,7 +60,6 @@ pub async fn bench() -> eyre::Result<()> { let contract = Erc20::new(contract_addr, &alice_wallet); let contract_bob = Erc20::new(contract_addr, &bob_wallet); - println!("Running benches..."); // IMPORTANT: Order matters! let receipts = vec![ ("name()", receipt!(contract.name())?), @@ -110,7 +106,7 @@ pub async fn bench() -> eyre::Result<()> { .expect("should at least bench one function") .0 .len(); - let name_width = max_name_width.max("Function".len()); + let name_width = max_name_width.max("ERC-20".len()); // Calculate the total width of the table. let total_width = name_width + 3 + 6 + 3 + 6 + 3 + 20 + 4; // 3 for padding, 4 for outer borders @@ -119,7 +115,7 @@ pub async fn bench() -> eyre::Result<()> { println!("+{}+", "-".repeat(total_width - 2)); println!( "| {: Address { cap_: CAP, }; let args = alloy::hex::encode(args.abi_encode()); - - let manifest_dir = - std::env::current_dir().expect("should get current dir from env"); - - let wasm_path = manifest_dir - .join("target") - .join("wasm32-unknown-unknown") - .join("release") - .join("erc20_example.wasm"); - let sol_path = manifest_dir - .join("examples") - .join("erc20") - .join("src") - .join("constructor.sol"); - - let pk = account.pk(); - let config = Deploy { - generate_config: koba::config::Generate { - wasm: wasm_path.clone(), - sol: sol_path, - args: Some(args), - legacy: false, - }, - auth: koba::config::PrivateKey { - private_key_path: None, - private_key: Some(pk), - keystore_path: None, - keystore_password_path: None, - }, - endpoint: RPC_URL.to_owned(), - deploy_only: false, - }; - - koba::deploy(&config).await.expect("should deploy contract") + crate::deploy(account, "erc20", &args).await } diff --git a/benches/src/lib.rs b/benches/src/lib.rs index f7f9f6de..e12633a2 100644 --- a/benches/src/lib.rs +++ b/benches/src/lib.rs @@ -1,8 +1,14 @@ +use alloy::primitives::Address; use alloy_primitives::U128; +use e2e::Account; +use koba::config::{Deploy, Generate, PrivateKey}; use serde::Deserialize; +pub mod access_control; pub mod erc20; +const RPC_URL: &str = "http://localhost:8547"; + #[derive(Debug, Deserialize)] struct ArbOtherFields { #[serde(rename = "gasUsedForL1")] @@ -11,3 +17,39 @@ struct ArbOtherFields { #[serde(rename = "l1BlockNumber")] l1_block_number: String, } + +async fn deploy(account: &Account, contract_name: &str, args: &str) -> Address { + let manifest_dir = + std::env::current_dir().expect("should get current dir from env"); + + let wasm_path = manifest_dir + .join("target") + .join("wasm32-unknown-unknown") + .join("release") + .join(format!("{}_example.wasm", contract_name.replace('-', "_"))); + let sol_path = manifest_dir + .join("examples") + .join(format!("{}", contract_name)) + .join("src") + .join("constructor.sol"); + + let pk = account.pk(); + let config = Deploy { + generate_config: Generate { + wasm: wasm_path.clone(), + sol: sol_path, + args: Some(args.to_owned()), + legacy: false, + }, + auth: PrivateKey { + private_key_path: None, + private_key: Some(pk), + keystore_path: None, + keystore_password_path: None, + }, + endpoint: RPC_URL.to_owned(), + deploy_only: false, + }; + + koba::deploy(&config).await.expect("should deploy contract") +} diff --git a/benches/src/main.rs b/benches/src/main.rs index 23d75a83..c80ed4fe 100644 --- a/benches/src/main.rs +++ b/benches/src/main.rs @@ -1,8 +1,8 @@ -use benches::erc20; +use benches::{access_control, erc20}; #[tokio::main] async fn main() -> eyre::Result<()> { - erc20::bench().await?; + let _ = tokio::join!(erc20::bench(), access_control::bench()); Ok(()) } diff --git a/examples/access-control/src/lib.rs b/examples/access-control/src/lib.rs index 43078d1c..2e9bcfb0 100644 --- a/examples/access-control/src/lib.rs +++ b/examples/access-control/src/lib.rs @@ -20,6 +20,7 @@ sol_storage! { } } +// `keccak256("TRANSFER_ROLE")` pub const TRANSFER_ROLE: [u8; 32] = [ 133, 2, 35, 48, 150, 217, 9, 190, 251, 218, 9, 153, 187, 142, 162, 243, 166, 190, 60, 19, 139, 159, 191, 0, 55, 82, 164, 200, 188, 232, 111, 108, From 62bcbe91dc9a7dcfa3d2f144070412ecaa245e30 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Tue, 9 Jul 2024 14:43:17 +0400 Subject: [PATCH 40/95] add error mapping --- .../token/erc721/extensions/consecutive.rs | 181 +++++++++++++----- .../src/token/erc721/extensions/enumerable.rs | 10 +- contracts/src/token/erc721/mod.rs | 23 ++- 3 files changed, 155 insertions(+), 59 deletions(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index 8cfe222c..ddc08c01 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -1,20 +1,24 @@ use alloy_primitives::{uint, Address, U128, U256}; use alloy_sol_types::sol; -use stylus_proc::{external, sol_storage}; +use stylus_proc::{external, sol_storage, SolidityError}; use stylus_sdk::{ abi::Bytes, call::MethodError, evm, msg, prelude::TopLevelStorage, }; use crate::{ - token::erc721::{ - Approval, ERC721IncorrectOwner, ERC721InvalidApprover, - ERC721InvalidReceiver, ERC721InvalidSender, ERC721NonexistentToken, - Erc721, Error, IERC721Receiver, IErc721, Transfer, + token::{ + erc721, + erc721::{ + Approval, ERC721IncorrectOwner, ERC721InvalidApprover, + ERC721InvalidReceiver, ERC721InvalidSender, ERC721NonexistentToken, + Erc721, Error as Erc721Error, IERC721Receiver, IErc721, Transfer, + }, }, utils::{ math::storage::{AddAssignUnchecked, SubAssignUnchecked}, structs::{ bitmap::BitMap, + checkpoints, checkpoints::{Trace160, U160, U96}, }, }, @@ -25,8 +29,8 @@ sol_storage! { #[cfg_attr(all(test, feature = "std"), derive(motsu::DefaultStorageLayout))] pub struct Erc721Consecutive { Erc721 erc721; - Trace160 sequential_ownership; - BitMap sequentian_burn; + Trace160 _sequential_ownership; + BitMap _sequentian_burn; } } @@ -42,7 +46,7 @@ sol! { sol! { /// Batch mint is restricted to the constructor. - /// Any batch mint not emitting the {IERC721-Transfer} event outside of the constructor + /// Any batch mint not emitting the [`IERC721::Transfer`] event outside of the constructor /// is non ERC-721 compliant. #[derive(Debug)] #[allow(missing_docs)] @@ -64,6 +68,34 @@ sol! { error ERC721ForbiddenBatchBurn(); } +#[derive(SolidityError, Debug)] +pub enum Error { + Erc721(erc721::Error), + Checkpoints(checkpoints::Error), + /// Batch mint is restricted to the constructor. + /// Any batch mint not emitting the [`IERC721::Transfer`] event outside of + /// the constructor is non ERC-721 compliant. + Erc721ForbiddenBatchMint(ERC721ForbiddenBatchMint), + /// Exceeds the max amount of mints per batch. + Erc721ExceededMaxBatchMint(ERC721ExceededMaxBatchMint), + /// Individual minting is not allowed. + Erc721ForbiddenMint(ERC721ForbiddenMint), + /// Batch burn is not supported. + Erc721ForbiddenBatchBurn(ERC721ForbiddenBatchBurn), +} + +impl MethodError for erc721::Error { + fn encode(self) -> Vec { + self.into() + } +} + +impl MethodError for checkpoints::Error { + fn encode(self) -> Vec { + self.into() + } +} + // 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. @@ -77,7 +109,24 @@ impl Erc721Consecutive { /// Override that checks the sequential ownership structure for tokens that /// have been minted as part of a batch, and not yet transferred. pub fn _owner_of_inner(&self, token_id: U256) -> Address { - self.__owner_of_inner(token_id) + let owner = self.__owner_of_inner(token_id); + // If token is owned by the core, or beyond consecutive range, return + // base value + if owner != Address::ZERO + || token_id > U256::from(U96::MAX) + || token_id < U256::from(FIRST_CONSECUTIVE_ID) + { + return owner; + } + + // Otherwise, check the token was not burned, and fetch ownership from + // the anchors. + // NOTE: no need for safe cast, + if self._sequentian_burn.get(token_id) { + Address::ZERO + } else { + self._sequential_ownership.lower_lookup(U96::from(token_id)).into() + } } // Mint a batch of tokens of length `batchSize` for `to`. Returns the token @@ -100,16 +149,19 @@ impl Erc721Consecutive { pub fn mint_consecutive( &mut self, to: Address, - batch_size: u128, // TODO: how to use U96 type - ) -> Result> { + batch_size: u128, + ) -> Result { let batch_size = U96::from(batch_size); let next = self.next_consecutive_id(); if batch_size > U96::ZERO { + //TODO#q: check address of this and revert with ERC721ForbiddenBatchMint + if to.is_zero() { - return Err( - ERC721InvalidReceiver { receiver: Address::ZERO }.encode() - ); + return Err(Erc721Error::InvalidReceiver( + ERC721InvalidReceiver { receiver: Address::ZERO }, + ) + .into()); } if batch_size > MAX_BATCH_SIZE.to() { @@ -117,13 +169,12 @@ impl Erc721Consecutive { batchSize: U256::from(batch_size), maxBatch: U256::from(MAX_BATCH_SIZE), } - .encode()); + .into()); } let last = next + batch_size - uint!(1_U96); - self.sequential_ownership - .push(last, U160::from_be_bytes(to.into_array())) - .map_err(Vec::::from)?; + self._sequential_ownership + .push(last, U160::from_be_bytes(to.into_array()))?; self.erc721._increase_balance(to, U128::from(batch_size)); evm::log(ConsecutiveTransfer { @@ -149,14 +200,32 @@ impl Erc721Consecutive { token_id: U256, auth: Address, ) -> Result { - self.__update(to, token_id, auth) + let previous_owner = self.__update(to, token_id, auth)?; + + // only mint after construction + if previous_owner == Address::ZERO + /* TODO#q: and address code is zero */ + { + return Err(ERC721ForbiddenMint {}.into()); // + } + + // record burn + if to == Address::ZERO // if we burn + && token_id < U256::from(self.next_consecutive_id()) // and the tokenId was minted in a batch + && !self._sequentian_burn.get(token_id) + // and the token was never marked as burnt + { + self._sequentian_burn.set(token_id); + } + + Ok(previous_owner) } /// Returns the next tokenId to mint using {_mintConsecutive}. It will /// return [`FIRST_CONSECUTIVE_ID`] if no consecutive tokenId has been /// minted before. fn next_consecutive_id(&self) -> U96 { - match self.sequential_ownership.latest_checkpoint() { + match self._sequential_ownership.latest_checkpoint() { None => FIRST_CONSECUTIVE_ID, Some((latest_id, _)) => latest_id + uint!(1_U96), } @@ -167,8 +236,10 @@ unsafe impl TopLevelStorage for Erc721Consecutive {} #[external] impl IErc721 for Erc721Consecutive { + type Error = Error; + fn balance_of(&self, owner: Address) -> Result { - self.erc721.balance_of(owner) + self.erc721.balance_of(owner).map_err(|e| e.into()) } fn owner_of(&self, token_id: U256) -> Result { @@ -195,13 +266,13 @@ impl IErc721 for Erc721Consecutive { data: Bytes, ) -> Result<(), Error> { self.transfer_from(from, to, token_id)?; - self.erc721._check_on_erc721_received( + Ok(self.erc721._check_on_erc721_received( msg::sender(), from, to, token_id, &data, - ) + )?) } fn transfer_from( @@ -211,9 +282,10 @@ impl IErc721 for Erc721Consecutive { token_id: U256, ) -> Result<(), Error> { if to.is_zero() { - return Err( - ERC721InvalidReceiver { receiver: Address::ZERO }.into() - ); + return Err(Erc721Error::InvalidReceiver(ERC721InvalidReceiver { + receiver: Address::ZERO, + }) + .into()); } // Setting an "auth" argument enables the `_is_authorized` check which @@ -221,11 +293,11 @@ impl IErc721 for Erc721Consecutive { // 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(ERC721IncorrectOwner { + return Err(Erc721Error::IncorrectOwner(ERC721IncorrectOwner { sender: from, token_id, owner: previous_owner, - } + }) .into()); } Ok(()) @@ -240,7 +312,7 @@ impl IErc721 for Erc721Consecutive { operator: Address, approved: bool, ) -> Result<(), Error> { - self.erc721.set_approval_for_all(operator, approved) + Ok(self.erc721.set_approval_for_all(operator, approved)?) } fn get_approved(&self, token_id: U256) -> Result { @@ -367,14 +439,18 @@ impl Erc721Consecutive { /// Emits a [`Transfer`] event. pub fn _mint(&mut self, to: Address, token_id: U256) -> Result<(), Error> { if to.is_zero() { - return Err( - ERC721InvalidReceiver { receiver: Address::ZERO }.into() - ); + return Err(Erc721Error::InvalidReceiver(ERC721InvalidReceiver { + receiver: Address::ZERO, + }) + .into()); } let previous_owner = self._update(to, token_id, Address::ZERO)?; if !previous_owner.is_zero() { - return Err(ERC721InvalidSender { sender: Address::ZERO }.into()); + return Err(Erc721Error::InvalidSender(ERC721InvalidSender { + sender: Address::ZERO, + }) + .into()); } Ok(()) } @@ -420,13 +496,13 @@ impl Erc721Consecutive { data: Bytes, ) -> Result<(), Error> { self._mint(to, token_id)?; - self.erc721._check_on_erc721_received( + Ok(self.erc721._check_on_erc721_received( msg::sender(), Address::ZERO, to, token_id, &data, - ) + )?) } /// Destroys `token_id`. @@ -456,7 +532,10 @@ impl Erc721Consecutive { let previous_owner = self._update(Address::ZERO, token_id, Address::ZERO)?; if previous_owner.is_zero() { - return Err(ERC721NonexistentToken { token_id }.into()); + return Err(Erc721Error::NonexistentToken( + ERC721NonexistentToken { token_id }, + ) + .into()); } Ok(()) } @@ -497,20 +576,24 @@ impl Erc721Consecutive { token_id: U256, ) -> Result<(), Error> { if to.is_zero() { - return Err( - ERC721InvalidReceiver { receiver: Address::ZERO }.into() - ); + return Err(Erc721Error::InvalidReceiver(ERC721InvalidReceiver { + receiver: Address::ZERO, + }) + .into()); } let previous_owner = self._update(to, token_id, Address::ZERO)?; if previous_owner.is_zero() { - return Err(ERC721NonexistentToken { token_id }.into()); + return Err(Erc721Error::NonexistentToken( + ERC721NonexistentToken { token_id }, + ) + .into()); } else if previous_owner != from { - return Err(ERC721IncorrectOwner { + return Err(Erc721Error::IncorrectOwner(ERC721IncorrectOwner { sender: from, token_id, owner: previous_owner, - } + }) .into()); } @@ -566,13 +649,13 @@ impl Erc721Consecutive { data: Bytes, ) -> Result<(), Error> { self._transfer(from, to, token_id)?; - self.erc721._check_on_erc721_received( + Ok(self.erc721._check_on_erc721_received( msg::sender(), from, to, token_id, &data, - ) + )?) } /// Variant of `approve_inner` with an optional flag to enable or disable @@ -614,7 +697,10 @@ impl Erc721Consecutive { && owner != auth && !self.is_approved_for_all(owner, auth) { - return Err(ERC721InvalidApprover { approver: auth }.into()); + return Err(Erc721Error::InvalidApprover( + ERC721InvalidApprover { approver: auth }, + ) + .into()); } if emit_event { @@ -644,7 +730,10 @@ impl Erc721Consecutive { pub fn _require_owned(&self, token_id: U256) -> Result { let owner = self._owner_of_inner(token_id); if owner.is_zero() { - return Err(ERC721NonexistentToken { token_id }.into()); + return Err(Erc721Error::NonexistentToken( + ERC721NonexistentToken { token_id }, + ) + .into()); } Ok(owner) } diff --git a/contracts/src/token/erc721/extensions/enumerable.rs b/contracts/src/token/erc721/extensions/enumerable.rs index 2761752a..9e2b5bad 100644 --- a/contracts/src/token/erc721/extensions/enumerable.rs +++ b/contracts/src/token/erc721/extensions/enumerable.rs @@ -10,7 +10,7 @@ use alloy_primitives::{uint, Address, U256}; use alloy_sol_types::sol; use stylus_proc::{external, sol_storage, SolidityError}; - +use crate::token::erc721; use crate::token::erc721::IErc721; sol! { @@ -155,8 +155,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); @@ -206,8 +206,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/mod.rs b/contracts/src/token/erc721/mod.rs index b0037ca9..764bc112 100644 --- a/contracts/src/token/erc721/mod.rs +++ b/contracts/src/token/erc721/mod.rs @@ -187,6 +187,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 @@ -198,7 +199,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. /// @@ -215,7 +216,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 @@ -261,7 +262,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`. /// @@ -308,7 +309,7 @@ pub trait IErc721 { to: Address, token_id: U256, data: Bytes, - ) -> Result<(), Error>; + ) -> Result<(), Self::Error>; /// Transfers `token_id` token from `from` to `to`. /// @@ -352,7 +353,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. @@ -382,7 +383,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. /// @@ -413,7 +418,7 @@ pub trait IErc721 { &mut self, operator: Address, approved: bool, - ) -> Result<(), Error>; + ) -> Result<(), Self::Error>; /// Returns the account approved for `token_id` token. /// @@ -430,7 +435,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`. @@ -445,6 +450,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()); From 3bea0331f17eedb4957a9d7eff88f9443da91c21 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Tue, 9 Jul 2024 15:09:12 +0400 Subject: [PATCH 41/95] ++ --- examples/erc721-consecutive/src/lib.rs | 119 ++----------------------- 1 file changed, 9 insertions(+), 110 deletions(-) diff --git a/examples/erc721-consecutive/src/lib.rs b/examples/erc721-consecutive/src/lib.rs index e800575c..448f8cc5 100644 --- a/examples/erc721-consecutive/src/lib.rs +++ b/examples/erc721-consecutive/src/lib.rs @@ -3,87 +3,28 @@ extern crate alloc; use alloc::vec::Vec; -use alloy_primitives::{uint, Address, U128, U256}; +use alloy_primitives::{Address, U256}; use alloy_sol_types::SolError; -use openzeppelin_stylus::{ - token::erc721::{ - extensions::IErc721Burnable, ERC721InvalidReceiver, Erc721, - }, - utils::structs::{ - bitmap::BitMap, - checkpoints::{Trace160, U160, U96}, - }, +use openzeppelin_stylus::token::erc721::extensions::{ + consecutive::Erc721Consecutive, IErc721Burnable, }; -use stylus_sdk::{alloy_sol_types::sol, evm, prelude::*}; +use stylus_sdk::prelude::*; sol_storage! { #[entrypoint] struct Erc721ConsecutiveExample { #[borrow] - Erc721 erc721; - Trace160 sequential_ownership; - BitMap sequentian_burn; + Erc721Consecutive erc721_consecutive; } } -sol! { - /// Emitted when the tokens from `fromTokenId` to `toTokenId` are transferred from `fromAddress` to `toAddress`. - event ConsecutiveTransfer( - uint256 indexed fromTokenId, - uint256 toTokenId, - address indexed fromAddress, - address indexed toAddress - ); -} - -sol! { - /// Batch mint is restricted to the constructor. - /// Any batch mint not emitting the {IERC721-Transfer} event outside of the constructor - /// is non ERC-721 compliant. - #[derive(Debug)] - #[allow(missing_docs)] - error ERC721ForbiddenBatchMint(); - - /// Exceeds the max amount of mints per batch. - #[derive(Debug)] - #[allow(missing_docs)] - error ERC721ExceededMaxBatchMint(uint256 batchSize, uint256 maxBatch); - - /// Individual minting is not allowed. - #[derive(Debug)] - #[allow(missing_docs)] - error ERC721ForbiddenMint(); - - /// Batch burn is not supported. - #[derive(Debug)] - #[allow(missing_docs)] - error ERC721ForbiddenBatchBurn(); -} - -// 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. -const MAX_BATCH_SIZE: U96 = uint!(5000_U96); - -// Used to offset the first token id in {_nextConsecutiveId} -const FIRST_CONSECUTIVE_ID: U96 = uint!(0_U96); - #[external] -#[inherit(Erc721)] +#[inherit(Erc721Consecutive)] impl Erc721ConsecutiveExample { // Burn token with `token_id` and record it at [`Self::sequentian_burn`] // storage. pub fn burn(&mut self, token_id: U256) -> Result<(), Vec> { - self.erc721.burn(token_id)?; - - // record burn - if token_id < self.next_consecutive_id().to() // the tokenId was minted in a batch - && !self.sequentian_burn.get(token_id) - // and the token was never marked as burnt - { - self.sequentian_burn.set(token_id) - } - Ok(()) + self.erc721_consecutive.burn(token_id)?; } // Mint a batch of tokens of length `batchSize` for `to`. Returns the token @@ -108,49 +49,7 @@ impl Erc721ConsecutiveExample { to: Address, batch_size: u128, // TODO: how to use U96 type ) -> Result> { - let batch_size = U96::from(batch_size); - let next = self.next_consecutive_id(); - - if batch_size > U96::ZERO { - if to.is_zero() { - return Err( - ERC721InvalidReceiver { receiver: Address::ZERO }.encode() - ); - } - - if batch_size > MAX_BATCH_SIZE.to() { - return Err(ERC721ExceededMaxBatchMint { - batchSize: U256::from(batch_size), - maxBatch: U256::from(MAX_BATCH_SIZE), - } - .encode()); - } - - let last = next + batch_size - uint!(1_U96); - self.sequential_ownership - .push(last, U160::from_be_bytes(to.into_array())) - .map_err(Vec::::from)?; - - self.erc721._increase_balance(to, U128::from(batch_size)); - evm::log(ConsecutiveTransfer { - fromTokenId: next.to::(), - toTokenId: last.to::(), - fromAddress: Address::ZERO, - toAddress: to, - }); - }; - Ok(next.to()) - } -} - -impl Erc721ConsecutiveExample { - /// Returns the next tokenId to mint using {_mintConsecutive}. It will - /// return [`FIRST_CONSECUTIVE_ID`] if no consecutive tokenId has been - /// minted before. - fn next_consecutive_id(&self) -> U96 { - match self.sequential_ownership.latest_checkpoint() { - None => FIRST_CONSECUTIVE_ID, - Some((latest_id, _)) => latest_id + uint!(1_U96), - } + // TODO#q: polish initialization logic + Ok(self.erc721_consecutive.mint_consecutive(to, batch_size)?) } } From 954fa01b20e919aa167c1273c8cd44161789172c Mon Sep 17 00:00:00 2001 From: Daniel Bigos Date: Tue, 9 Jul 2024 21:13:45 +0200 Subject: [PATCH 42/95] test(erc20): add ERC-20 Pausable Extension E2E tests (#181) Resolves #176 --- examples/erc20/tests/abi.rs | 17 ++- examples/erc20/tests/erc20.rs | 267 ++++++++++++++++++++++++++++++++++ 2 files changed, 283 insertions(+), 1 deletion(-) diff --git a/examples/erc20/tests/abi.rs b/examples/erc20/tests/abi.rs index 3a80c6be..6360980a 100644 --- a/examples/erc20/tests/abi.rs +++ b/examples/erc20/tests/abi.rs @@ -20,6 +20,17 @@ sol!( function burn(uint256 amount) external; function burnFrom(address account, uint256 amount) external; + function paused() external view returns (bool paused); + function pause() external; + function unpause() external; + #[derive(Debug)] + function whenPaused() external view; + #[derive(Debug)] + function whenNotPaused() external view; + + error EnforcedPause(); + error ExpectedPause(); + error ERC20ExceededCap(uint256 increased_supply, uint256 cap); error ERC20InvalidCap(uint256 cap); @@ -31,8 +42,12 @@ sol!( #[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); + + #[derive(Debug, PartialEq)] + event Paused(address account); + #[derive(Debug, PartialEq)] + event Unpaused(address account); } ); diff --git a/examples/erc20/tests/erc20.rs b/examples/erc20/tests/erc20.rs index 4bda27a7..221022cc 100644 --- a/examples/erc20/tests/erc20.rs +++ b/examples/erc20/tests/erc20.rs @@ -950,3 +950,270 @@ async fn should_not_deploy_capped_with_invalid_cap( assert!(err_string.contains(&expected)); Ok(()) } + +// ============================================================================ +// Integration Tests: ERC-20 Pausable Extension +// ============================================================================ + +#[e2e::test] +async fn pauses(alice: Account) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk(), None).await?; + let contract = Erc20::new(contract_addr, &alice.wallet); + + let receipt = receipt!(contract.pause())?; + + assert!(receipt.emits(Erc20::Paused { account: alice.address() })); + + let Erc20::pausedReturn { paused } = contract.paused().call().await?; + + assert_eq!(true, paused); + + let result = contract.whenPaused().call().await; + + assert!(result.is_ok()); + + let err = contract + .whenNotPaused() + .call() + .await + .expect_err("should return `EnforcedPause`"); + + assert!(err.reverted_with(Erc20::EnforcedPause {})); + + Ok(()) +} + +#[e2e::test] +async fn unpauses(alice: Account) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk(), None).await?; + let contract = Erc20::new(contract_addr, &alice.wallet); + + let _ = watch!(contract.pause())?; + + let receipt = receipt!(contract.unpause())?; + + assert!(receipt.emits(Erc20::Unpaused { account: alice.address() })); + + let Erc20::pausedReturn { paused } = contract.paused().call().await?; + + assert_eq!(false, paused); + + let result = contract.whenNotPaused().call().await; + + assert!(result.is_ok()); + + let err = contract + .whenPaused() + .call() + .await + .expect_err("should return `ExpectedPause`"); + + assert!(err.reverted_with(Erc20::ExpectedPause {})); + + Ok(()) +} + +#[e2e::test] +async fn error_when_burn_in_paused_state(alice: Account) -> Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk(), None).await?; + let contract = Erc20::new(contract_addr, &alice.wallet); + let alice_addr = alice.address(); + + let balance = uint!(10_U256); + let value = uint!(1_U256); + + let _ = watch!(contract.mint(alice.address(), balance))?; + + let Erc20::balanceOfReturn { balance: initial_balance } = + contract.balanceOf(alice_addr).call().await?; + let Erc20::totalSupplyReturn { totalSupply: initial_supply } = + contract.totalSupply().call().await?; + + let _ = watch!(contract.pause())?; + + let err = + send!(contract.burn(value)).expect_err("should return EnforcedPause"); + assert!(err.reverted_with(Erc20::EnforcedPause {})); + + let Erc20::balanceOfReturn { balance } = + contract.balanceOf(alice_addr).call().await?; + let Erc20::totalSupplyReturn { totalSupply: supply } = + contract.totalSupply().call().await?; + + assert_eq!(initial_balance, balance); + assert_eq!(initial_supply, supply); + + Ok(()) +} + +#[e2e::test] +async fn error_when_burn_from_in_paused_state( + alice: Account, + bob: Account, +) -> Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk(), None).await?; + let contract_alice = Erc20::new(contract_addr, &alice.wallet); + let contract_bob = Erc20::new(contract_addr, &bob.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + + let balance = uint!(10_U256); + let value = uint!(1_U256); + + let _ = watch!(contract_alice.mint(alice.address(), balance))?; + + let Erc20::balanceOfReturn { balance: initial_alice_balance } = + contract_alice.balanceOf(alice_addr).call().await?; + let Erc20::balanceOfReturn { balance: initial_bob_balance } = + contract_alice.balanceOf(bob_addr).call().await?; + let Erc20::totalSupplyReturn { totalSupply: initial_supply } = + contract_alice.totalSupply().call().await?; + + let _ = watch!(contract_alice.approve(bob_addr, balance))?; + + let Erc20::allowanceReturn { allowance: initial_allowance } = + contract_alice.allowance(alice_addr, bob_addr).call().await?; + + let _ = watch!(contract_alice.pause())?; + + let err = send!(contract_bob.burnFrom(alice_addr, value)) + .expect_err("should return EnforcedPause"); + assert!(err.reverted_with(Erc20::EnforcedPause {})); + + let Erc20::balanceOfReturn { balance: alice_balance } = + contract_alice.balanceOf(alice_addr).call().await?; + let Erc20::balanceOfReturn { balance: bob_balance } = + contract_alice.balanceOf(bob_addr).call().await?; + let Erc20::totalSupplyReturn { totalSupply: supply } = + contract_alice.totalSupply().call().await?; + let Erc20::allowanceReturn { allowance } = + contract_alice.allowance(alice_addr, bob_addr).call().await?; + + assert_eq!(initial_alice_balance, alice_balance); + assert_eq!(initial_bob_balance, bob_balance); + assert_eq!(initial_supply, supply); + assert_eq!(initial_allowance, allowance); + + Ok(()) +} + +#[e2e::test] +async fn error_when_mint_in_paused_state(alice: Account) -> Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk(), None).await?; + let contract = Erc20::new(contract_addr, &alice.wallet); + let alice_addr = alice.address(); + + let Erc20::balanceOfReturn { balance: initial_balance } = + contract.balanceOf(alice_addr).call().await?; + let Erc20::totalSupplyReturn { totalSupply: initial_supply } = + contract.totalSupply().call().await?; + + assert_eq!(U256::ZERO, initial_balance); + assert_eq!(U256::ZERO, initial_supply); + + let _ = watch!(contract.pause())?; + + let err = send!(contract.mint(alice_addr, uint!(1_U256))) + .expect_err("should return EnforcedPause"); + assert!(err.reverted_with(Erc20::EnforcedPause {})); + + let Erc20::balanceOfReturn { balance } = + contract.balanceOf(alice_addr).call().await?; + let Erc20::totalSupplyReturn { totalSupply: total_supply } = + contract.totalSupply().call().await?; + + assert_eq!(initial_balance, balance); + assert_eq!(initial_supply, total_supply); + Ok(()) +} + +#[e2e::test] +async fn error_when_transfer_in_paused_state( + alice: Account, + bob: Account, +) -> Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk(), None).await?; + let contract_alice = Erc20::new(contract_addr, &alice.wallet); + let alice_addr = alice.address(); + let bob_addr = bob.address(); + + let balance = uint!(10_U256); + + let _ = watch!(contract_alice.mint(alice.address(), balance))?; + + let Erc20::balanceOfReturn { balance: initial_alice_balance } = + contract_alice.balanceOf(alice_addr).call().await?; + let Erc20::balanceOfReturn { balance: initial_bob_balance } = + contract_alice.balanceOf(bob_addr).call().await?; + let Erc20::totalSupplyReturn { totalSupply: initial_supply } = + contract_alice.totalSupply().call().await?; + + let _ = watch!(contract_alice.pause())?; + + let err = send!(contract_alice.transfer(bob_addr, uint!(1_U256))) + .expect_err("should return EnforcedPause"); + assert!(err.reverted_with(Erc20::EnforcedPause {})); + + let Erc20::balanceOfReturn { balance: alice_balance } = + contract_alice.balanceOf(alice_addr).call().await?; + let Erc20::balanceOfReturn { balance: bob_balance } = + contract_alice.balanceOf(bob_addr).call().await?; + let Erc20::totalSupplyReturn { totalSupply: supply } = + contract_alice.totalSupply().call().await?; + + assert_eq!(initial_alice_balance, alice_balance); + assert_eq!(initial_bob_balance, bob_balance); + assert_eq!(initial_supply, supply); + + Ok(()) +} + +#[e2e::test] +async fn error_when_transfer_from(alice: Account, bob: Account) -> Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk(), None).await?; + let contract_alice = Erc20::new(contract_addr, &alice.wallet); + let contract_bob = Erc20::new(contract_addr, &bob.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + + let balance = uint!(10_U256); + + let _ = watch!(contract_alice.mint(alice.address(), balance))?; + + let Erc20::balanceOfReturn { balance: initial_alice_balance } = + contract_alice.balanceOf(alice_addr).call().await?; + let Erc20::balanceOfReturn { balance: initial_bob_balance } = + contract_alice.balanceOf(bob_addr).call().await?; + let Erc20::totalSupplyReturn { totalSupply: initial_supply } = + contract_alice.totalSupply().call().await?; + + let _ = watch!(contract_alice.approve(bob_addr, balance))?; + + let Erc20::allowanceReturn { allowance: initial_allowance } = + contract_alice.allowance(alice_addr, bob_addr).call().await?; + + let _ = watch!(contract_alice.pause())?; + + let err = + send!(contract_bob.transferFrom(alice_addr, bob_addr, uint!(1_U256))) + .expect_err("should return EnforcedPause"); + assert!(err.reverted_with(Erc20::EnforcedPause {})); + + let Erc20::balanceOfReturn { balance: alice_balance } = + contract_alice.balanceOf(alice_addr).call().await?; + let Erc20::balanceOfReturn { balance: bob_balance } = + contract_alice.balanceOf(bob_addr).call().await?; + let Erc20::totalSupplyReturn { totalSupply: supply } = + contract_alice.totalSupply().call().await?; + let Erc20::allowanceReturn { allowance } = + contract_alice.allowance(alice_addr, bob_addr).call().await?; + + assert_eq!(initial_alice_balance, alice_balance); + assert_eq!(initial_bob_balance, bob_balance); + assert_eq!(initial_supply, supply); + assert_eq!(initial_allowance, allowance); + + Ok(()) +} From 5f3d4bb71f76b5c8e8c283b1006c74e62eeaeb44 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Wed, 10 Jul 2024 16:19:30 +0400 Subject: [PATCH 43/95] add init function --- .../token/erc721/extensions/consecutive.rs | 8 +-- examples/erc721-consecutive/src/lib.rs | 50 ++++++++----------- examples/erc721-consecutive/tests/abi.rs | 2 +- 3 files changed, 26 insertions(+), 34 deletions(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index ddc08c01..3faa3c35 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -1,3 +1,4 @@ +use alloc::vec; use alloy_primitives::{uint, Address, U128, U256}; use alloy_sol_types::sol; use stylus_proc::{external, sol_storage, SolidityError}; @@ -85,13 +86,13 @@ pub enum Error { } impl MethodError for erc721::Error { - fn encode(self) -> Vec { + fn encode(self) -> alloc::vec::Vec { self.into() } } impl MethodError for checkpoints::Error { - fn encode(self) -> Vec { + fn encode(self) -> alloc::vec::Vec { self.into() } } @@ -149,9 +150,8 @@ impl Erc721Consecutive { pub fn mint_consecutive( &mut self, to: Address, - batch_size: u128, + batch_size: U96, ) -> Result { - let batch_size = U96::from(batch_size); let next = self.next_consecutive_id(); if batch_size > U96::ZERO { diff --git a/examples/erc721-consecutive/src/lib.rs b/examples/erc721-consecutive/src/lib.rs index 448f8cc5..aa5fecca 100644 --- a/examples/erc721-consecutive/src/lib.rs +++ b/examples/erc721-consecutive/src/lib.rs @@ -5,8 +5,12 @@ use alloc::vec::Vec; use alloy_primitives::{Address, U256}; use alloy_sol_types::SolError; -use openzeppelin_stylus::token::erc721::extensions::{ - consecutive::Erc721Consecutive, IErc721Burnable, +use openzeppelin_stylus::{ + token::erc721::extensions::{ + consecutive::{Erc721Consecutive, Error}, + IErc721Burnable, + }, + utils::structs::checkpoints::U96, }; use stylus_sdk::prelude::*; @@ -21,35 +25,23 @@ sol_storage! { #[external] #[inherit(Erc721Consecutive)] impl Erc721ConsecutiveExample { - // Burn token with `token_id` and record it at [`Self::sequentian_burn`] - // storage. - pub fn burn(&mut self, token_id: U256) -> Result<(), Vec> { - self.erc721_consecutive.burn(token_id)?; + pub fn burn(&mut self, token_id: U256) -> Result<(), Error> { + self.erc721_consecutive._burn(token_id) } - // Mint a batch of tokens of length `batchSize` for `to`. Returns the token - // id of the first token minted in the batch; if `batchSize` is 0, - // returns the number of consecutive ids minted so far. - // - // Requirements: - // - // - `batchSize` must not be greater than [`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 `onERC721Received` on the receiver. - // - // Emits a [`ConsecutiveTransfer`] event. - pub fn mint_consecutive( + pub fn init( &mut self, - to: Address, - batch_size: u128, // TODO: how to use U96 type - ) -> Result> { - // TODO#q: polish initialization logic - Ok(self.erc721_consecutive.mint_consecutive(to, batch_size)?) + receivers: Vec
, + batches: Vec, + ) -> Result<(), Error> { + let len = batches.len(); + for i in 0..len { + let receiver = receivers[i]; + let batch = batches[i]; + let token_id = self + .erc721_consecutive + .mint_consecutive(receiver, U96::from(batch))?; + } + Ok(()) } } diff --git a/examples/erc721-consecutive/tests/abi.rs b/examples/erc721-consecutive/tests/abi.rs index 268ef73f..eb4d2c48 100644 --- a/examples/erc721-consecutive/tests/abi.rs +++ b/examples/erc721-consecutive/tests/abi.rs @@ -15,7 +15,7 @@ sol!( function isApprovedForAll(address owner, address operator) external view returns (bool); function burn(uint256 tokenId) external; - function mintConsecutive(address to, uint128 batchSize) external; + function init(address[] memory receivers, uint256[] memory amounts) external; error ERC721InvalidOwner(address owner); error ERC721NonexistentToken(uint256 tokenId); From 8c0512a1588f829cdc6b03e1db1a3ec431bff867 Mon Sep 17 00:00:00 2001 From: Daniel Bigos Date: Wed, 10 Jul 2024 14:59:41 +0200 Subject: [PATCH 44/95] test(erc721): integration tests for ERC721 token and its extensions (#160) Resolves #93 --- Cargo.lock | 16 + Cargo.toml | 2 + .../token/erc721/extensions/uri_storage.rs | 6 +- contracts/src/token/erc721/mod.rs | 21 +- .../tests/{abi.rs => abi/mod.rs} | 0 examples/erc20/tests/{abi.rs => abi/mod.rs} | 0 examples/erc20/tests/erc20.rs | 3 +- examples/erc721-metadata/Cargo.toml | 27 + examples/erc721-metadata/src/constructor.sol | 22 + examples/erc721-metadata/src/lib.rs | 71 + .../tests/abi/mod.rs} | 45 +- examples/erc721-metadata/tests/erc721.rs | 259 +++ examples/erc721/src/ERC721ReceiverMock.sol | 48 + examples/erc721/src/constructor.sol | 9 +- examples/erc721/src/lib.rs | 49 +- examples/erc721/tests/abi/mod.rs | 59 + examples/erc721/tests/erc721.rs | 1932 ++++++++++++++++- examples/erc721/tests/mock/mod.rs | 1 + examples/erc721/tests/mock/receiver.rs | 67 + examples/ownable/tests/{abi.rs => abi/mod.rs} | 0 examples/ownable/tests/ownable.rs | 4 +- 21 files changed, 2526 insertions(+), 115 deletions(-) rename examples/access-control/tests/{abi.rs => abi/mod.rs} (100%) rename examples/erc20/tests/{abi.rs => abi/mod.rs} (100%) create mode 100644 examples/erc721-metadata/Cargo.toml create mode 100644 examples/erc721-metadata/src/constructor.sol create mode 100644 examples/erc721-metadata/src/lib.rs rename examples/{erc721/tests/abi.rs => erc721-metadata/tests/abi/mod.rs} (81%) create mode 100644 examples/erc721-metadata/tests/erc721.rs create mode 100644 examples/erc721/src/ERC721ReceiverMock.sol create mode 100644 examples/erc721/tests/abi/mod.rs create mode 100644 examples/erc721/tests/mock/mod.rs create mode 100644 examples/erc721/tests/mock/receiver.rs rename examples/ownable/tests/{abi.rs => abi/mod.rs} (100%) diff --git a/Cargo.lock b/Cargo.lock index a8b6559e..e26344d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1583,6 +1583,22 @@ dependencies = [ "tokio", ] +[[package]] +name = "erc721-metadata-example" +version = "0.0.0" +dependencies = [ + "alloy", + "alloy-primitives 0.3.3", + "e2e", + "eyre", + "mini-alloc", + "openzeppelin-stylus", + "rand", + "stylus-proc", + "stylus-sdk", + "tokio", +] + [[package]] name = "errno" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 40ac0191..bdf039b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "lib/e2e-proc", "examples/erc20", "examples/erc721", + "examples/erc721-metadata", "examples/merkle-proofs", "examples/ownable", "examples/access-control", @@ -23,6 +24,7 @@ default-members = [ "lib/e2e-proc", "examples/erc20", "examples/erc721", + "examples/erc721-metadata", "examples/merkle-proofs", "examples/ownable", "examples/access-control", diff --git a/contracts/src/token/erc721/extensions/uri_storage.rs b/contracts/src/token/erc721/extensions/uri_storage.rs index b4822e75..255c78e8 100644 --- a/contracts/src/token/erc721/extensions/uri_storage.rs +++ b/contracts/src/token/erc721/extensions/uri_storage.rs @@ -43,7 +43,7 @@ impl Erc721UriStorage { /// /// # Events /// Emits a [`MetadataUpdate`] event. - pub fn set_token_uri(&mut self, token_id: U256, token_uri: String) { + pub fn _set_token_uri(&mut self, token_id: U256, token_uri: String) { self._token_uris.setter(token_id).set_str(token_uri); evm::log(MetadataUpdate { token_id }); } @@ -59,7 +59,7 @@ impl Erc721UriStorage { /// * `token_id` - Id of a token. #[must_use] pub fn token_uri(&self, token_id: U256) -> String { - self._token_uris.get(token_id).get_string() + self._token_uris.getter(token_id).get_string() } } @@ -92,7 +92,7 @@ mod tests { contract._token_uris.setter(token_id).set_str(initial_token_uri); let token_uri = String::from("Updated Token URI"); - contract.set_token_uri(token_id, token_uri.clone()); + contract._set_token_uri(token_id, token_uri.clone()); assert_eq!(token_uri, contract.token_uri(token_id)); } diff --git a/contracts/src/token/erc721/mod.rs b/contracts/src/token/erc721/mod.rs index b0037ca9..e2e04f3a 100644 --- a/contracts/src/token/erc721/mod.rs +++ b/contracts/src/token/erc721/mod.rs @@ -1203,6 +1203,24 @@ mod tests { )); } + #[motsu::test] + fn error_when_minting_token_invalid_receiver(contract: Erc721) { + let invalid_receiver = Address::ZERO; + + let token_id = random_token_id(); + + let err = contract + ._mint(invalid_receiver, token_id) + .expect_err("should not mint a token for invalid receiver"); + + assert!(matches!( + err, + Error::InvalidReceiver(ERC721InvalidReceiver { + receiver + }) if receiver == invalid_receiver + )); + } + #[motsu::test] fn safe_mints(contract: Erc721) { let alice = msg::sender(); @@ -1364,7 +1382,6 @@ mod tests { )); // FIXME: this check should pass - // TODO: confirm in E2E tests that owner is not changed: #93 // let owner = contract // .owner_of(token_id) // .expect("should return the owner of the token"); @@ -1508,7 +1525,6 @@ mod tests { )); // FIXME: this check should pass - // TODO: confirm in E2E tests that owner is not changed: #93 // let owner = contract // .owner_of(token_id) // .expect("should return the owner of the token"); @@ -1680,7 +1696,6 @@ mod tests { )); // FIXME: this check should pass - // TODO: confirm in E2E tests that owner is not changed: #93 // let owner = contract // .owner_of(token_id) // .expect("should return the owner of the token"); diff --git a/examples/access-control/tests/abi.rs b/examples/access-control/tests/abi/mod.rs similarity index 100% rename from examples/access-control/tests/abi.rs rename to examples/access-control/tests/abi/mod.rs diff --git a/examples/erc20/tests/abi.rs b/examples/erc20/tests/abi/mod.rs similarity index 100% rename from examples/erc20/tests/abi.rs rename to examples/erc20/tests/abi/mod.rs diff --git a/examples/erc20/tests/erc20.rs b/examples/erc20/tests/erc20.rs index 221022cc..b14c208c 100644 --- a/examples/erc20/tests/erc20.rs +++ b/examples/erc20/tests/erc20.rs @@ -1,5 +1,6 @@ #![cfg(feature = "e2e")] +use abi::Erc20; use alloy::{ primitives::{Address, U256}, sol, @@ -9,8 +10,6 @@ use alloy_primitives::uint; use e2e::{receipt, send, watch, Account, EventExt, Panic, PanicCode, Revert}; use eyre::Result; -use crate::abi::Erc20; - mod abi; sol!("src/constructor.sol"); diff --git a/examples/erc721-metadata/Cargo.toml b/examples/erc721-metadata/Cargo.toml new file mode 100644 index 00000000..1bc6050a --- /dev/null +++ b/examples/erc721-metadata/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "erc721-metadata-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 +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-metadata/src/constructor.sol b/examples/erc721-metadata/src/constructor.sol new file mode 100644 index 00000000..ec6df515 --- /dev/null +++ b/examples/erc721-metadata/src/constructor.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +contract Erc721MetadataExample { + 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; + + string private _name; + string private _symbol; + string private _baseUri; + + mapping(uint256 => string) _tokenUris; + + constructor(string memory name_, string memory symbol_, string memory baseUri_) { + _name = name_; + _symbol = symbol_; + _baseUri = baseUri_; + } +} diff --git a/examples/erc721-metadata/src/lib.rs b/examples/erc721-metadata/src/lib.rs new file mode 100644 index 00000000..2dea5e55 --- /dev/null +++ b/examples/erc721-metadata/src/lib.rs @@ -0,0 +1,71 @@ +#![cfg_attr(not(test), no_main, no_std)] +extern crate alloc; + +use alloc::{ + string::{String, ToString}, + vec::Vec, +}; + +use alloy_primitives::{Address, U256}; +use openzeppelin_stylus::token::erc721::{ + extensions::{ + Erc721Metadata as Metadata, Erc721UriStorage as UriStorage, + IErc721Burnable, IErc721Metadata, + }, + Erc721, IErc721, +}; +use stylus_sdk::prelude::{entrypoint, external, sol_storage}; + +sol_storage! { + #[entrypoint] + struct Erc721MetadataExample { + #[borrow] + Erc721 erc721; + #[borrow] + Metadata metadata; + #[borrow] + UriStorage uri_storage; + } +} + +#[external] +#[inherit(Erc721, Metadata, UriStorage)] +impl Erc721MetadataExample { + pub fn mint(&mut self, to: Address, token_id: U256) -> Result<(), Vec> { + Ok(self.erc721._mint(to, token_id)?) + } + + pub fn burn(&mut self, token_id: U256) -> Result<(), Vec> { + Ok(self.erc721.burn(token_id)?) + } + + // Overrides [`Erc721UriStorage::token_uri`]. + // Returns the Uniform Resource Identifier (URI) for tokenId token. + #[selector(name = "tokenURI")] + pub fn token_uri(&self, token_id: U256) -> Result> { + let _owner = self.erc721.owner_of(token_id)?; + + let base = self.metadata.base_uri(); + let token_uri = self.uri_storage.token_uri(token_id); + + // If there is no base URI, return the token URI. + if base.is_empty() { + return Ok(token_uri); + } + + // If both are set, + // concatenate the base URI and token URI. + let uri = if !token_uri.is_empty() { + base + &token_uri + } else { + base + &token_id.to_string() + }; + + Ok(uri) + } + + #[selector(name = "setTokenURI")] + pub fn set_token_uri(&mut self, token_id: U256, token_uri: String) { + self.uri_storage._set_token_uri(token_id, token_uri) + } +} diff --git a/examples/erc721/tests/abi.rs b/examples/erc721-metadata/tests/abi/mod.rs similarity index 81% rename from examples/erc721/tests/abi.rs rename to examples/erc721-metadata/tests/abi/mod.rs index 21b5d3bd..9fd31eae 100644 --- a/examples/erc721/tests/abi.rs +++ b/examples/erc721-metadata/tests/abi/mod.rs @@ -4,28 +4,28 @@ use alloy::sol; sol!( #[sol(rpc)] contract Erc721 { - function name() external view returns (string memory name); - function symbol() external view returns (string memory symbol); - function tokenURI(uint256 tokenId) external view returns (string memory); - - function supportsInterface(bytes4 interfaceIf) external pure returns (bool); - + function approve(address to, uint256 tokenId) external; + #[derive(Debug)] function balanceOf(address owner) external view returns (uint256 balance); + #[derive(Debug)] + function getApproved(uint256 tokenId) external view returns (address approved); + #[derive(Debug)] + function isApprovedForAll(address owner, address operator) external view returns (bool approved); + #[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 burn(uint256 tokenId) external; + function transferFrom(address from, address to, uint256 tokenId) external; function mint(address to, uint256 tokenId) external; - - function paused() external view returns (bool); - function pause() external; - function unpause() external; + function burn(uint256 tokenId) external; + function baseUri() external view returns (string memory baseURI); + function name() external view returns (string memory name); + function symbol() external view returns (string memory symbol); + #[derive(Debug)] + function tokenURI(uint256 tokenId) external view returns (string memory tokenURI); + function setTokenURI(uint256 tokenId, string memory tokenURI) external; + function supportsInterface(bytes4 interfaceIf) external pure returns (bool); error ERC721InvalidOwner(address owner); error ERC721NonexistentToken(uint256 tokenId); @@ -36,16 +36,13 @@ sol!( error ERC721InvalidApprover(address approver); error ERC721InvalidOperator(address operator); - error EnforcedPause(); - error ExpectedPause(); - - #[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 Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + #[derive(Debug, PartialEq)] + event MetadataUpdate(uint256 tokenId); + } ); diff --git a/examples/erc721-metadata/tests/erc721.rs b/examples/erc721-metadata/tests/erc721.rs new file mode 100644 index 00000000..658cacd8 --- /dev/null +++ b/examples/erc721-metadata/tests/erc721.rs @@ -0,0 +1,259 @@ +#![cfg(feature = "e2e")] + +use abi::Erc721; +use alloy::{ + primitives::{Address, U256}, + sol, + sol_types::SolConstructor, +}; +use e2e::{receipt, watch, Account, EventExt, Revert}; + +mod abi; + +sol!("src/constructor.sol"); + +const TOKEN_NAME: &str = "Test Token"; +const TOKEN_SYMBOL: &str = "NFT"; + +fn random_token_id() -> U256 { + let num: u32 = rand::random(); + U256::from(num) +} + +async fn deploy( + rpc_url: &str, + private_key: &str, + base_uri: &str, +) -> eyre::Result
{ + let args = Erc721MetadataExample::constructorCall { + name_: TOKEN_NAME.to_owned(), + symbol_: TOKEN_SYMBOL.to_owned(), + baseUri_: base_uri.to_owned(), + }; + let args = alloy::hex::encode(args.abi_encode()); + e2e::deploy(rpc_url, private_key, Some(args)).await +} + +// ============================================================================ +// Integration Tests: ERC-721 Metadata Extension +// ============================================================================ + +#[e2e::test] +async fn constructs(alice: Account) -> eyre::Result<()> { + let base_uri = ""; + + let contract_addr = deploy(alice.url(), &alice.pk(), base_uri).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let Erc721::nameReturn { name } = contract.name().call().await?; + let Erc721::symbolReturn { symbol } = contract.symbol().call().await?; + let Erc721::baseUriReturn { baseURI } = contract.baseUri().call().await?; + + assert_eq!(TOKEN_NAME.to_owned(), name); + assert_eq!(TOKEN_SYMBOL.to_owned(), symbol); + assert_eq!(base_uri.to_owned(), baseURI); + + Ok(()) +} + +#[e2e::test] +async fn constructs_with_base_uri(alice: Account) -> eyre::Result<()> { + let base_uri = "https://github.com/OpenZeppelin/rust-contracts-stylus"; + + let contract_addr = deploy(alice.url(), &alice.pk(), base_uri).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let Erc721::baseUriReturn { baseURI } = contract.baseUri().call().await?; + + assert_eq!(base_uri.to_owned(), baseURI); + + Ok(()) +} + +// ============================================================================ +// Integration Tests: ERC-721 URI Storage Extension +// ============================================================================ + +#[e2e::test] +async fn error_when_checking_token_uri_for_nonexistent_token( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk(), "").await?; + + let contract = Erc721::new(contract_addr, &alice.wallet); + + let token_id = random_token_id(); + + let err = contract + .tokenURI(token_id) + .call() + .await + .expect_err("should return ERC721NonexistentToken"); + + assert!( + err.reverted_with(Erc721::ERC721NonexistentToken { tokenId: token_id }) + ); + Ok(()) +} + +#[e2e::test] +async fn return_empty_token_uri_when_without_base_uri_and_token_uri( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk(), "").await?; + + let contract = Erc721::new(contract_addr, &alice.wallet); + + let token_id = random_token_id(); + + let _ = watch!(contract.mint(alice.address(), token_id))?; + + let Erc721::tokenURIReturn { tokenURI } = + contract.tokenURI(token_id).call().await?; + + assert_eq!("", tokenURI); + + Ok(()) +} + +#[e2e::test] +async fn return_token_uri_with_base_uri_and_without_token_uri( + alice: Account, +) -> eyre::Result<()> { + let base_uri = "https://github.com/OpenZeppelin/rust-contracts-stylus/"; + + let contract_addr = deploy(alice.url(), &alice.pk(), base_uri).await?; + + let contract = Erc721::new(contract_addr, &alice.wallet); + + let token_id = random_token_id(); + + let _ = watch!(contract.mint(alice.address(), token_id))?; + + let Erc721::tokenURIReturn { tokenURI } = + contract.tokenURI(token_id).call().await?; + + assert_eq!(base_uri.to_owned() + &token_id.to_string(), tokenURI); + Ok(()) +} + +#[e2e::test] +async fn return_token_uri_with_base_uri_and_token_uri( + alice: Account, +) -> eyre::Result<()> { + let base_uri = "https://github.com/OpenZeppelin/rust-contracts-stylus/"; + + let contract_addr = deploy(alice.url(), &alice.pk(), base_uri).await?; + + let contract = Erc721::new(contract_addr, &alice.wallet); + + let token_id = random_token_id(); + + let _ = watch!(contract.mint(alice.address(), token_id))?; + + let token_uri = String::from( + "blob/main/contracts/src/token/erc721/extensions/uri_storage.rs", + ); + + let receipt = receipt!(contract.setTokenURI(token_id, token_uri.clone()))?; + + assert!(receipt.emits(Erc721::MetadataUpdate { tokenId: token_id })); + + let Erc721::tokenURIReturn { tokenURI } = + contract.tokenURI(token_id).call().await?; + + assert_eq!(base_uri.to_owned() + &token_uri, tokenURI); + + Ok(()) +} + +#[e2e::test] +async fn set_token_uri_before_mint(alice: Account) -> eyre::Result<()> { + let base_uri = "https://github.com/OpenZeppelin/rust-contracts-stylus/"; + + let contract_addr = deploy(alice.url(), &alice.pk(), base_uri).await?; + + let contract = Erc721::new(contract_addr, &alice.wallet); + + let token_id = random_token_id(); + + let err = contract + .ownerOf(token_id) + .call() + .await + .expect_err("should return `ERC721NonexistentToken`"); + + assert!( + err.reverted_with(Erc721::ERC721NonexistentToken { tokenId: token_id }) + ); + + let token_uri = String::from( + "blob/main/contracts/src/token/erc721/extensions/uri_storage.rs", + ); + + let receipt = receipt!(contract.setTokenURI(token_id, token_uri.clone()))?; + + assert!(receipt.emits(Erc721::MetadataUpdate { tokenId: token_id })); + + let _ = watch!(contract.mint(alice.address(), token_id))?; + + let Erc721::tokenURIReturn { tokenURI } = + contract.tokenURI(token_id).call().await?; + + assert_eq!(base_uri.to_owned() + &token_uri, tokenURI); + + Ok(()) +} + +#[e2e::test] +async fn return_token_uri_after_burn_and_remint( + alice: Account, +) -> eyre::Result<()> { + let base_uri = "https://github.com/OpenZeppelin/rust-contracts-stylus/"; + + let alice_addr = alice.address(); + + let contract_addr = deploy(alice.url(), &alice.pk(), base_uri).await?; + + let contract = Erc721::new(contract_addr, &alice.wallet); + + let token_id = random_token_id(); + + let _ = watch!(contract.mint(alice.address(), token_id))?; + + let receipt = receipt!(contract.burn(token_id))?; + + assert!(receipt.emits(Erc721::Transfer { + from: alice_addr, + to: Address::ZERO, + tokenId: token_id, + })); + + let err = contract + .ownerOf(token_id) + .call() + .await + .expect_err("should return `ERC721NonexistentToken`"); + + assert!( + err.reverted_with(Erc721::ERC721NonexistentToken { tokenId: token_id }) + ); + + let receipt = receipt!(contract.mint(alice_addr, token_id))?; + + assert!(receipt.emits(Erc721::Transfer { + from: Address::ZERO, + to: alice_addr, + tokenId: token_id + })); + + let Erc721::ownerOfReturn { ownerOf: owner_of } = + contract.ownerOf(token_id).call().await?; + assert_eq!(alice_addr, owner_of); + + let Erc721::tokenURIReturn { tokenURI } = + contract.tokenURI(token_id).call().await?; + + assert_eq!(base_uri.to_owned() + &token_id.to_string(), tokenURI); + Ok(()) +} diff --git a/examples/erc721/src/ERC721ReceiverMock.sol b/examples/erc721/src/ERC721ReceiverMock.sol new file mode 100644 index 00000000..620df066 --- /dev/null +++ b/examples/erc721/src/ERC721ReceiverMock.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.21; + +import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.0.2/contracts/token/ERC721/IERC721Receiver.sol"; + +contract ERC721ReceiverMock is IERC721Receiver { + enum RevertType { + None, + RevertWithoutMessage, + RevertWithMessage, + RevertWithCustomError, + Panic + } + + bytes4 private immutable _retval; + RevertType private immutable _error; + + event Received(address operator, address from, uint256 tokenId, bytes data); + + error CustomError(bytes4); + + constructor(bytes4 retval, RevertType error) { + _retval = retval; + _error = error; + } + + function onERC721Received( + address operator, + address from, + uint256 tokenId, + bytes memory data + ) public returns (bytes4) { + if (_error == RevertType.RevertWithoutMessage) { + revert(); + } else if (_error == RevertType.RevertWithMessage) { + revert("ERC721ReceiverMock: reverting"); + } else if (_error == RevertType.RevertWithCustomError) { + revert CustomError(_retval); + } else if (_error == RevertType.Panic) { + uint256 a = uint256(0) / uint256(0); + a; + } + + emit Received(operator, from, tokenId, data); + return _retval; + } +} diff --git a/examples/erc721/src/constructor.sol b/examples/erc721/src/constructor.sol index 3467a585..79c3e778 100644 --- a/examples/erc721/src/constructor.sol +++ b/examples/erc721/src/constructor.sol @@ -14,16 +14,9 @@ contract Erc721Example { uint256[] private _allTokens; mapping(uint256 tokenId => uint256) private _allTokensIndex; - string private _name; - string private _symbol; - bool _paused; - mapping(uint256 => string) _tokenUris; - - constructor(string memory name_, string memory symbol_) { - _name = name_; - _symbol = symbol_; + constructor() { _paused = false; } } diff --git a/examples/erc721/src/lib.rs b/examples/erc721/src/lib.rs index 6454a449..fe873815 100644 --- a/examples/erc721/src/lib.rs +++ b/examples/erc721/src/lib.rs @@ -1,18 +1,12 @@ #![cfg_attr(not(test), no_main, no_std)] extern crate alloc; -use alloc::{ - string::{String, ToString}, - vec::Vec, -}; +use alloc::vec::Vec; use alloy_primitives::{Address, U256}; use openzeppelin_stylus::{ token::erc721::{ - extensions::{ - Erc721Enumerable as Enumerable, Erc721Metadata as Metadata, - Erc721UriStorage as UriStorage, IErc721Burnable, IErc721Metadata, - }, + extensions::{Erc721Enumerable as Enumerable, IErc721Burnable}, Erc721, IErc721, }, utils::Pausable, @@ -30,23 +24,27 @@ sol_storage! { #[borrow] Enumerable enumerable; #[borrow] - Metadata metadata; - #[borrow] Pausable pausable; - #[borrow] - UriStorage uri_storage; } } #[external] -#[inherit(Erc721, Enumerable, Metadata, Pausable, UriStorage)] +#[inherit(Erc721, Enumerable, Pausable)] impl Erc721Example { pub fn burn(&mut self, token_id: U256) -> Result<(), Vec> { self.pausable.when_not_paused()?; + // Retrieve the owner. + let owner = self.erc721.owner_of(token_id)?; + self.erc721.burn(token_id)?; // Update the extension's state. + self.enumerable._remove_token_from_owner_enumeration( + owner, + token_id, + &self.erc721, + )?; self.enumerable._remove_token_from_all_tokens_enumeration(token_id); Ok(()) @@ -59,6 +57,11 @@ impl Erc721Example { // Update the extension's state. self.enumerable._add_token_to_all_tokens_enumeration(token_id); + self.enumerable._add_token_to_owner_enumeration( + to, + token_id, + &self.erc721, + )?; Ok(()) } @@ -148,24 +151,4 @@ impl Erc721Example { Ok(()) } - - // Overrides [`Erc721UriStorage::token_uri`]. - // Returns the Uniform Resource Identifier (URI) for tokenId token. - pub fn token_uri(&self, token_id: U256) -> String { - let base = self.metadata.base_uri(); - let token_uri = self.uri_storage.token_uri(token_id); - - // If there is no base URI, return the token URI. - if base.is_empty() { - return token_uri; - } - - // If both are set, - // concatenate the base URI and token URI. - if !token_uri.is_empty() { - base + &token_uri - } else { - base + &token_id.to_string() - } - } } diff --git a/examples/erc721/tests/abi/mod.rs b/examples/erc721/tests/abi/mod.rs new file mode 100644 index 00000000..b02f4448 --- /dev/null +++ b/examples/erc721/tests/abi/mod.rs @@ -0,0 +1,59 @@ +#![allow(dead_code)] +use alloy::sol; + +sol!( + #[sol(rpc)] + contract Erc721 { + function approve(address to, uint256 tokenId) external; + #[derive(Debug)] + function balanceOf(address owner) external view returns (uint256 balance); + #[derive(Debug)] + function getApproved(uint256 tokenId) external view returns (address approved); + #[derive(Debug)] + function isApprovedForAll(address owner, address operator) external view returns (bool approved); + #[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 setApprovalForAll(address operator, bool approved) external; + function totalSupply() external view returns (uint256 totalSupply); + function transferFrom(address from, address to, uint256 tokenId) external; + function mint(address to, uint256 tokenId) external; + function burn(uint256 tokenId) external; + function paused() external view returns (bool paused); + function pause() external; + function unpause() external; + #[derive(Debug)] + function whenPaused() external view; + #[derive(Debug)] + function whenNotPaused() external view; + #[derive(Debug)] + function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256 tokenId); + #[derive(Debug)] + function tokenByIndex(uint256 index) external view returns (uint256 tokenId); + + error ERC721IncorrectOwner(address sender, uint256 tokenId, address owner); + error ERC721InsufficientApproval(address operator, uint256 tokenId); + error ERC721InvalidApprover(address approver); + error ERC721InvalidOperator(address operator); + error ERC721InvalidOwner(address owner); + error ERC721InvalidReceiver(address receiver); + error ERC721InvalidSender(address sender); + error ERC721NonexistentToken(uint256 tokenId); + error ERC721OutOfBoundsIndex(address owner, uint256 index); + error ERC721EnumerableForbiddenBatchMint(); + error EnforcedPause(); + error ExpectedPause(); + + #[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 Paused(address account); + #[derive(Debug, PartialEq)] + event Unpaused(address account); + } +); diff --git a/examples/erc721/tests/erc721.rs b/examples/erc721/tests/erc721.rs index e78f9eea..64ec3924 100644 --- a/examples/erc721/tests/erc721.rs +++ b/examples/erc721/tests/erc721.rs @@ -1,46 +1,97 @@ #![cfg(feature = "e2e")] +use abi::Erc721; use alloy::{ - primitives::{Address, U256}, + primitives::{fixed_bytes, Address, Bytes, U256}, sol, sol_types::SolConstructor, }; use alloy_primitives::uint; use e2e::{receipt, send, watch, Account, EventExt, Revert}; - -use crate::abi::Erc721; +use mock::{receiver, receiver::ERC721ReceiverMock}; mod abi; +mod mock; sol!("src/constructor.sol"); -const TOKEN_NAME: &str = "Test Token"; -const TOKEN_SYMBOL: &str = "NFT"; - fn random_token_id() -> U256 { let num: u32 = rand::random(); U256::from(num) } async fn deploy(rpc_url: &str, private_key: &str) -> eyre::Result
{ - let args = Erc721Example::constructorCall { - name_: TOKEN_NAME.to_owned(), - symbol_: TOKEN_SYMBOL.to_owned(), - }; + let args = Erc721Example::constructorCall {}; let args = alloy::hex::encode(args.abi_encode()); e2e::deploy(rpc_url, private_key, Some(args)).await } +// ============================================================================ +// Integration Tests: ERC-721 Token +// ============================================================================ + #[e2e::test] async fn constructs(alice: Account) -> eyre::Result<()> { let contract_addr = deploy(alice.url(), &alice.pk()).await?; let contract = Erc721::new(contract_addr, &alice.wallet); - let name = contract.name().call().await?.name; - let symbol = contract.symbol().call().await?.symbol; + let Erc721::pausedReturn { paused } = contract.paused().call().await?; + + assert_eq!(false, paused); + + Ok(()) +} + +#[e2e::test] +async fn error_when_checking_balance_of_invalid_owner( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + let invalid_owner = Address::ZERO; + + let err = contract + .balanceOf(invalid_owner) + .call() + .await + .expect_err("should return `ERC721InvalidOwner`"); + assert!( + err.reverted_with(Erc721::ERC721InvalidOwner { owner: invalid_owner }) + ); + + Ok(()) +} + +#[e2e::test] +async fn balance_of_zero_balance(alice: Account) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let Erc721::balanceOfReturn { balance } = + contract.balanceOf(alice.address()).call().await?; + assert_eq!(uint!(0_U256), balance); + + Ok(()) +} + +#[e2e::test] +async fn error_when_checking_owner_of_nonexistent_token( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + let token_id = random_token_id(); + + let err = contract + .ownerOf(token_id) + .call() + .await + .expect_err("should return `ERC721NonexistentToken`"); + + assert!( + err.reverted_with(Erc721::ERC721NonexistentToken { tokenId: token_id }) + ); - assert_eq!(name, TOKEN_NAME.to_owned()); - assert_eq!(symbol, TOKEN_SYMBOL.to_owned()); Ok(()) } @@ -51,17 +102,27 @@ async fn mints(alice: Account) -> eyre::Result<()> { let alice_addr = alice.address(); let token_id = random_token_id(); - let _ = watch!(contract.mint(alice_addr, token_id))?; - let owner_of = contract.ownerOf(token_id).call().await?.ownerOf; - assert_eq!(owner_of, alice_addr); + let receipt = receipt!(contract.mint(alice_addr, token_id))?; + + assert!(receipt.emits(Erc721::Transfer { + from: Address::ZERO, + to: alice_addr, + tokenId: token_id + })); + + let Erc721::ownerOfReturn { ownerOf: owner_of } = + contract.ownerOf(token_id).call().await?; + assert_eq!(alice_addr, owner_of); + + let Erc721::balanceOfReturn { balance } = + contract.balanceOf(alice_addr).call().await?; + assert_eq!(uint!(1_U256), balance); - let balance = contract.balanceOf(alice_addr).call().await?.balance; - assert!(balance >= uint!(1_U256)); Ok(()) } #[e2e::test] -async fn errors_when_reusing_token_id(alice: Account) -> eyre::Result<()> { +async fn error_when_minting_token_id_twice(alice: Account) -> eyre::Result<()> { let contract_addr = deploy(alice.url(), &alice.pk()).await?; let contract = Erc721::new(contract_addr, &alice.wallet); @@ -73,11 +134,31 @@ async fn errors_when_reusing_token_id(alice: Account) -> eyre::Result<()> { .expect_err("should not mint a token id twice"); assert!(err .reverted_with(Erc721::ERC721InvalidSender { sender: Address::ZERO })); + + Ok(()) +} + +#[e2e::test] +async fn error_when_minting_token_to_invalid_receiver( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let token_id = random_token_id(); + let invalid_receiver = Address::ZERO; + + let err = send!(contract.mint(invalid_receiver, token_id)) + .expect_err("should not mint a token for invalid receiver"); + assert!(err.reverted_with(Erc721::ERC721InvalidReceiver { + receiver: invalid_receiver + })); + Ok(()) } #[e2e::test] -async fn transfers(alice: Account, bob: Account) -> eyre::Result<()> { +async fn transfers_from(alice: Account, bob: Account) -> eyre::Result<()> { let contract_addr = deploy(alice.url(), &alice.pk()).await?; let contract = Erc721::new(contract_addr, &alice.wallet); @@ -85,65 +166,197 @@ async fn transfers(alice: Account, bob: Account) -> eyre::Result<()> { let bob_addr = bob.address(); let token_id = random_token_id(); let _ = watch!(contract.mint(alice_addr, token_id))?; + + let Erc721::balanceOfReturn { balance: initial_alice_balance } = + contract.balanceOf(alice_addr).call().await?; + + let Erc721::balanceOfReturn { balance: initial_bob_balance } = + contract.balanceOf(bob_addr).call().await?; + let receipt = receipt!(contract.transferFrom(alice_addr, bob_addr, token_id))?; - receipt.emits(Erc721::Transfer { + assert!(receipt.emits(Erc721::Transfer { from: alice_addr, to: bob_addr, tokenId: token_id, - }); + })); let Erc721::ownerOfReturn { ownerOf } = contract.ownerOf(token_id).call().await?; - assert_eq!(ownerOf, bob_addr); + assert_eq!(bob_addr, ownerOf); + + let Erc721::balanceOfReturn { balance: alice_balance } = + contract.balanceOf(alice_addr).call().await?; + + let Erc721::balanceOfReturn { balance: bob_balance } = + contract.balanceOf(bob_addr).call().await?; + + let one = uint!(1_U256); + assert_eq!(initial_alice_balance - one, alice_balance); + assert_eq!(initial_bob_balance + one, bob_balance); + + Ok(()) +} + +#[e2e::test] +async fn transfers_from_approved_token( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract_alice = Erc721::new(contract_addr, &alice.wallet); + let contract_bob = Erc721::new(contract_addr, &bob.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + let token_id = random_token_id(); + + let _ = watch!(contract_alice.mint(alice_addr, token_id))?; + let _ = watch!(contract_alice.approve(bob_addr, token_id))?; + + let Erc721::balanceOfReturn { balance: initial_alice_balance } = + contract_alice.balanceOf(alice_addr).call().await?; + + let Erc721::balanceOfReturn { balance: initial_bob_balance } = + contract_bob.balanceOf(bob_addr).call().await?; + + let receipt = + receipt!(contract_bob.transferFrom(alice_addr, bob_addr, token_id))?; + + assert!(receipt.emits(Erc721::Transfer { + from: alice_addr, + to: bob_addr, + tokenId: token_id, + })); + + let Erc721::ownerOfReturn { ownerOf } = + contract_alice.ownerOf(token_id).call().await?; + assert_eq!(bob_addr, ownerOf); + + let Erc721::balanceOfReturn { balance: alice_balance } = + contract_alice.balanceOf(alice_addr).call().await?; + + let Erc721::balanceOfReturn { balance: bob_balance } = + contract_bob.balanceOf(bob_addr).call().await?; + + let one = uint!(1_U256); + assert_eq!(initial_alice_balance - one, alice_balance); + assert_eq!(initial_bob_balance + one, bob_balance); + Ok(()) } #[e2e::test] -async fn errors_when_transfer_nonexistent_token( +async fn transfers_from_approved_for_all( alice: Account, bob: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract_alice = Erc721::new(contract_addr, &alice.wallet); + let contract_bob = Erc721::new(contract_addr, &bob.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + let token_id = random_token_id(); + + let _ = watch!(contract_alice.mint(alice_addr, token_id))?; + let _ = watch!(contract_alice.setApprovalForAll(bob_addr, true))?; + + let Erc721::balanceOfReturn { balance: initial_alice_balance } = + contract_alice.balanceOf(alice_addr).call().await?; + + let Erc721::balanceOfReturn { balance: initial_bob_balance } = + contract_bob.balanceOf(bob_addr).call().await?; + + let receipt = + receipt!(contract_bob.transferFrom(alice_addr, bob_addr, token_id))?; + + assert!(receipt.emits(Erc721::Transfer { + from: alice_addr, + to: bob_addr, + tokenId: token_id, + })); + + let Erc721::ownerOfReturn { ownerOf } = + contract_alice.ownerOf(token_id).call().await?; + assert_eq!(bob_addr, ownerOf); + + let Erc721::balanceOfReturn { balance: alice_balance } = + contract_alice.balanceOf(alice_addr).call().await?; + + let Erc721::balanceOfReturn { balance: bob_balance } = + contract_bob.balanceOf(bob_addr).call().await?; + + let one = uint!(1_U256); + assert_eq!(initial_alice_balance - one, alice_balance); + assert_eq!(initial_bob_balance + one, bob_balance); + + Ok(()) +} + +#[e2e::test] +async fn error_when_transfer_to_invalid_receiver( + alice: Account, ) -> eyre::Result<()> { let contract_addr = deploy(alice.url(), &alice.pk()).await?; let contract = Erc721::new(contract_addr, &alice.wallet); let alice_addr = alice.address(); + let invalid_receiver = Address::ZERO; let token_id = random_token_id(); - let tx = contract.transferFrom(alice_addr, bob.address(), token_id); - let err = send!(tx).expect_err("should not transfer a non-existent token"); - assert!( - err.reverted_with(Erc721::ERC721NonexistentToken { tokenId: token_id }) - ); + let _ = watch!(contract.mint(alice_addr, token_id))?; + + let err = + send!(contract.transferFrom(alice_addr, invalid_receiver, token_id)) + .expect_err("should not transfer the token to invalid receiver"); + assert!(err.reverted_with(Erc721::ERC721InvalidReceiver { + receiver: invalid_receiver + })); + + let Erc721::ownerOfReturn { ownerOf } = + contract.ownerOf(token_id).call().await?; + assert_eq!(alice_addr, ownerOf); + Ok(()) } #[e2e::test] -async fn approves_token_transfer( +async fn error_when_transfer_from_incorrect_owner( alice: Account, bob: Account, + dave: Account, ) -> eyre::Result<()> { let contract_addr = deploy(alice.url(), &alice.pk()).await?; let contract = Erc721::new(contract_addr, &alice.wallet); let alice_addr = alice.address(); let bob_addr = bob.address(); + let dave_addr = dave.address(); + let token_id = random_token_id(); + let _ = watch!(contract.mint(alice_addr, token_id))?; - let _ = watch!(contract.approve(bob_addr, token_id))?; - let contract = Erc721::new(contract_addr, &bob.wallet); - let _ = watch!(contract.transferFrom(alice_addr, bob_addr, token_id))?; + let err = send!(contract.transferFrom(dave_addr, bob_addr, token_id)) + .expect_err("should not transfer the token from incorrect owner"); + + assert!(err.reverted_with(Erc721::ERC721IncorrectOwner { + sender: dave_addr, + owner: alice_addr, + tokenId: token_id + })); + let Erc721::ownerOfReturn { ownerOf } = contract.ownerOf(token_id).call().await?; - assert_ne!(ownerOf, alice_addr); - assert_eq!(ownerOf, bob_addr); + assert_eq!(alice_addr, ownerOf); + Ok(()) } #[e2e::test] -async fn errors_when_transfer_unapproved_token( +async fn error_when_transfer_with_insufficient_approval( alice: Account, bob: Account, ) -> eyre::Result<()> { @@ -156,12 +369,1653 @@ async fn errors_when_transfer_unapproved_token( let _ = watch!(contract.mint(alice_addr, token_id))?; let contract = Erc721::new(contract_addr, &bob.wallet); - let tx = contract.transferFrom(alice_addr, bob_addr, token_id); + let err = send!(contract.transferFrom(alice_addr, bob_addr, token_id)) + .expect_err("should not transfer unapproved token"); - let err = send!(tx).expect_err("should not transfer unapproved token"); assert!(err.reverted_with(Erc721::ERC721InsufficientApproval { operator: bob_addr, tokenId: token_id, })); + + let Erc721::ownerOfReturn { ownerOf } = + contract.ownerOf(token_id).call().await?; + assert_eq!(alice_addr, ownerOf); + + Ok(()) +} + +#[e2e::test] +async fn error_when_transfer_nonexistent_token( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let token_id = random_token_id(); + + let err = send!(contract.transferFrom(alice_addr, bob.address(), token_id)) + .expect_err("should not transfer a non-existent token"); + assert!( + err.reverted_with(Erc721::ERC721NonexistentToken { tokenId: token_id }) + ); + + let err = contract + .ownerOf(token_id) + .call() + .await + .expect_err("should return `ERC721NonexistentToken`"); + + assert!( + err.reverted_with(Erc721::ERC721NonexistentToken { tokenId: token_id }) + ); + + Ok(()) +} + +#[e2e::test] +async fn safe_transfers_from(alice: Account, bob: Account) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + let token_id = random_token_id(); + let _ = watch!(contract.mint(alice_addr, token_id))?; + + let Erc721::balanceOfReturn { balance: initial_alice_balance } = + contract.balanceOf(alice_addr).call().await?; + + let Erc721::balanceOfReturn { balance: initial_bob_balance } = + contract.balanceOf(bob_addr).call().await?; + + let receipt = + receipt!(contract.safeTransferFrom_0(alice_addr, bob_addr, token_id))?; + + assert!(receipt.emits(Erc721::Transfer { + from: alice_addr, + to: bob_addr, + tokenId: token_id, + })); + + let Erc721::ownerOfReturn { ownerOf } = + contract.ownerOf(token_id).call().await?; + assert_eq!(bob_addr, ownerOf); + + let Erc721::balanceOfReturn { balance: alice_balance } = + contract.balanceOf(alice_addr).call().await?; + + let Erc721::balanceOfReturn { balance: bob_balance } = + contract.balanceOf(bob_addr).call().await?; + + let one = uint!(1_U256); + assert_eq!(initial_alice_balance - one, alice_balance); + assert_eq!(initial_bob_balance + one, bob_balance); + + Ok(()) +} + +#[e2e::test] +async fn safe_transfers_to_receiver_contract( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let receiver_address = + receiver::deploy(&alice.wallet, ERC721ReceiverMock::RevertType::None) + .await?; + + let alice_addr = alice.address(); + let token_id = random_token_id(); + + let _ = watch!(contract.mint(alice_addr, token_id))?; + + let Erc721::balanceOfReturn { balance: initial_alice_balance } = + contract.balanceOf(alice_addr).call().await?; + + let Erc721::balanceOfReturn { balance: initial_receiver_balance } = + contract.balanceOf(receiver_address).call().await?; + + let receipt = receipt!(contract.safeTransferFrom_0( + alice_addr, + receiver_address, + token_id + ))?; + + assert!(receipt.emits(Erc721::Transfer { + from: alice_addr, + to: receiver_address, + tokenId: token_id, + })); + + assert!(receipt.emits(ERC721ReceiverMock::Received { + operator: alice_addr, + from: alice_addr, + tokenId: token_id, + data: fixed_bytes!("").into(), + })); + + let Erc721::ownerOfReturn { ownerOf } = + contract.ownerOf(token_id).call().await?; + assert_eq!(receiver_address, ownerOf); + + let Erc721::balanceOfReturn { balance: alice_balance } = + contract.balanceOf(alice_addr).call().await?; + + let Erc721::balanceOfReturn { balance: receiver_balance } = + contract.balanceOf(receiver_address).call().await?; + + let one = uint!(1_U256); + assert_eq!(initial_alice_balance - one, alice_balance); + assert_eq!(initial_receiver_balance + one, receiver_balance); + + Ok(()) +} + +#[e2e::test] +async fn safe_transfers_from_approved_token( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract_alice = Erc721::new(contract_addr, &alice.wallet); + let contract_bob = Erc721::new(contract_addr, &bob.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + let token_id = random_token_id(); + + let _ = watch!(contract_alice.mint(alice_addr, token_id))?; + let _ = watch!(contract_alice.approve(bob_addr, token_id))?; + + let Erc721::balanceOfReturn { balance: initial_alice_balance } = + contract_alice.balanceOf(alice_addr).call().await?; + + let Erc721::balanceOfReturn { balance: initial_bob_balance } = + contract_bob.balanceOf(bob_addr).call().await?; + + let receipt = receipt!( + contract_bob.safeTransferFrom_0(alice_addr, bob_addr, token_id) + )?; + + assert!(receipt.emits(Erc721::Transfer { + from: alice_addr, + to: bob_addr, + tokenId: token_id, + })); + + let Erc721::ownerOfReturn { ownerOf } = + contract_alice.ownerOf(token_id).call().await?; + assert_eq!(bob_addr, ownerOf); + + let Erc721::balanceOfReturn { balance: alice_balance } = + contract_alice.balanceOf(alice_addr).call().await?; + + let Erc721::balanceOfReturn { balance: bob_balance } = + contract_bob.balanceOf(bob_addr).call().await?; + + let one = uint!(1_U256); + assert_eq!(initial_alice_balance - one, alice_balance); + assert_eq!(initial_bob_balance + one, bob_balance); + + Ok(()) +} + +#[e2e::test] +async fn safe_transfers_from_approved_for_all( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract_alice = Erc721::new(contract_addr, &alice.wallet); + let contract_bob = Erc721::new(contract_addr, &bob.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + let token_id = random_token_id(); + + let _ = watch!(contract_alice.mint(alice_addr, token_id))?; + let _ = watch!(contract_alice.setApprovalForAll(bob_addr, true))?; + + let Erc721::balanceOfReturn { balance: initial_alice_balance } = + contract_alice.balanceOf(alice_addr).call().await?; + + let Erc721::balanceOfReturn { balance: initial_bob_balance } = + contract_bob.balanceOf(bob_addr).call().await?; + + let receipt = receipt!( + contract_bob.safeTransferFrom_0(alice_addr, bob_addr, token_id) + )?; + + assert!(receipt.emits(Erc721::Transfer { + from: alice_addr, + to: bob_addr, + tokenId: token_id, + })); + + let Erc721::ownerOfReturn { ownerOf } = + contract_alice.ownerOf(token_id).call().await?; + assert_eq!(bob_addr, ownerOf); + + let Erc721::balanceOfReturn { balance: alice_balance } = + contract_alice.balanceOf(alice_addr).call().await?; + + let Erc721::balanceOfReturn { balance: bob_balance } = + contract_bob.balanceOf(bob_addr).call().await?; + + let one = uint!(1_U256); + assert_eq!(initial_alice_balance - one, alice_balance); + assert_eq!(initial_bob_balance + one, bob_balance); + + Ok(()) +} + +#[e2e::test] +async fn error_when_safe_transfer_to_invalid_receiver( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let invalid_receiver = Address::ZERO; + let token_id = random_token_id(); + + let _ = watch!(contract.mint(alice_addr, token_id))?; + + let err = send!(contract.safeTransferFrom_0( + alice_addr, + invalid_receiver, + token_id + )) + .expect_err("should not transfer the token to invalid receiver"); + assert!(err.reverted_with(Erc721::ERC721InvalidReceiver { + receiver: invalid_receiver + })); + + let Erc721::ownerOfReturn { ownerOf } = + contract.ownerOf(token_id).call().await?; + assert_eq!(alice_addr, ownerOf); + + Ok(()) +} + +#[e2e::test] +async fn error_when_safe_transfer_from_incorrect_owner( + alice: Account, + bob: Account, + dave: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + let dave_addr = dave.address(); + + let token_id = random_token_id(); + + let _ = watch!(contract.mint(alice_addr, token_id))?; + + let err = send!(contract.safeTransferFrom_0(dave_addr, bob_addr, token_id)) + .expect_err("should not transfer the token from incorrect owner"); + + assert!(err.reverted_with(Erc721::ERC721IncorrectOwner { + sender: dave_addr, + owner: alice_addr, + tokenId: token_id + })); + + let Erc721::ownerOfReturn { ownerOf } = + contract.ownerOf(token_id).call().await?; + assert_eq!(alice_addr, ownerOf); + + Ok(()) +} + +#[e2e::test] +async fn error_when_safe_transfer_with_insufficient_approval( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + let token_id = random_token_id(); + let _ = watch!(contract.mint(alice_addr, token_id))?; + + let contract = Erc721::new(contract_addr, &bob.wallet); + + let err = + send!(contract.safeTransferFrom_0(alice_addr, bob_addr, token_id)) + .expect_err("should not transfer unapproved token"); + + assert!(err.reverted_with(Erc721::ERC721InsufficientApproval { + operator: bob_addr, + tokenId: token_id, + })); + + let Erc721::ownerOfReturn { ownerOf } = + contract.ownerOf(token_id).call().await?; + assert_eq!(alice_addr, ownerOf); + + Ok(()) +} + +#[e2e::test] +async fn error_when_safe_transfer_nonexistent_token( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let token_id = random_token_id(); + + let err = + send!(contract.safeTransferFrom_0(alice_addr, bob.address(), token_id)) + .expect_err("should not transfer a non-existent token"); + assert!( + err.reverted_with(Erc721::ERC721NonexistentToken { tokenId: token_id }) + ); + + let err = contract + .ownerOf(token_id) + .call() + .await + .expect_err("should return `ERC721NonexistentToken`"); + + assert!( + err.reverted_with(Erc721::ERC721NonexistentToken { tokenId: token_id }) + ); + + Ok(()) +} + +// TODO: Test reverts & panics on ERC721ReceiverMock for +// `Erc721::safeTransferFrom_0`. + +#[e2e::test] +async fn safe_transfers_from_with_data( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + let token_id = random_token_id(); + let _ = watch!(contract.mint(alice_addr, token_id))?; + + let Erc721::balanceOfReturn { balance: initial_alice_balance } = + contract.balanceOf(alice_addr).call().await?; + + let Erc721::balanceOfReturn { balance: initial_bob_balance } = + contract.balanceOf(bob_addr).call().await?; + + let receipt = receipt!(contract.safeTransferFrom_1( + alice_addr, + bob_addr, + token_id, + fixed_bytes!("deadbeef").into() + ))?; + + assert!(receipt.emits(Erc721::Transfer { + from: alice_addr, + to: bob_addr, + tokenId: token_id, + })); + + let Erc721::ownerOfReturn { ownerOf } = + contract.ownerOf(token_id).call().await?; + assert_eq!(bob_addr, ownerOf); + + let Erc721::balanceOfReturn { balance: alice_balance } = + contract.balanceOf(alice_addr).call().await?; + + let Erc721::balanceOfReturn { balance: bob_balance } = + contract.balanceOf(bob_addr).call().await?; + + let one = uint!(1_U256); + assert_eq!(initial_alice_balance - one, alice_balance); + assert_eq!(initial_bob_balance + one, bob_balance); + + Ok(()) +} + +#[e2e::test] +async fn safe_transfers_with_data_to_receiver_contract( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let receiver_address = + receiver::deploy(&alice.wallet, ERC721ReceiverMock::RevertType::None) + .await?; + + let alice_addr = alice.address(); + let token_id = random_token_id(); + let data: Bytes = fixed_bytes!("deadbeef").into(); + + let _ = watch!(contract.mint(alice_addr, token_id))?; + + let Erc721::balanceOfReturn { balance: initial_alice_balance } = + contract.balanceOf(alice_addr).call().await?; + + let Erc721::balanceOfReturn { balance: initial_receiver_balance } = + contract.balanceOf(receiver_address).call().await?; + + let receipt = receipt!(contract.safeTransferFrom_1( + alice_addr, + receiver_address, + token_id, + data.clone() + ))?; + + assert!(receipt.emits(Erc721::Transfer { + from: alice_addr, + to: receiver_address, + tokenId: token_id, + })); + + assert!(receipt.emits(ERC721ReceiverMock::Received { + operator: alice_addr, + from: alice_addr, + tokenId: token_id, + data, + })); + + let Erc721::ownerOfReturn { ownerOf } = + contract.ownerOf(token_id).call().await?; + assert_eq!(receiver_address, ownerOf); + + let Erc721::balanceOfReturn { balance: alice_balance } = + contract.balanceOf(alice_addr).call().await?; + + let Erc721::balanceOfReturn { balance: receiver_balance } = + contract.balanceOf(receiver_address).call().await?; + + let one = uint!(1_U256); + assert_eq!(initial_alice_balance - one, alice_balance); + assert_eq!(initial_receiver_balance + one, receiver_balance); + + Ok(()) +} + +#[e2e::test] +async fn safe_transfers_from_with_data_approved_token( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract_alice = Erc721::new(contract_addr, &alice.wallet); + let contract_bob = Erc721::new(contract_addr, &bob.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + let token_id = random_token_id(); + + let _ = watch!(contract_alice.mint(alice_addr, token_id))?; + let _ = watch!(contract_alice.approve(bob_addr, token_id))?; + + let Erc721::balanceOfReturn { balance: initial_alice_balance } = + contract_alice.balanceOf(alice_addr).call().await?; + + let Erc721::balanceOfReturn { balance: initial_bob_balance } = + contract_bob.balanceOf(bob_addr).call().await?; + + let receipt = receipt!(contract_bob.safeTransferFrom_1( + alice_addr, + bob_addr, + token_id, + fixed_bytes!("deadbeef").into() + ))?; + + assert!(receipt.emits(Erc721::Transfer { + from: alice_addr, + to: bob_addr, + tokenId: token_id, + })); + + let Erc721::ownerOfReturn { ownerOf } = + contract_alice.ownerOf(token_id).call().await?; + assert_eq!(bob_addr, ownerOf); + + let Erc721::balanceOfReturn { balance: alice_balance } = + contract_alice.balanceOf(alice_addr).call().await?; + + let Erc721::balanceOfReturn { balance: bob_balance } = + contract_bob.balanceOf(bob_addr).call().await?; + + let one = uint!(1_U256); + assert_eq!(initial_alice_balance - one, alice_balance); + assert_eq!(initial_bob_balance + one, bob_balance); + + Ok(()) +} + +#[e2e::test] +async fn safe_transfers_from_with_data_approved_for_all( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract_alice = Erc721::new(contract_addr, &alice.wallet); + let contract_bob = Erc721::new(contract_addr, &bob.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + let token_id = random_token_id(); + + let _ = watch!(contract_alice.mint(alice_addr, token_id))?; + let _ = watch!(contract_alice.setApprovalForAll(bob_addr, true))?; + + let Erc721::balanceOfReturn { balance: initial_alice_balance } = + contract_alice.balanceOf(alice_addr).call().await?; + + let Erc721::balanceOfReturn { balance: initial_bob_balance } = + contract_bob.balanceOf(bob_addr).call().await?; + + let receipt = receipt!(contract_bob.safeTransferFrom_1( + alice_addr, + bob_addr, + token_id, + fixed_bytes!("deadbeef").into() + ))?; + + assert!(receipt.emits(Erc721::Transfer { + from: alice_addr, + to: bob_addr, + tokenId: token_id, + })); + + let Erc721::ownerOfReturn { ownerOf } = + contract_alice.ownerOf(token_id).call().await?; + assert_eq!(bob_addr, ownerOf); + + let Erc721::balanceOfReturn { balance: alice_balance } = + contract_alice.balanceOf(alice_addr).call().await?; + + let Erc721::balanceOfReturn { balance: bob_balance } = + contract_bob.balanceOf(bob_addr).call().await?; + + let one = uint!(1_U256); + assert_eq!(initial_alice_balance - one, alice_balance); + assert_eq!(initial_bob_balance + one, bob_balance); + + Ok(()) +} + +#[e2e::test] +async fn error_when_safe_transfer_with_data_to_invalid_receiver( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let invalid_receiver = Address::ZERO; + let token_id = random_token_id(); + + let _ = watch!(contract.mint(alice_addr, token_id))?; + + let err = send!(contract.safeTransferFrom_1( + alice_addr, + invalid_receiver, + token_id, + fixed_bytes!("deadbeef").into() + )) + .expect_err("should not transfer the token to invalid receiver"); + assert!(err.reverted_with(Erc721::ERC721InvalidReceiver { + receiver: invalid_receiver + })); + + let Erc721::ownerOfReturn { ownerOf } = + contract.ownerOf(token_id).call().await?; + assert_eq!(alice_addr, ownerOf); + + Ok(()) +} + +#[e2e::test] +async fn error_when_safe_transfer_with_data_from_incorrect_owner( + alice: Account, + bob: Account, + dave: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + let dave_addr = dave.address(); + + let token_id = random_token_id(); + + let _ = watch!(contract.mint(alice_addr, token_id))?; + + let err = send!(contract.safeTransferFrom_1( + dave_addr, + bob_addr, + token_id, + fixed_bytes!("deadbeef").into() + )) + .expect_err("should not transfer the token from incorrect owner"); + + assert!(err.reverted_with(Erc721::ERC721IncorrectOwner { + sender: dave_addr, + owner: alice_addr, + tokenId: token_id + })); + + let Erc721::ownerOfReturn { ownerOf } = + contract.ownerOf(token_id).call().await?; + assert_eq!(alice_addr, ownerOf); + + Ok(()) +} + +#[e2e::test] +async fn error_when_safe_transfer_with_data_with_insufficient_approval( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + let token_id = random_token_id(); + let _ = watch!(contract.mint(alice_addr, token_id))?; + + let contract = Erc721::new(contract_addr, &bob.wallet); + + let err = send!(contract.safeTransferFrom_1( + alice_addr, + bob_addr, + token_id, + fixed_bytes!("deadbeef").into() + )) + .expect_err("should not transfer unapproved token"); + + assert!(err.reverted_with(Erc721::ERC721InsufficientApproval { + operator: bob_addr, + tokenId: token_id, + })); + + let Erc721::ownerOfReturn { ownerOf } = + contract.ownerOf(token_id).call().await?; + assert_eq!(alice_addr, ownerOf); + + Ok(()) +} + +#[e2e::test] +async fn error_when_safe_transfer_with_data_nonexistent_token( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let token_id = random_token_id(); + + let err = send!(contract.safeTransferFrom_1( + alice_addr, + bob.address(), + token_id, + fixed_bytes!("deadbeef").into() + )) + .expect_err("should not transfer a non-existent token"); + assert!( + err.reverted_with(Erc721::ERC721NonexistentToken { tokenId: token_id }) + ); + + let err = contract + .ownerOf(token_id) + .call() + .await + .expect_err("should return `ERC721NonexistentToken`"); + + assert!( + err.reverted_with(Erc721::ERC721NonexistentToken { tokenId: token_id }) + ); + + Ok(()) +} + +// TODO: Test reverts & panics on ERC721ReceiverMock for +// `Erc721::safeTransferFrom_1`. + +#[e2e::test] +async fn approves(alice: Account, bob: Account) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + let token_id = random_token_id(); + + let _ = watch!(contract.mint(alice_addr, token_id))?; + + let Erc721::getApprovedReturn { approved } = + contract.getApproved(token_id).call().await?; + assert_eq!(Address::ZERO, approved); + + let receipt = receipt!(contract.approve(bob_addr, token_id))?; + + assert!(receipt.emits(Erc721::Approval { + owner: alice_addr, + approved: bob_addr, + tokenId: token_id, + })); + + let Erc721::getApprovedReturn { approved } = + contract.getApproved(token_id).call().await?; + assert_eq!(bob_addr, approved); + + Ok(()) +} + +#[e2e::test] +async fn error_when_approve_for_nonexistent_token( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let bob_addr = bob.address(); + let token_id = random_token_id(); + + let err = send!(contract.approve(bob_addr, token_id)) + .expect_err("should not approve for a non-existent token"); + + assert!( + err.reverted_with(Erc721::ERC721NonexistentToken { tokenId: token_id }) + ); + + Ok(()) +} + +#[e2e::test] +async fn error_when_approve_by_invalid_approver( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract_alice = Erc721::new(contract_addr, &alice.wallet); + let contract_bob = Erc721::new(contract_addr, &bob.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + let token_id = random_token_id(); + + let _ = watch!(contract_alice.mint(alice_addr, token_id))?; + + let err = send!(contract_bob.approve(bob_addr, token_id)) + .expect_err("should not approve when invalid approver"); + + assert!( + err.reverted_with(Erc721::ERC721InvalidApprover { approver: bob_addr }) + ); + + let Erc721::getApprovedReturn { approved } = + contract_bob.getApproved(token_id).call().await?; + assert_eq!(Address::ZERO, approved); + + Ok(()) +} + +#[e2e::test] +async fn error_when_checking_approved_of_nonexistent_token( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let token_id = random_token_id(); + + let err = contract + .getApproved(token_id) + .call() + .await + .expect_err("should return `ERC721NonexistentToken`"); + + assert!( + err.reverted_with(Erc721::ERC721NonexistentToken { tokenId: token_id }) + ); + Ok(()) +} + +#[e2e::test] +async fn sets_approval_for_all( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + + let approved_value = true; + let receipt = + receipt!(contract.setApprovalForAll(bob_addr, approved_value))?; + + assert!(receipt.emits(Erc721::ApprovalForAll { + owner: alice_addr, + operator: bob_addr, + approved: approved_value, + })); + + let Erc721::isApprovedForAllReturn { approved } = + contract.isApprovedForAll(alice_addr, bob_addr).call().await?; + assert_eq!(approved_value, approved); + + let approved_value = false; + let receipt = + receipt!(contract.setApprovalForAll(bob_addr, approved_value))?; + + assert!(receipt.emits(Erc721::ApprovalForAll { + owner: alice_addr, + operator: bob_addr, + approved: approved_value, + })); + + let Erc721::isApprovedForAllReturn { approved } = + contract.isApprovedForAll(alice_addr, bob_addr).call().await?; + assert_eq!(approved_value, approved); + + Ok(()) +} + +#[e2e::test] +async fn error_when_set_approval_for_all_by_invalid_operator( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let invalid_operator = Address::ZERO; + + let err = send!(contract.setApprovalForAll(invalid_operator, true)) + .expect_err("should return ERC721InvalidOperator"); + + assert!(err.reverted_with(Erc721::ERC721InvalidOperator { + operator: invalid_operator + })); + + Ok(()) +} + +#[e2e::test] +async fn is_approved_for_all_invalid_operator( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let invalid_operator = Address::ZERO; + + let Erc721::isApprovedForAllReturn { approved } = contract + .isApprovedForAll(alice.address(), invalid_operator) + .call() + .await?; + + assert_eq!(false, approved); + + Ok(()) +} + +// ============================================================================ +// Integration Tests: ERC-721 Pausable Extension +// ============================================================================ + +#[e2e::test] +async fn pauses(alice: Account) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let receipt = receipt!(contract.pause())?; + + assert!(receipt.emits(Erc721::Paused { account: alice.address() })); + + let Erc721::pausedReturn { paused } = contract.paused().call().await?; + + assert!(paused); + + let result = contract.whenPaused().call().await; + + assert!(result.is_ok()); + + let err = contract + .whenNotPaused() + .call() + .await + .expect_err("should return `EnforcedPause`"); + + assert!(err.reverted_with(Erc721::EnforcedPause {})); + + Ok(()) +} + +#[e2e::test] +async fn unpauses(alice: Account) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let _ = watch!(contract.pause())?; + + let receipt = receipt!(contract.unpause())?; + + assert!(receipt.emits(Erc721::Unpaused { account: alice.address() })); + + let Erc721::pausedReturn { paused } = contract.paused().call().await?; + + assert_eq!(false, paused); + + let result = contract.whenNotPaused().call().await; + + assert!(result.is_ok()); + + let err = contract + .whenPaused() + .call() + .await + .expect_err("should return `ExpectedPause`"); + + assert!(err.reverted_with(Erc721::ExpectedPause {})); + + Ok(()) +} + +#[e2e::test] +async fn error_when_burn_in_paused_state(alice: Account) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let token_id = random_token_id(); + let _ = watch!(contract.mint(alice_addr, token_id))?; + + let Erc721::balanceOfReturn { balance: initial_balance } = + contract.balanceOf(alice_addr).call().await?; + + let _ = watch!(contract.pause()); + + let err = send!(contract.burn(token_id)) + .expect_err("should return EnforcedPause"); + + assert!(err.reverted_with(Erc721::EnforcedPause {})); + + let Erc721::balanceOfReturn { balance } = + contract.balanceOf(alice_addr).call().await?; + + assert_eq!(initial_balance, balance); + + let Erc721::ownerOfReturn { ownerOf } = + contract.ownerOf(token_id).call().await?; + + assert_eq!(alice_addr, ownerOf); + Ok(()) +} + +#[e2e::test] +async fn error_when_mint_in_paused_state(alice: Account) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let token_id = random_token_id(); + + let _ = watch!(contract.pause()); + + let err = send!(contract.mint(alice_addr, token_id)) + .expect_err("should return EnforcedPause"); + assert!(err.reverted_with(Erc721::EnforcedPause {})); + + let err = contract + .ownerOf(token_id) + .call() + .await + .expect_err("should return ERC721NonexistentToken"); + + assert!( + err.reverted_with(Erc721::ERC721NonexistentToken { tokenId: token_id }) + ); + + let Erc721::balanceOfReturn { balance } = + contract.balanceOf(alice_addr).call().await?; + assert_eq!(uint!(0_U256), balance); + + Ok(()) +} + +#[e2e::test] +async fn error_when_transfer_in_paused_state( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + let token_id = random_token_id(); + let _ = watch!(contract.mint(alice_addr, token_id))?; + + let Erc721::balanceOfReturn { balance: initial_alice_balance } = + contract.balanceOf(alice_addr).call().await?; + + let Erc721::balanceOfReturn { balance: initial_bob_balance } = + contract.balanceOf(bob_addr).call().await?; + + let _ = watch!(contract.pause()); + + let err = send!(contract.transferFrom(alice_addr, bob_addr, token_id)) + .expect_err("should return EnforcedPause"); + assert!(err.reverted_with(Erc721::EnforcedPause {})); + + let Erc721::ownerOfReturn { ownerOf } = + contract.ownerOf(token_id).call().await?; + assert_eq!(alice_addr, ownerOf); + + let Erc721::balanceOfReturn { balance: alice_balance } = + contract.balanceOf(alice_addr).call().await?; + + let Erc721::balanceOfReturn { balance: bob_balance } = + contract.balanceOf(bob_addr).call().await?; + + assert_eq!(initial_alice_balance, alice_balance); + assert_eq!(initial_bob_balance, bob_balance); + + Ok(()) +} + +#[e2e::test] +async fn error_when_safe_transfer_in_paused_state( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + let token_id = random_token_id(); + let _ = watch!(contract.mint(alice_addr, token_id))?; + + let Erc721::balanceOfReturn { balance: initial_alice_balance } = + contract.balanceOf(alice_addr).call().await?; + + let Erc721::balanceOfReturn { balance: initial_bob_balance } = + contract.balanceOf(bob_addr).call().await?; + + let _ = watch!(contract.pause()); + + let err = + send!(contract.safeTransferFrom_0(alice_addr, bob_addr, token_id)) + .expect_err("should return EnforcedPause"); + assert!(err.reverted_with(Erc721::EnforcedPause {})); + + let Erc721::ownerOfReturn { ownerOf } = + contract.ownerOf(token_id).call().await?; + assert_eq!(alice_addr, ownerOf); + + let Erc721::balanceOfReturn { balance: alice_balance } = + contract.balanceOf(alice_addr).call().await?; + + let Erc721::balanceOfReturn { balance: bob_balance } = + contract.balanceOf(bob_addr).call().await?; + + assert_eq!(initial_alice_balance, alice_balance); + assert_eq!(initial_bob_balance, bob_balance); + + Ok(()) +} + +#[e2e::test] +async fn error_when_safe_transfer_with_data_in_paused_state( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + let token_id = random_token_id(); + let _ = watch!(contract.mint(alice_addr, token_id))?; + + let Erc721::balanceOfReturn { balance: initial_alice_balance } = + contract.balanceOf(alice_addr).call().await?; + + let Erc721::balanceOfReturn { balance: initial_bob_balance } = + contract.balanceOf(bob_addr).call().await?; + + let _ = watch!(contract.pause()); + + let err = send!(contract.safeTransferFrom_1( + alice_addr, + bob_addr, + token_id, + fixed_bytes!("deadbeef").into() + )) + .expect_err("should return EnforcedPause"); + assert!(err.reverted_with(Erc721::EnforcedPause {})); + + let Erc721::ownerOfReturn { ownerOf } = + contract.ownerOf(token_id).call().await?; + assert_eq!(alice_addr, ownerOf); + + let Erc721::balanceOfReturn { balance: alice_balance } = + contract.balanceOf(alice_addr).call().await?; + + let Erc721::balanceOfReturn { balance: bob_balance } = + contract.balanceOf(bob_addr).call().await?; + + assert_eq!(initial_alice_balance, alice_balance); + assert_eq!(initial_bob_balance, bob_balance); + + Ok(()) +} + +// ============================================================================ +// Integration Tests: ERC-721 Burnable Extension +// ============================================================================ + +#[e2e::test] +async fn burns(alice: Account) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let token_id = random_token_id(); + let _ = watch!(contract.mint(alice_addr, token_id))?; + + let Erc721::balanceOfReturn { balance: initial_balance } = + contract.balanceOf(alice_addr).call().await?; + + let receipt = receipt!(contract.burn(token_id))?; + + assert!(receipt.emits(Erc721::Transfer { + from: alice_addr, + to: Address::ZERO, + tokenId: token_id, + })); + + let Erc721::balanceOfReturn { balance } = + contract.balanceOf(alice_addr).call().await?; + + assert_eq!(initial_balance - uint!(1_U256), balance); + + let err = contract + .ownerOf(token_id) + .call() + .await + .expect_err("should return `ERC721NonexistentToken`"); + + assert!( + err.reverted_with(Erc721::ERC721NonexistentToken { tokenId: token_id }) + ); + + Ok(()) +} + +#[e2e::test] +async fn burns_approved_token( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract_alice = Erc721::new(contract_addr, &alice.wallet); + let contract_bob = Erc721::new(contract_addr, &bob.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + let token_id = random_token_id(); + + let _ = watch!(contract_alice.mint(alice_addr, token_id))?; + let _ = watch!(contract_alice.approve(bob_addr, token_id))?; + + let Erc721::balanceOfReturn { balance: initial_balance } = + contract_alice.balanceOf(alice_addr).call().await?; + + let receipt = receipt!(contract_bob.burn(token_id))?; + + assert!(receipt.emits(Erc721::Transfer { + from: alice_addr, + to: Address::ZERO, + tokenId: token_id, + })); + + let Erc721::balanceOfReturn { balance } = + contract_alice.balanceOf(alice_addr).call().await?; + + assert_eq!(initial_balance - uint!(1_U256), balance); + + let err = contract_bob + .ownerOf(token_id) + .call() + .await + .expect_err("should return `ERC721NonexistentToken`"); + + assert!( + err.reverted_with(Erc721::ERC721NonexistentToken { tokenId: token_id }) + ); + + Ok(()) +} + +#[e2e::test] +async fn burns_approved_for_all( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract_alice = Erc721::new(contract_addr, &alice.wallet); + let contract_bob = Erc721::new(contract_addr, &bob.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + let token_id = random_token_id(); + + let _ = watch!(contract_alice.mint(alice_addr, token_id))?; + let _ = watch!(contract_alice.setApprovalForAll(bob_addr, true))?; + + let Erc721::balanceOfReturn { balance: initial_balance } = + contract_alice.balanceOf(alice_addr).call().await?; + + let receipt = receipt!(contract_bob.burn(token_id))?; + + assert!(receipt.emits(Erc721::Transfer { + from: alice_addr, + to: Address::ZERO, + tokenId: token_id, + })); + + let Erc721::balanceOfReturn { balance } = + contract_alice.balanceOf(alice_addr).call().await?; + + assert_eq!(initial_balance - uint!(1_U256), balance); + + let err = contract_bob + .ownerOf(token_id) + .call() + .await + .expect_err("should return `ERC721NonexistentToken`"); + + assert!( + err.reverted_with(Erc721::ERC721NonexistentToken { tokenId: token_id }) + ); + + Ok(()) +} + +#[e2e::test] +async fn error_when_burn_with_insufficient_approval( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + let token_id = random_token_id(); + let _ = watch!(contract.mint(alice_addr, token_id))?; + + let Erc721::balanceOfReturn { balance: initial_balance } = + contract.balanceOf(alice_addr).call().await?; + + let contract = Erc721::new(contract_addr, &bob.wallet); + let err = send!(contract.burn(token_id)) + .expect_err("should not burn unapproved token"); + + assert!(err.reverted_with(Erc721::ERC721InsufficientApproval { + operator: bob_addr, + tokenId: token_id, + })); + + let Erc721::balanceOfReturn { balance } = + contract.balanceOf(alice_addr).call().await?; + + assert_eq!(initial_balance, balance); + + Ok(()) +} + +#[e2e::test] +async fn error_when_burn_nonexistent_token(alice: Account) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let token_id = random_token_id(); + + let err = send!(contract.burn(token_id)) + .expect_err("should not burn a non-existent token"); + assert!( + err.reverted_with(Erc721::ERC721NonexistentToken { tokenId: token_id }) + ); + Ok(()) +} + +// ============================================================================ +// Integration Tests: ERC-721 Enumerable Extension +// ============================================================================ + +#[e2e::test] +async fn totally_supply_works(alice: Account) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + + let token_1 = random_token_id(); + let _ = watch!(contract.mint(alice_addr, token_1))?; + + let token_2 = random_token_id(); + let _ = watch!(contract.mint(alice_addr, token_2))?; + + let Erc721::totalSupplyReturn { totalSupply } = + contract.totalSupply().call().await?; + + assert_eq!(uint!(2_U256), totalSupply); + + Ok(()) +} + +#[e2e::test] +async fn error_when_checking_token_of_owner_by_index_out_of_bound( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + + let _ = watch!(contract.mint(alice_addr, random_token_id()))?; + let _ = watch!(contract.mint(alice_addr, random_token_id()))?; + + let index_out_of_bound = uint!(2_U256); + + let err = contract + .tokenOfOwnerByIndex(alice_addr, index_out_of_bound) + .call() + .await + .expect_err("should return `ERC721OutOfBoundsIndex`"); + + assert!(err.reverted_with(Erc721::ERC721OutOfBoundsIndex { + owner: alice_addr, + index: index_out_of_bound + })); + + Ok(()) +} + +#[e2e::test] +async fn error_when_checking_token_of_owner_by_index_account_has_no_tokens( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + + let index = uint!(0_U256); + + let err = contract + .tokenOfOwnerByIndex(alice_addr, index) + .call() + .await + .expect_err("should return `ERC721OutOfBoundsIndex`"); + + assert!(err.reverted_with(Erc721::ERC721OutOfBoundsIndex { + owner: alice_addr, + index + })); + + Ok(()) +} + +#[e2e::test] +async fn token_of_owner_by_index_works(alice: Account) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + + let token_0 = random_token_id(); + let _ = watch!(contract.mint(alice_addr, token_0))?; + + let token_1 = random_token_id(); + let _ = watch!(contract.mint(alice_addr, token_1))?; + + let Erc721::tokenOfOwnerByIndexReturn { tokenId } = + contract.tokenOfOwnerByIndex(alice_addr, uint!(0_U256)).call().await?; + assert_eq!(token_0, tokenId); + + let Erc721::tokenOfOwnerByIndexReturn { tokenId } = + contract.tokenOfOwnerByIndex(alice_addr, uint!(1_U256)).call().await?; + assert_eq!(token_1, tokenId); + + Ok(()) +} + +#[e2e::test] +async fn token_of_owner_by_index_after_transfer_to_another_account( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + + let token_0 = random_token_id(); + let _ = watch!(contract.mint(alice_addr, token_0))?; + + let token_1 = random_token_id(); + let _ = watch!(contract.mint(alice_addr, token_1))?; + + let _ = watch!(contract.transferFrom(alice_addr, bob_addr, token_1))?; + let _ = watch!(contract.transferFrom(alice_addr, bob_addr, token_0))?; + + // should be in reverse order + let index = uint!(0_U256); + let Erc721::tokenOfOwnerByIndexReturn { tokenId } = + contract.tokenOfOwnerByIndex(bob_addr, index).call().await?; + assert_eq!(token_1, tokenId); + let err = contract + .tokenOfOwnerByIndex(alice_addr, index) + .call() + .await + .expect_err("should return `ERC721OutOfBoundsIndex`"); + assert!(err.reverted_with(Erc721::ERC721OutOfBoundsIndex { + owner: alice_addr, + index + })); + + let index = uint!(1_U256); + let Erc721::tokenOfOwnerByIndexReturn { tokenId } = + contract.tokenOfOwnerByIndex(bob_addr, index).call().await?; + assert_eq!(token_0, tokenId); + let err = contract + .tokenOfOwnerByIndex(alice_addr, index) + .call() + .await + .expect_err("should return `ERC721OutOfBoundsIndex`"); + assert!(err.reverted_with(Erc721::ERC721OutOfBoundsIndex { + owner: alice_addr, + index + })); + + let Erc721::totalSupplyReturn { totalSupply } = + contract.totalSupply().call().await?; + + assert_eq!(uint!(2_U256), totalSupply); + + Ok(()) +} + +#[e2e::test] +async fn error_when_checking_token_by_index_account_has_no_tokens( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let index = uint!(0_U256); + + let err = contract + .tokenByIndex(index) + .call() + .await + .expect_err("should return `ERC721OutOfBoundsIndex`"); + + assert!(err.reverted_with(Erc721::ERC721OutOfBoundsIndex { + owner: Address::ZERO, + index + })); + + Ok(()) +} + +#[e2e::test] +async fn error_when_checking_token_by_index_out_of_bound( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + + let _ = watch!(contract.mint(alice_addr, random_token_id()))?; + let _ = watch!(contract.mint(alice_addr, random_token_id()))?; + + let index_out_of_bound = uint!(2_U256); + + let err = contract + .tokenByIndex(index_out_of_bound) + .call() + .await + .expect_err("should return `ERC721OutOfBoundsIndex`"); + + assert!(err.reverted_with(Erc721::ERC721OutOfBoundsIndex { + owner: Address::ZERO, + index: index_out_of_bound + })); + + Ok(()) +} + +#[e2e::test] +async fn token_by_index_works(alice: Account) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + + let token_0 = random_token_id(); + let _ = watch!(contract.mint(alice_addr, token_0))?; + + let token_1 = random_token_id(); + let _ = watch!(contract.mint(alice_addr, token_1))?; + + let Erc721::tokenByIndexReturn { tokenId } = + contract.tokenByIndex(uint!(0_U256)).call().await?; + assert_eq!(token_0, tokenId); + + let Erc721::tokenByIndexReturn { tokenId } = + contract.tokenByIndex(uint!(1_U256)).call().await?; + assert_eq!(token_1, tokenId); + + Ok(()) +} + +#[e2e::test] +async fn token_by_index_after_burn(alice: Account) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + + let token_0 = random_token_id(); + let _ = watch!(contract.mint(alice_addr, token_0))?; + + let token_1 = random_token_id(); + let _ = watch!(contract.mint(alice_addr, token_1))?; + + let _ = watch!(contract.burn(token_1))?; + + let Erc721::tokenByIndexReturn { tokenId } = + contract.tokenByIndex(uint!(0_U256)).call().await?; + assert_eq!(token_0, tokenId); + + let index_of_burnt_token = uint!(1_U256); + let err = contract + .tokenByIndex(index_of_burnt_token) + .call() + .await + .expect_err("should return `ERC721OutOfBoundsIndex`"); + + assert!(err.reverted_with(Erc721::ERC721OutOfBoundsIndex { + owner: Address::ZERO, + index: index_of_burnt_token + })); + + let Erc721::totalSupplyReturn { totalSupply } = + contract.totalSupply().call().await?; + + assert_eq!(uint!(1_U256), totalSupply); + + Ok(()) +} + +#[e2e::test] +async fn token_by_index_after_burn_and_some_mints( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + + let token_0 = random_token_id(); + let _ = watch!(contract.mint(alice_addr, token_0))?; + + let token_1 = random_token_id(); + let _ = watch!(contract.mint(alice_addr, token_1))?; + + let _ = watch!(contract.burn(token_1))?; + + let token_2 = random_token_id(); + let _ = watch!(contract.mint(alice_addr, token_2))?; + + let token_3 = random_token_id(); + let _ = watch!(contract.mint(alice_addr, token_3))?; + + let Erc721::tokenByIndexReturn { tokenId } = + contract.tokenByIndex(uint!(0_U256)).call().await?; + assert_eq!(token_0, tokenId); + + let Erc721::tokenByIndexReturn { tokenId } = + contract.tokenByIndex(uint!(1_U256)).call().await?; + assert_eq!(token_2, tokenId); + + let Erc721::tokenByIndexReturn { tokenId } = + contract.tokenByIndex(uint!(2_U256)).call().await?; + assert_eq!(token_3, tokenId); + Ok(()) } diff --git a/examples/erc721/tests/mock/mod.rs b/examples/erc721/tests/mock/mod.rs new file mode 100644 index 00000000..4c0db7ea --- /dev/null +++ b/examples/erc721/tests/mock/mod.rs @@ -0,0 +1 @@ +pub mod receiver; diff --git a/examples/erc721/tests/mock/receiver.rs b/examples/erc721/tests/mock/receiver.rs new file mode 100644 index 00000000..f759227e --- /dev/null +++ b/examples/erc721/tests/mock/receiver.rs @@ -0,0 +1,67 @@ +#![allow(dead_code)] +#![cfg(feature = "e2e")] +use alloy::{ + primitives::{fixed_bytes, Address}, + sol, +}; +use e2e::Wallet; + +sol! { + #[allow(missing_docs)] + // Built with Remix IDE; solc v0.8.21+commit.d9974bed + #[sol(rpc, bytecode="60c060405234801561000f575f80fd5b5060405161093438038061093483398181016040528101906100319190610126565b817bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166080817bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19168152505080600481111561008a57610089610164565b5b60a081600481111561009f5761009e610164565b5b815250505050610191565b5f80fd5b5f7fffffffff0000000000000000000000000000000000000000000000000000000082169050919050565b6100e2816100ae565b81146100ec575f80fd5b50565b5f815190506100fd816100d9565b92915050565b6005811061010f575f80fd5b50565b5f8151905061012081610103565b92915050565b5f806040838503121561013c5761013b6100aa565b5b5f610149858286016100ef565b925050602061015a85828601610112565b9150509250929050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602160045260245ffd5b60805160a0516107686101cc5f395f818160740152818160c40152818161014b01526101f301525f8181610183015261027801526107685ff3fe608060405234801561000f575f80fd5b5060043610610029575f3560e01c8063150b7a021461002d575b5f80fd5b6100476004803603810190610042919061047b565b61005d565b6040516100549190610535565b60405180910390f35b5f600160048111156100725761007161054e565b5b7f000000000000000000000000000000000000000000000000000000000000000060048111156100a5576100a461054e565b5b036100ae575f80fd5b600260048111156100c2576100c161054e565b5b7f000000000000000000000000000000000000000000000000000000000000000060048111156100f5576100f461054e565b5b03610135576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161012c906105d5565b60405180910390fd5b600360048111156101495761014861054e565b5b7f0000000000000000000000000000000000000000000000000000000000000000600481111561017c5761017b61054e565b5b036101de577f00000000000000000000000000000000000000000000000000000000000000006040517f66435bc00000000000000000000000000000000000000000000000000000000081526004016101d59190610535565b60405180910390fd5b6004808111156101f1576101f061054e565b5b7f000000000000000000000000000000000000000000000000000000000000000060048111156102245761022361054e565b5b0361023a575f805f6102369190610620565b9050505b7ed9411ae77b2bacabe5cbe62a2abdbeb78992a0182c6f3c83e0029c7615d6b68585858560405161026e94939291906106e8565b60405180910390a17f00000000000000000000000000000000000000000000000000000000000000009050949350505050565b5f604051905090565b5f80fd5b5f80fd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6102db826102b2565b9050919050565b6102eb816102d1565b81146102f5575f80fd5b50565b5f81359050610306816102e2565b92915050565b5f819050919050565b61031e8161030c565b8114610328575f80fd5b50565b5f8135905061033981610315565b92915050565b5f80fd5b5f80fd5b5f601f19601f8301169050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b61038d82610347565b810181811067ffffffffffffffff821117156103ac576103ab610357565b5b80604052505050565b5f6103be6102a1565b90506103ca8282610384565b919050565b5f67ffffffffffffffff8211156103e9576103e8610357565b5b6103f282610347565b9050602081019050919050565b828183375f83830152505050565b5f61041f61041a846103cf565b6103b5565b90508281526020810184848401111561043b5761043a610343565b5b6104468482856103ff565b509392505050565b5f82601f8301126104625761046161033f565b5b813561047284826020860161040d565b91505092915050565b5f805f8060808587031215610493576104926102aa565b5b5f6104a0878288016102f8565b94505060206104b1878288016102f8565b93505060406104c28782880161032b565b925050606085013567ffffffffffffffff8111156104e3576104e26102ae565b5b6104ef8782880161044e565b91505092959194509250565b5f7fffffffff0000000000000000000000000000000000000000000000000000000082169050919050565b61052f816104fb565b82525050565b5f6020820190506105485f830184610526565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602160045260245ffd5b5f82825260208201905092915050565b7f45524337323152656365697665724d6f636b3a20726576657274696e670000005f82015250565b5f6105bf601d8361057b565b91506105ca8261058b565b602082019050919050565b5f6020820190508181035f8301526105ec816105b3565b9050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601260045260245ffd5b5f61062a8261030c565b91506106358361030c565b925082610645576106446105f3565b5b828204905092915050565b610659816102d1565b82525050565b6106688161030c565b82525050565b5f81519050919050565b5f82825260208201905092915050565b5f5b838110156106a557808201518184015260208101905061068a565b5f8484015250505050565b5f6106ba8261066e565b6106c48185610678565b93506106d4818560208601610688565b6106dd81610347565b840191505092915050565b5f6080820190506106fb5f830187610650565b6107086020830186610650565b610715604083018561065f565b818103606083015261072781846106b0565b90509594505050505056fea2646970667358221220b3015e4cfe9292167a6c435826958084eb394c9609b38940a0899ffb42eaa2df64736f6c63430008150033")] + contract ERC721ReceiverMock is IERC721Receiver { + enum RevertType { + None, + RevertWithoutMessage, + RevertWithMessage, + RevertWithCustomError, + Panic + } + + bytes4 private immutable _retval; + RevertType private immutable _error; + + #[derive(Debug, PartialEq)] + event Received(address operator, address from, uint256 tokenId, bytes data); + + error CustomError(bytes4); + + constructor(bytes4 retval, RevertType error) { + _retval = retval; + _error = error; + } + + function onERC721Received( + address operator, + address from, + uint256 tokenId, + bytes memory data + ) public returns (bytes4) { + if (_error == RevertType.RevertWithoutMessage) { + revert(); + } else if (_error == RevertType.RevertWithMessage) { + revert("ERC721ReceiverMock: reverting"); + } else if (_error == RevertType.RevertWithCustomError) { + revert CustomError(_retval); + } else if (_error == RevertType.Panic) { + uint256 a = uint256(0) / uint256(0); + a; + } + + emit Received(operator, from, tokenId, data); + return _retval; + } + } +} + +pub async fn deploy( + wallet: &Wallet, + error: ERC721ReceiverMock::RevertType, +) -> eyre::Result
{ + let retval = fixed_bytes!("150b7a02"); + + // Deploy the contract. + let contract = ERC721ReceiverMock::deploy(wallet, retval, error).await?; + Ok(*contract.address()) +} diff --git a/examples/ownable/tests/abi.rs b/examples/ownable/tests/abi/mod.rs similarity index 100% rename from examples/ownable/tests/abi.rs rename to examples/ownable/tests/abi/mod.rs diff --git a/examples/ownable/tests/ownable.rs b/examples/ownable/tests/ownable.rs index 855d841f..342ea545 100644 --- a/examples/ownable/tests/ownable.rs +++ b/examples/ownable/tests/ownable.rs @@ -1,6 +1,6 @@ #![cfg(feature = "e2e")] -use abi::Ownable::OwnershipTransferred; +use abi::{Ownable, Ownable::OwnershipTransferred}; use alloy::{ primitives::Address, providers::Provider, @@ -11,8 +11,6 @@ use alloy::{ use e2e::{receipt, send, Account, EventExt, Revert}; use eyre::Result; -use crate::abi::Ownable; - mod abi; sol!("src/constructor.sol"); From 73a1289fdbbd2e49d57a65600990d09e88431dd4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Jul 2024 08:00:45 +0200 Subject: [PATCH 45/95] build(deps): bump crate-ci/typos from 1.23.1 to 1.23.2 (#192) [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=crate-ci/typos&package-manager=github_actions&previous-version=1.23.1&new-version=1.23.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 0e6d71b9..05403935 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -110,4 +110,4 @@ jobs: - name: Checkout Actions Repository uses: actions/checkout@v4 - name: Check spelling of files in the workspace - uses: crate-ci/typos@v1.23.1 + uses: crate-ci/typos@v1.23.2 From 1e174c69ae82b435b94e9ac9c12f0098f47a04ca Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Thu, 11 Jul 2024 15:53:10 +0400 Subject: [PATCH 46/95] add init function --- examples/erc721-consecutive/tests/erc721.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/erc721-consecutive/tests/erc721.rs b/examples/erc721-consecutive/tests/erc721.rs index 9ee6db18..be458df0 100644 --- a/examples/erc721-consecutive/tests/erc721.rs +++ b/examples/erc721-consecutive/tests/erc721.rs @@ -22,7 +22,7 @@ async fn constructs(alice: Account) -> eyre::Result<()> { let contract = Erc721::new(contract_addr, &alice.wallet); let alice_addr = alice.address(); - let res = contract.mintConsecutive(alice_addr, 10_u128).call().await?; + let res = contract.mi(alice_addr, 10_u128).call().await?; todo!(); Ok(()) From 0d234d9ff6bd80a4c664c0d38cd14f25fedc9169 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Thu, 11 Jul 2024 16:03:45 +0400 Subject: [PATCH 47/95] ++ --- .../erc721-consecutive/src/constructor.sol | 19 +++++++++++++++++++ examples/erc721-consecutive/tests/erc721.rs | 9 ++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 examples/erc721-consecutive/src/constructor.sol diff --git a/examples/erc721-consecutive/src/constructor.sol b/examples/erc721-consecutive/src/constructor.sol new file mode 100644 index 00000000..48e4431b --- /dev/null +++ b/examples/erc721-consecutive/src/constructor.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +contract Erc721Example { + 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; + + mapping(address owner => mapping(uint256 index => uint256)) + private _ownedTokens; + mapping(uint256 tokenId => uint256) private _ownedTokensIndex; + uint256[] private _allTokens; + mapping(uint256 tokenId => uint256) private _allTokensIndex; + + constructor() { + } +} diff --git a/examples/erc721-consecutive/tests/erc721.rs b/examples/erc721-consecutive/tests/erc721.rs index be458df0..18bda941 100644 --- a/examples/erc721-consecutive/tests/erc721.rs +++ b/examples/erc721-consecutive/tests/erc721.rs @@ -1,6 +1,7 @@ #![cfg(feature = "e2e")] use alloy::primitives::{Address, U256}; +use alloy_primitives::uint; use e2e::{Account, EventExt, Revert}; use crate::abi::Erc721; @@ -22,8 +23,10 @@ async fn constructs(alice: Account) -> eyre::Result<()> { let contract = Erc721::new(contract_addr, &alice.wallet); let alice_addr = alice.address(); - let res = contract.mi(alice_addr, 10_u128).call().await?; - - todo!(); + let receivers = vec![alice_addr]; + let amounts = vec![uint!(10_U256)]; + let res = contract.init(receivers, amounts).call().await?; Ok(()) } + +// TODO#q: construct batches From af523db2516a2eb6bdab58a737e3ff3b53775193 Mon Sep 17 00:00:00 2001 From: Developer Uche <69772615+developeruche@users.noreply.github.com> Date: Thu, 11 Jul 2024 14:56:05 +0100 Subject: [PATCH 48/95] feat: Nonces implementation (#187) Resolves #182 --- contracts/src/utils/mod.rs | 1 + contracts/src/utils/nonces.rs | 157 ++++++++++++++++++++++++++++++++++ lib/crypto/src/merkle.rs | 22 +++-- 3 files changed, 168 insertions(+), 12 deletions(-) create mode 100644 contracts/src/utils/nonces.rs diff --git a/contracts/src/utils/mod.rs b/contracts/src/utils/mod.rs index 256bee20..8934fe58 100644 --- a/contracts/src/utils/mod.rs +++ b/contracts/src/utils/mod.rs @@ -1,6 +1,7 @@ //! Common Smart Contracts utilities. pub mod math; pub mod metadata; +pub mod nonces; pub mod pausable; pub mod structs; diff --git a/contracts/src/utils/nonces.rs b/contracts/src/utils/nonces.rs new file mode 100644 index 00000000..d10a6d3b --- /dev/null +++ b/contracts/src/utils/nonces.rs @@ -0,0 +1,157 @@ +//! Nonces Contract +//! +//! Contract module which provides functionalities for tracking nonces for +//! addresses. +//! +//! Note: Nonce will only increment. + +use alloy_primitives::{uint, Address, U256}; +use alloy_sol_types::sol; +use stylus_proc::{external, sol_storage, SolidityError}; + +const ONE: U256 = uint!(1_U256); + +sol! { + /// The nonce used for an `account` is not the expected current nonce. + #[derive(Debug)] + #[allow(missing_docs)] + error InvalidAccountNonce(address account, uint256 currentNonce); +} + +/// A Nonces error. +#[derive(SolidityError, Debug)] +pub enum Error { + /// The nonce used for an `account` is not the expected current nonce. + InvalidAccountNonce(InvalidAccountNonce), +} + +sol_storage! { + /// State of a Nonces Contract. + #[cfg_attr(all(test, feature = "std"), derive(motsu::DefaultStorageLayout))] + pub struct Nonces { + /// Mapping from address to its nonce. + mapping(address => uint256) _nonces; + } +} + +#[external] +impl Nonces { + /// Returns the unused nonce for the given `account`. + /// + /// # Arguments + /// + /// * `&self` - Read access to the contract's state. + /// * `owner` - The address for which to return the nonce. + fn nonce(&self, owner: Address) -> U256 { + self._nonces.get(owner) + } + + /// Consumes a nonce for the given `account`. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `owner` - The address for which to consume the nonce. + /// + /// /// # Panics + /// + /// This function will panic if the nonce for the given `owner` has reached + /// the maximum value representable by `U256`, causing the `checked_add` + /// method to return `None`. + fn use_nonce(&mut self, owner: Address) -> U256 { + let nonce = self._nonces.get(owner); + self._nonces + .setter(owner) + .set(unsafe { nonce.checked_add(ONE).unwrap_unchecked() }); + + nonce + } + + /// Same as `use_nonce` but checking that the `nonce` is the next valid for + /// the owner. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `owner` - The address for which to consume the nonce. + /// * `nonce` - The nonce to consume. + /// + /// # Panics + /// + /// This function will panic if the nonce for the given `owner` has reached + /// the maximum value representable by `U256`, causing the `checked_add` + /// method to return `None`. + /// + /// # Errors + /// + /// Returns an error if the `nonce` is not the next valid nonce for the + /// owner. + fn use_checked_nonce( + &mut self, + owner: Address, + nonce: U256, + ) -> Result<(), Error> { + let current_nonce = self._nonces.get(owner); + + if nonce != current_nonce { + return Err(Error::InvalidAccountNonce(InvalidAccountNonce { + account: owner, + currentNonce: current_nonce, + })); + } + + self._nonces + .setter(owner) + .set(unsafe { nonce.checked_add(ONE).unwrap_unchecked() }); + + Ok(()) + } +} + +#[cfg(all(test, feature = "std"))] +mod tests { + use alloy_primitives::{address, uint, U256}; + + use super::ONE; + use crate::utils::nonces::{Error, Nonces}; + + #[motsu::test] + fn test_initiate_nonce(contract: Nonces) { + let owner = address!("d8da6bf26964af9d7eed9e03e53415d37aa96045"); + + assert_eq!(contract.nonce(owner), U256::ZERO); + } + + #[motsu::test] + fn test_use_nonce(contract: Nonces) { + let owner = address!("d8da6bf26964af9d7eed9e03e53415d37aa96045"); + + let use_nonce = contract.use_nonce(owner); + assert_eq!(use_nonce, U256::ZERO); + + let nonce = contract.nonce(owner); + assert_eq!(nonce, ONE); + } + + #[motsu::test] + fn test_use_checked_nonce(contract: Nonces) { + let owner = address!("d8da6bf26964af9d7eed9e03e53415d37aa96045"); + + let use_checked_nonce = contract.use_checked_nonce(owner, U256::ZERO); + assert!(use_checked_nonce.is_ok()); + + let nonce = contract.nonce(owner); + assert_eq!(nonce, ONE); + } + + #[motsu::test] + fn test_use_checked_nonce_invalid_nonce(contract: Nonces) { + let owner = address!("d8da6bf26964af9d7eed9e03e53415d37aa96045"); + + let use_checked_nonce = contract.use_checked_nonce(owner, ONE); + assert!(matches!( + use_checked_nonce, + Err(Error::InvalidAccountNonce(_)) + )); + } +} diff --git a/lib/crypto/src/merkle.rs b/lib/crypto/src/merkle.rs index 653af41e..90e71a71 100644 --- a/lib/crypto/src/merkle.rs +++ b/lib/crypto/src/merkle.rs @@ -79,9 +79,9 @@ impl Verifier { /// CAUTION: Not all Merkle trees admit multiproofs. To use multiproofs, /// it is sufficient to ensure that: /// - The tree is complete (but not necessarily perfect). - /// - The leaves to be proven are in the opposite order they appear in - /// the tree (i.e., as seen from right to left starting at the deepest - /// layer and continuing at the next layer). + /// - The leaves to be proven are in the opposite order they appear in the + /// tree (i.e., as seen from right to left starting at the deepest layer + /// and continuing at the next layer). /// /// NOTE: This implementation is *not* equivalent to it's Solidity /// counterpart. In Rust, access to uninitialized memory panics, which @@ -93,11 +93,10 @@ impl Verifier { /// /// * `proof` - A slice of hashes that constitute the merkle proof. /// * `proof_flags` - A slice of booleans that determine whether to hash - /// leaves - /// or the proof. + /// leaves or the proof. /// * `root` - The root of the merkle tree, in bytes. /// * `leaves` - A slice of hashes that constitute the leaves of the merkle - /// tree to be proven, each leaf in bytes. + /// tree to be proven, each leaf in bytes. /// /// # Errors /// @@ -214,9 +213,9 @@ where /// CAUTION: Not all Merkle trees admit multiproofs. To use multiproofs, /// it is sufficient to ensure that: /// - The tree is complete (but not necessarily perfect). - /// - The leaves to be proven are in the opposite order they appear in - /// the tree (i.e., as seen from right to left starting at the deepest - /// layer and continuing at the next layer). + /// - The leaves to be proven are in the opposite order they appear in the + /// tree (i.e., as seen from right to left starting at the deepest layer + /// and continuing at the next layer). /// /// NOTE: This implementation is *not* equivalent to it's Solidity /// counterpart. In Rust, access to uninitialized memory panics, which @@ -228,11 +227,10 @@ where /// /// * `proof` - A slice of hashes that constitute the merkle proof. /// * `proof_flags` - A slice of booleans that determine whether to hash - /// leaves - /// or the proof. + /// leaves or the proof. /// * `root` - The root of the merkle tree, in bytes. /// * `leaves` - A slice of hashes that constitute the leaves of the merkle - /// tree to be proven, each leaf in bytes. + /// tree to be proven, each leaf in bytes. /// * `builder` - A [`BuildHasher`] that represents a hashing algorithm. /// /// # Errors From a01c05429c51ca79595e39770ac4fd4c4ace7b73 Mon Sep 17 00:00:00 2001 From: Daniel Bigos Date: Thu, 11 Jul 2024 18:49:17 +0200 Subject: [PATCH 49/95] fix: solve clippy warnings (#194) Resolves #193 --- lib/motsu/src/context.rs | 1 + lib/motsu/src/storage.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/motsu/src/context.rs b/lib/motsu/src/context.rs index e9afa583..aeb83d5f 100644 --- a/lib/motsu/src/context.rs +++ b/lib/motsu/src/context.rs @@ -21,6 +21,7 @@ pub fn acquire_storage() -> MutexGuard<'static, ()> { } /// Decorates a closure by running it with exclusive access to storage. +#[allow(clippy::module_name_repetitions)] pub fn with_context(closure: impl FnOnce(&mut C)) { let _lock = acquire_storage(); let mut contract = C::default(); diff --git a/lib/motsu/src/storage.rs b/lib/motsu/src/storage.rs index 4dde3a43..4e8d23d4 100644 --- a/lib/motsu/src/storage.rs +++ b/lib/motsu/src/storage.rs @@ -26,6 +26,7 @@ pub(crate) unsafe fn write_bytes32(key: *mut u8, val: Bytes32) { /// # Panics /// /// May panic if the storage lock is already held by the current thread. +#[allow(clippy::module_name_repetitions)] pub fn reset_storage() { STORAGE.lock().unwrap().clear(); } From fcfceafe5a9c21ab62ee392fe59f3c9b19239b75 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Fri, 12 Jul 2024 13:22:55 +0400 Subject: [PATCH 50/95] update docs --- .../token/erc721/extensions/consecutive.rs | 104 ++++++++++++------ examples/erc721-consecutive/src/lib.rs | 5 +- .../{erc721.rs => erc721-consecutive.rs} | 20 +++- 3 files changed, 86 insertions(+), 43 deletions(-) rename examples/erc721-consecutive/tests/{erc721.rs => erc721-consecutive.rs} (51%) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index 3faa3c35..0100cbd1 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -1,4 +1,5 @@ use alloc::vec; + use alloy_primitives::{uint, Address, U128, U256}; use alloy_sol_types::sol; use stylus_proc::{external, sol_storage, SolidityError}; @@ -37,6 +38,11 @@ sol_storage! { sol! { /// Emitted when the tokens from `fromTokenId` to `toTokenId` are transferred from `fromAddress` to `toAddress`. + /// + /// * `fromTokenId` - First token being transfered. + /// * `toTokenId` - Last token being transfered. + /// * `fromAddress` - Address from which tokens will be transferred. + /// * `toAddress` - Address where the tokens will be transferred to. event ConsecutiveTransfer( uint256 indexed fromTokenId, uint256 toTokenId, @@ -53,7 +59,7 @@ sol! { #[allow(missing_docs)] error ERC721ForbiddenBatchMint(); - /// Exceeds the max amount of mints per batch. + /// Exceeds the max number of mints per batch. #[derive(Debug)] #[allow(missing_docs)] error ERC721ExceededMaxBatchMint(uint256 batchSize, uint256 maxBatch); @@ -71,7 +77,9 @@ sol! { #[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 [`IERC721::Transfer`] event outside of @@ -97,6 +105,8 @@ impl MethodError for checkpoints::Error { } } +// TODO#q: have these constants generic + // 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. @@ -107,13 +117,15 @@ const FIRST_CONSECUTIVE_ID: U96 = uint!(0_U96); /// Consecutive extension related implementation: impl Erc721Consecutive { - /// Override that checks the sequential ownership structure for tokens that - /// have been minted as part of a batch, and not yet transferred. + /// Override of [`Erc721::_owner_of_inner`] that checks the sequential + /// ownership structure for tokens that have been minted as part of a + /// batch, and not yet transferred. pub fn _owner_of_inner(&self, token_id: U256) -> Address { let owner = self.__owner_of_inner(token_id); // If token is owned by the core, or beyond consecutive range, return // base value if owner != Address::ZERO + || token_id > U256::from(U96::MAX) || token_id > U256::from(U96::MAX) || token_id < U256::from(FIRST_CONSECUTIVE_ID) { @@ -130,33 +142,48 @@ impl Erc721Consecutive { } } - // Mint a batch of tokens of length `batchSize` for `to`. Returns the token - // id of the first token minted in the batch; if `batchSize` is 0, - // returns the number of consecutive ids minted so far. - // - // Requirements: - // - // - `batchSize` must not be greater than [`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 `onERC721Received` on the receiver. - // - // Emits a [`ConsecutiveTransfer`] event. - pub fn mint_consecutive( + /// Mint a batch of tokens of length `batch_size` for `to`. Returns the + /// token id of the first token minted in the batch; if `batchSize` is + /// 0, returns the number of consecutive ids minted so far. + /// + /// Requirements: + /// + /// - `batchSize` must not be greater than [`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 `onERC721Received` 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`] error [`Erc721Error::InvalidReceiver`] is + /// returned. + /// If batch size exceeds [`MAX_BATCH_SIZE`] error + /// [`Error::ERC721ExceededMaxBatchMint`] is returned. + /// + /// # Events + /// + /// Emits a [`ConsecutiveTransfer`] event. + pub fn _mint_consecutive( &mut self, to: Address, batch_size: U96, - ) -> Result { - let next = self.next_consecutive_id(); + ) -> Result { + let next = self._next_consecutive_id(); if batch_size > U96::ZERO { - //TODO#q: check address of this and revert with ERC721ForbiddenBatchMint - + //TODO#q: check address of this and revert with + // ERC721ForbiddenBatchMint + if to.is_zero() { return Err(Erc721Error::InvalidReceiver( ERC721InvalidReceiver { receiver: Address::ZERO }, @@ -184,14 +211,15 @@ impl Erc721Consecutive { toAddress: to, }); }; - Ok(next.to()) + Ok(next) } - /// Override version that restricts normal minting to after construction. + /// Override of [`Erc721::_update`] that restricts normal minting to after + /// construction. /// /// WARNING: Using [`Erc721Consecutive`] prevents minting during - /// construction in favor of [`Erc721Consecutive::mint_consecutive`]. - /// After construction,[`Erc721Consecutive::mint_consecutive`] is no + /// construction in favor of [`Erc721Consecutive::_mint_consecutive`]. + /// After construction,[`Erc721Consecutive::_mint_consecutive`] is no /// longer available and minting through [`Erc721Consecutive::_update`] /// becomes possible. pub fn _update( @@ -211,7 +239,7 @@ impl Erc721Consecutive { // record burn if to == Address::ZERO // if we burn - && token_id < U256::from(self.next_consecutive_id()) // and the tokenId was minted in a batch + && token_id < U256::from(self._next_consecutive_id()) // and the tokenId was minted in a batch && !self._sequentian_burn.get(token_id) // and the token was never marked as burnt { @@ -221,10 +249,14 @@ impl Erc721Consecutive { Ok(previous_owner) } - /// Returns the next tokenId to mint using {_mintConsecutive}. It will - /// return [`FIRST_CONSECUTIVE_ID`] if no consecutive tokenId has been - /// minted before. - fn next_consecutive_id(&self) -> U96 { + /// Returns the next tokenId to mint using [`Self::_mint_consecutive`]. It + /// will return [`FIRST_CONSECUTIVE_ID`] if no consecutive tokenId 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 => FIRST_CONSECUTIVE_ID, Some((latest_id, _)) => latest_id + uint!(1_U96), @@ -342,7 +374,7 @@ impl Erc721Consecutive { /// * `&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 { + fn __owner_of_inner(&self, token_id: U256) -> Address { self.erc721._owners.get(token_id) } @@ -375,7 +407,7 @@ impl Erc721Consecutive { /// # Events /// /// Emits a [`Transfer`] event. - pub fn __update( + fn __update( &mut self, to: Address, token_id: U256, diff --git a/examples/erc721-consecutive/src/lib.rs b/examples/erc721-consecutive/src/lib.rs index aa5fecca..ae7bc804 100644 --- a/examples/erc721-consecutive/src/lib.rs +++ b/examples/erc721-consecutive/src/lib.rs @@ -28,7 +28,8 @@ impl Erc721ConsecutiveExample { pub fn burn(&mut self, token_id: U256) -> Result<(), Error> { self.erc721_consecutive._burn(token_id) } - + + // TODO#q: we should be able to call it just once pub fn init( &mut self, receivers: Vec
, @@ -40,7 +41,7 @@ impl Erc721ConsecutiveExample { let batch = batches[i]; let token_id = self .erc721_consecutive - .mint_consecutive(receiver, U96::from(batch))?; + ._mint_consecutive(receiver, U96::from(batch))?; } Ok(()) } diff --git a/examples/erc721-consecutive/tests/erc721.rs b/examples/erc721-consecutive/tests/erc721-consecutive.rs similarity index 51% rename from examples/erc721-consecutive/tests/erc721.rs rename to examples/erc721-consecutive/tests/erc721-consecutive.rs index 18bda941..3e965f0c 100644 --- a/examples/erc721-consecutive/tests/erc721.rs +++ b/examples/erc721-consecutive/tests/erc721-consecutive.rs @@ -1,20 +1,28 @@ #![cfg(feature = "e2e")] -use alloy::primitives::{Address, U256}; +use alloy::{ + primitives::{Address, U256}, + sol, + sol_types::SolConstructor, +}; use alloy_primitives::uint; -use e2e::{Account, EventExt, Revert}; +use e2e::{watch, Account, EventExt, Revert}; use crate::abi::Erc721; mod abi; +sol!("src/constructor.sol"); + fn random_token_id() -> U256 { let num: u32 = rand::random(); U256::from(num) } async fn deploy(rpc_url: &str, private_key: &str) -> eyre::Result
{ - e2e::deploy(rpc_url, private_key, None).await + let args = Erc721Example::constructorCall {}; + let args = alloy::hex::encode(args.abi_encode()); + e2e::deploy(rpc_url, private_key, Some(args)).await } #[e2e::test] @@ -25,8 +33,10 @@ async fn constructs(alice: Account) -> eyre::Result<()> { let alice_addr = alice.address(); let receivers = vec![alice_addr]; let amounts = vec![uint!(10_U256)]; - let res = contract.init(receivers, amounts).call().await?; + let _ = watch!(contract.init(receivers, amounts))?; + let balance = contract.balanceOf(alice_addr).call().await?.balance; + assert_eq!(balance, uint!(10_U256)); Ok(()) } -// TODO#q: construct batches +// TODO#q: add erc721 implementation related tests From 99abce942eb4e86946195f931f2c315d32d4866b Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Fri, 12 Jul 2024 14:09:39 +0400 Subject: [PATCH 51/95] add stop mint consecutive feature --- .../token/erc721/extensions/consecutive.rs | 33 +++++++++++++------ examples/erc721-consecutive/src/lib.rs | 10 ++---- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index 0100cbd1..d1906d3b 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -33,6 +33,9 @@ sol_storage! { Erc721 erc721; Trace160 _sequential_ownership; BitMap _sequentian_burn; + /// Initialization marker. If true this means that the constructor was + /// called. + bool _initialized } } @@ -84,13 +87,13 @@ pub enum Error { /// Batch mint is restricted to the constructor. /// Any batch mint not emitting the [`IERC721::Transfer`] event outside of /// the constructor is non ERC-721 compliant. - Erc721ForbiddenBatchMint(ERC721ForbiddenBatchMint), + ForbiddenBatchMint(ERC721ForbiddenBatchMint), /// Exceeds the max amount of mints per batch. - Erc721ExceededMaxBatchMint(ERC721ExceededMaxBatchMint), + ExceededMaxBatchMint(ERC721ExceededMaxBatchMint), /// Individual minting is not allowed. - Erc721ForbiddenMint(ERC721ForbiddenMint), + ForbiddenMint(ERC721ForbiddenMint), /// Batch burn is not supported. - Erc721ForbiddenBatchBurn(ERC721ForbiddenBatchBurn), + ForbiddenBatchBurn(ERC721ForbiddenBatchBurn), } impl MethodError for erc721::Error { @@ -181,8 +184,9 @@ impl Erc721Consecutive { let next = self._next_consecutive_id(); if batch_size > U96::ZERO { - //TODO#q: check address of this and revert with - // ERC721ForbiddenBatchMint + if self._initialized.get() { + return Err(ERC721ForbiddenBatchMint {}.into()); + } if to.is_zero() { return Err(Erc721Error::InvalidReceiver( @@ -214,6 +218,17 @@ impl Erc721Consecutive { Ok(next) } + /// Should be called to restrict consecutive mint after. + /// After this function being called, every call to + /// [`Self::_mint_consecutive`] will fail. + /// + /// # Arguments + /// + /// * `&self` - Write access to the contract's state. + pub fn _stop_mint_consecutive(&mut self) { + self._initialized.set(true); + } + /// Override of [`Erc721::_update`] that restricts normal minting to after /// construction. /// @@ -231,10 +246,8 @@ impl Erc721Consecutive { let previous_owner = self.__update(to, token_id, auth)?; // only mint after construction - if previous_owner == Address::ZERO - /* TODO#q: and address code is zero */ - { - return Err(ERC721ForbiddenMint {}.into()); // + if previous_owner == Address::ZERO && !self._initialized.get() { + return Err(ERC721ForbiddenMint {}.into()); } // record burn diff --git a/examples/erc721-consecutive/src/lib.rs b/examples/erc721-consecutive/src/lib.rs index ae7bc804..5ebec1eb 100644 --- a/examples/erc721-consecutive/src/lib.rs +++ b/examples/erc721-consecutive/src/lib.rs @@ -4,12 +4,8 @@ extern crate alloc; use alloc::vec::Vec; use alloy_primitives::{Address, U256}; -use alloy_sol_types::SolError; use openzeppelin_stylus::{ - token::erc721::extensions::{ - consecutive::{Erc721Consecutive, Error}, - IErc721Burnable, - }, + token::erc721::extensions::consecutive::{Erc721Consecutive, Error}, utils::structs::checkpoints::U96, }; use stylus_sdk::prelude::*; @@ -28,8 +24,7 @@ impl Erc721ConsecutiveExample { pub fn burn(&mut self, token_id: U256) -> Result<(), Error> { self.erc721_consecutive._burn(token_id) } - - // TODO#q: we should be able to call it just once + pub fn init( &mut self, receivers: Vec
, @@ -43,6 +38,7 @@ impl Erc721ConsecutiveExample { .erc721_consecutive ._mint_consecutive(receiver, U96::from(batch))?; } + self.erc721_consecutive._stop_mint_consecutive(); Ok(()) } } From 9215fb2523b347756d6ba396a952b3d049f3ae69 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Fri, 12 Jul 2024 19:11:43 +0400 Subject: [PATCH 52/95] add mints and transfer_from tests --- .../token/erc721/extensions/consecutive.rs | 116 +++++++++++++++++- 1 file changed, 115 insertions(+), 1 deletion(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index d1906d3b..88f4c213 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -28,7 +28,6 @@ use crate::{ sol_storage! { /// State of an [`Erc72Erc721Consecutive`] token. - #[cfg_attr(all(test, feature = "std"), derive(motsu::DefaultStorageLayout))] pub struct Erc721Consecutive { Erc721 erc721; Trace160 _sequential_ownership; @@ -783,3 +782,118 @@ impl Erc721Consecutive { Ok(owner) } } + +#[cfg(all(test, feature = "std"))] +mod test { + use alloy_primitives::{address, uint, Address, U256}; + use stylus_sdk::{msg, prelude::StorageType}; + + use crate::{ + token::erc721::{ + extensions::consecutive::Erc721Consecutive, tests::random_token_id, + Erc721, IErc721, + }, + utils::structs::{ + bitmap::BitMap, + checkpoints::{Trace160, U96}, + }, + }; + + // TODO#q: add support for complex contracts creation for motsu + impl Default for Erc721Consecutive { + fn default() -> Self { + #[derive(Default)] + struct StorageTypeFactory { + root: U256, + } + impl StorageTypeFactory { + fn create(&mut self) -> T { + let instance = unsafe { T::new(self.root, 0) }; + self.root += U256::from(T::SLOT_BYTES); + instance + } + } + + let mut factory = StorageTypeFactory::default(); + Erc721Consecutive { + erc721: Erc721 { + _owners: factory.create(), + _balances: factory.create(), + _token_approvals: factory.create(), + _operator_approvals: factory.create(), + }, + _sequential_ownership: Trace160 { + _checkpoints: factory.create(), + }, + _sequentian_burn: BitMap { _data: factory.create() }, + _initialized: factory.create(), + } + } + } + + const BOB: Address = address!("F4EaCDAbEf3c8f1EdE91b6f2A6840bc2E4DD3526"); + const DAVE: Address = address!("0BB78F7e7132d1651B4Fd884B7624394e92156F1"); + + fn init( + contract: &mut Erc721Consecutive, + receivers: Vec
, + batches: Vec, + ) -> Vec { + let token_ids = receivers + .into_iter() + .zip(batches) + .map(|(to, batch_size)| { + contract + ._mint_consecutive(to, batch_size) + .expect("should mint consecutively") + }) + .collect(); + contract._stop_mint_consecutive(); + token_ids + } + + #[motsu::test] + fn mints(contract: Erc721Consecutive) { + let alice = msg::sender(); + let token_id = random_token_id(); + + 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)); + + 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 transfers_from(contract: Erc721Consecutive) { + contract._stop_mint_consecutive(); + let alice = msg::sender(); + let token_id = random_token_id(); + contract._mint(alice, token_id).expect("should mint a token to Alice"); + contract + .transfer_from(alice, BOB, token_id) + .expect("should transfer a token from Alice to Bob"); + let owner = contract + .owner_of(token_id) + .expect("should return the owner of the token"); + assert_eq!(owner, BOB); + } +} From bf7bd433be230fa8e4905181f061e1ff3079b9f0 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Fri, 12 Jul 2024 21:34:23 +0400 Subject: [PATCH 53/95] ++ --- .../token/erc721/extensions/consecutive.rs | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index 88f4c213..a835ed84 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -1,3 +1,28 @@ +//! 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 of 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. +//! +//! IMPORTANT: This extension does not call the {_update} function for tokens +//! minted in batch. Any logic added to this function through overrides will not +//! be triggered when token are minted in batch. You may want to also override +//! [`Erc721Consecutive::_increaseBalance`] or +//! [`Erc721Consecutive::_mintConsecutive`] to account for these mints. +//! +//! IMPORTANT: When overriding [`Erc721Consecutive::_mintConsecutive`], be +//! careful about call ordering. [`Erc721Consecutive::owner_of`] may return +//! invalid values during the [`Erc721Consecutive::_mintConsecutive`] +//! execution if the super call is not called first. To be safe, execute the +//! super call before your custom logic. + use alloc::vec; use alloy_primitives::{uint, Address, U128, U256}; @@ -107,7 +132,7 @@ impl MethodError for checkpoints::Error { } } -// TODO#q: have these constants generic +// TODO: add option to override these constants // 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 @@ -881,9 +906,14 @@ mod test { assert_eq!(balance2, balance1 + uint!(1_U256)); } - + + // TODO#q: error_when_not_minted_consecutive ERC721ForbiddenBatchMint + // TODO#q: error_when_to_is_zero InvalidReceiver + // TODO#q: error_when_exceed_batch_size ERC721ExceededMaxBatchMint + #[motsu::test] fn transfers_from(contract: Erc721Consecutive) { + // TODO#q: consecutive transfers_from contract._stop_mint_consecutive(); let alice = msg::sender(); let token_id = random_token_id(); @@ -896,4 +926,6 @@ mod test { .expect("should return the owner of the token"); assert_eq!(owner, BOB); } + + // TODO#q: burns (consecutive and erc721 default implementation) } From fd92550ab1af0457cebfa7915177d5fa94266723 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Fri, 12 Jul 2024 22:48:10 +0400 Subject: [PATCH 54/95] add error tests --- .../token/erc721/extensions/consecutive.rs | 103 +++++++++++++----- .../src/token/erc721/extensions/enumerable.rs | 8 +- 2 files changed, 80 insertions(+), 31 deletions(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index a835ed84..7d8dc9d8 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -38,7 +38,7 @@ use crate::{ erc721::{ Approval, ERC721IncorrectOwner, ERC721InvalidApprover, ERC721InvalidReceiver, ERC721InvalidSender, ERC721NonexistentToken, - Erc721, Error as Erc721Error, IERC721Receiver, IErc721, Transfer, + Erc721, IERC721Receiver, IErc721, Transfer, }, }, utils::{ @@ -137,10 +137,10 @@ impl MethodError for checkpoints::Error { // 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. -const MAX_BATCH_SIZE: U96 = uint!(5000_U96); +pub const MAX_BATCH_SIZE: U96 = uint!(5000_U96); // Used to offset the first token id in {_nextConsecutiveId} -const FIRST_CONSECUTIVE_ID: U96 = uint!(0_U96); +pub const FIRST_CONSECUTIVE_ID: U96 = uint!(0_U96); /// Consecutive extension related implementation: impl Erc721Consecutive { @@ -192,7 +192,7 @@ impl Erc721Consecutive { /// /// # Errors /// - /// If to is [`Address::ZERO`] error [`Erc721Error::InvalidReceiver`] is + /// If to is [`Address::ZERO`] error [`rc721::Error::InvalidReceiver`] is /// returned. /// If batch size exceeds [`MAX_BATCH_SIZE`] error /// [`Error::ERC721ExceededMaxBatchMint`] is returned. @@ -213,7 +213,7 @@ impl Erc721Consecutive { } if to.is_zero() { - return Err(Erc721Error::InvalidReceiver( + return Err(erc721::Error::InvalidReceiver( ERC721InvalidReceiver { receiver: Address::ZERO }, ) .into()); @@ -351,9 +351,9 @@ impl IErc721 for Erc721Consecutive { token_id: U256, ) -> Result<(), Error> { if to.is_zero() { - return Err(Erc721Error::InvalidReceiver(ERC721InvalidReceiver { - receiver: Address::ZERO, - }) + return Err(erc721::Error::InvalidReceiver( + ERC721InvalidReceiver { receiver: Address::ZERO }, + ) .into()); } @@ -362,7 +362,7 @@ impl IErc721 for Erc721Consecutive { // 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(Erc721Error::IncorrectOwner(ERC721IncorrectOwner { + return Err(erc721::Error::IncorrectOwner(ERC721IncorrectOwner { sender: from, token_id, owner: previous_owner, @@ -508,15 +508,15 @@ impl Erc721Consecutive { /// Emits a [`Transfer`] event. pub fn _mint(&mut self, to: Address, token_id: U256) -> Result<(), Error> { if to.is_zero() { - return Err(Erc721Error::InvalidReceiver(ERC721InvalidReceiver { - receiver: Address::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(Erc721Error::InvalidSender(ERC721InvalidSender { + return Err(erc721::Error::InvalidSender(ERC721InvalidSender { sender: Address::ZERO, }) .into()); @@ -601,7 +601,7 @@ impl Erc721Consecutive { let previous_owner = self._update(Address::ZERO, token_id, Address::ZERO)?; if previous_owner.is_zero() { - return Err(Erc721Error::NonexistentToken( + return Err(erc721::Error::NonexistentToken( ERC721NonexistentToken { token_id }, ) .into()); @@ -645,20 +645,20 @@ impl Erc721Consecutive { token_id: U256, ) -> Result<(), Error> { if to.is_zero() { - return Err(Erc721Error::InvalidReceiver(ERC721InvalidReceiver { - receiver: Address::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(Erc721Error::NonexistentToken( + return Err(erc721::Error::NonexistentToken( ERC721NonexistentToken { token_id }, ) .into()); } else if previous_owner != from { - return Err(Erc721Error::IncorrectOwner(ERC721IncorrectOwner { + return Err(erc721::Error::IncorrectOwner(ERC721IncorrectOwner { sender: from, token_id, owner: previous_owner, @@ -766,7 +766,7 @@ impl Erc721Consecutive { && owner != auth && !self.is_approved_for_all(owner, auth) { - return Err(Erc721Error::InvalidApprover( + return Err(erc721::Error::InvalidApprover( ERC721InvalidApprover { approver: auth }, ) .into()); @@ -799,7 +799,7 @@ impl Erc721Consecutive { pub fn _require_owned(&self, token_id: U256) -> Result { let owner = self._owner_of_inner(token_id); if owner.is_zero() { - return Err(Erc721Error::NonexistentToken( + return Err(erc721::Error::NonexistentToken( ERC721NonexistentToken { token_id }, ) .into()); @@ -814,9 +814,16 @@ mod test { use stylus_sdk::{msg, prelude::StorageType}; use crate::{ - token::erc721::{ - extensions::consecutive::Erc721Consecutive, tests::random_token_id, - Erc721, IErc721, + token::{ + erc721, + erc721::{ + extensions::consecutive::{ + ERC721ExceededMaxBatchMint, ERC721ForbiddenBatchMint, + Erc721Consecutive, Error, MAX_BATCH_SIZE, + }, + tests::random_token_id, + ERC721InvalidReceiver, Erc721, IErc721, + }, }, utils::structs::{ bitmap::BitMap, @@ -907,9 +914,51 @@ mod test { assert_eq!(balance2, balance1 + uint!(1_U256)); } - // TODO#q: error_when_not_minted_consecutive ERC721ForbiddenBatchMint - // TODO#q: error_when_to_is_zero InvalidReceiver - // TODO#q: error_when_exceed_batch_size ERC721ExceededMaxBatchMint + #[motsu::test] + fn error_when_not_minted_consecutive(contract: Erc721Consecutive) { + let alice = msg::sender(); + + init(contract, vec![alice], vec![uint!(10_U96)]); + + let err = contract._mint_consecutive(BOB, uint!(11_U96)).expect_err( + "should not mint consecutive when consecutive mint is finalised", + ); + assert!(matches!( + err, + Error::ForbiddenBatchMint(ERC721ForbiddenBatchMint {}) + )); + } + + #[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 when consecutive mint is finalised", + ); + 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 = MAX_BATCH_SIZE + uint!(1_U96); + let err = contract._mint_consecutive(alice, batch_size).expect_err( + "should not mint consecutive when consecutive mint is finalised", + ); + assert!(matches!( + err, + Error::ExceededMaxBatchMint(ERC721ExceededMaxBatchMint { + batchSize, + maxBatch + }) if batchSize == U256::from(batch_size) && maxBatch == U256::from(MAX_BATCH_SIZE) + )); + } #[motsu::test] fn transfers_from(contract: Erc721Consecutive) { diff --git a/contracts/src/token/erc721/extensions/enumerable.rs b/contracts/src/token/erc721/extensions/enumerable.rs index 9e2b5bad..dbf8cdfc 100644 --- a/contracts/src/token/erc721/extensions/enumerable.rs +++ b/contracts/src/token/erc721/extensions/enumerable.rs @@ -10,8 +10,8 @@ use alloy_primitives::{uint, Address, U256}; use alloy_sol_types::sol; use stylus_proc::{external, sol_storage, SolidityError}; -use crate::token::erc721; -use crate::token::erc721::IErc721; + +use crate::token::{erc721, erc721::IErc721}; sol! { /// Indicates an error when an `owner`'s token query @@ -155,7 +155,7 @@ impl Erc721Enumerable { &mut self, to: Address, token_id: U256, - erc721: &impl IErc721, + 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); @@ -206,7 +206,7 @@ impl Erc721Enumerable { &mut self, from: Address, token_id: U256, - erc721: &impl IErc721, + 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, From f3b937f17824045f85a4a19c95f881d760363d70 Mon Sep 17 00:00:00 2001 From: alexfertel Date: Mon, 15 Jul 2024 08:33:53 +0200 Subject: [PATCH 55/95] ref(docs): polish Nonces tests & docs (#195) --- contracts/src/token/erc20/mod.rs | 4 ++-- contracts/src/utils/nonces.rs | 28 ++++++++++++---------------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/contracts/src/token/erc20/mod.rs b/contracts/src/token/erc20/mod.rs index 1bb2c69b..9594d0ff 100644 --- a/contracts/src/token/erc20/mod.rs +++ b/contracts/src/token/erc20/mod.rs @@ -1,6 +1,6 @@ -//! Implementation of the [`Erc20`] token standard. +//! Implementation of the ERC-20 token standard. //! -//! We have followed general ``OpenZeppelin`` Contracts guidelines: functions +//! We have followed general `OpenZeppelin` Contracts guidelines: functions //! revert instead of returning `false` on failure. This behavior is //! nonetheless conventional and does not conflict with the expectations of //! [`Erc20`] applications. diff --git a/contracts/src/utils/nonces.rs b/contracts/src/utils/nonces.rs index d10a6d3b..de6b81a5 100644 --- a/contracts/src/utils/nonces.rs +++ b/contracts/src/utils/nonces.rs @@ -1,9 +1,6 @@ -//! Nonces Contract +//! Implementation of nonce tracking for addresses. //! -//! Contract module which provides functionalities for tracking nonces for -//! addresses. -//! -//! Note: Nonce will only increment. +//! Nonces will only increment. use alloy_primitives::{uint, Address, U256}; use alloy_sol_types::sol; @@ -110,21 +107,20 @@ impl Nonces { #[cfg(all(test, feature = "std"))] mod tests { - use alloy_primitives::{address, uint, U256}; + use alloy_primitives::U256; + use stylus_sdk::msg; use super::ONE; use crate::utils::nonces::{Error, Nonces}; #[motsu::test] - fn test_initiate_nonce(contract: Nonces) { - let owner = address!("d8da6bf26964af9d7eed9e03e53415d37aa96045"); - - assert_eq!(contract.nonce(owner), U256::ZERO); + fn initiate_nonce(contract: Nonces) { + assert_eq!(contract.nonce(msg::sender()), U256::ZERO); } #[motsu::test] - fn test_use_nonce(contract: Nonces) { - let owner = address!("d8da6bf26964af9d7eed9e03e53415d37aa96045"); + fn use_nonce(contract: Nonces) { + let owner = msg::sender(); let use_nonce = contract.use_nonce(owner); assert_eq!(use_nonce, U256::ZERO); @@ -134,8 +130,8 @@ mod tests { } #[motsu::test] - fn test_use_checked_nonce(contract: Nonces) { - let owner = address!("d8da6bf26964af9d7eed9e03e53415d37aa96045"); + fn use_checked_nonce(contract: Nonces) { + let owner = msg::sender(); let use_checked_nonce = contract.use_checked_nonce(owner, U256::ZERO); assert!(use_checked_nonce.is_ok()); @@ -145,8 +141,8 @@ mod tests { } #[motsu::test] - fn test_use_checked_nonce_invalid_nonce(contract: Nonces) { - let owner = address!("d8da6bf26964af9d7eed9e03e53415d37aa96045"); + fn use_checked_nonce_invalid_nonce(contract: Nonces) { + let owner = msg::sender(); let use_checked_nonce = contract.use_checked_nonce(owner, ONE); assert!(matches!( From 6232e9a2e38b653abf5414d86096dc71874cd18f Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Mon, 15 Jul 2024 15:10:56 +0400 Subject: [PATCH 56/95] add transfer_from and burns tests --- .../token/erc721/extensions/consecutive.rs | 110 ++++++++++++++++-- 1 file changed, 101 insertions(+), 9 deletions(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index 7d8dc9d8..13bda885 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -415,6 +415,7 @@ impl Erc721Consecutive { self.erc721._owners.get(token_id) } + // TODO#q: move arguments and errors documentation to the public function /// 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. @@ -822,7 +823,7 @@ mod test { Erc721Consecutive, Error, MAX_BATCH_SIZE, }, tests::random_token_id, - ERC721InvalidReceiver, Erc721, IErc721, + ERC721InvalidReceiver, ERC721NonexistentToken, Erc721, IErc721, }, }, utils::structs::{ @@ -956,25 +957,116 @@ mod test { Error::ExceededMaxBatchMint(ERC721ExceededMaxBatchMint { batchSize, maxBatch - }) if batchSize == U256::from(batch_size) && maxBatch == U256::from(MAX_BATCH_SIZE) + }) + if batchSize == U256::from(batch_size) && maxBatch == U256::from(MAX_BATCH_SIZE) )); } #[motsu::test] fn transfers_from(contract: Erc721Consecutive) { - // TODO#q: consecutive transfers_from - contract._stop_mint_consecutive(); 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)); + + // Test 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)); + + // Test 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 owner = contract - .owner_of(token_id) - .expect("should return the owner of the token"); - assert_eq!(owner, 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)); } - // TODO#q: burns (consecutive and erc721 default implementation) + #[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 the balance of Alice"); + 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) + )); + } } From fa70e3043b094cb83b67a2ebddbedeaa2b809555 Mon Sep 17 00:00:00 2001 From: alexfertel Date: Mon, 15 Jul 2024 18:49:26 +0200 Subject: [PATCH 57/95] feat: bench merkle proofs verification gas usage (#196) Resolves #174 ![Screenshot 2024-07-15 at 16 45 37](https://github.com/user-attachments/assets/557f6ba9-d5c0-4a9e-832d-4974ba87c9c6) --- Cargo.lock | 4 +- Cargo.toml | 2 +- benches/src/access_control.rs | 2 +- benches/src/erc20.rs | 2 +- benches/src/lib.rs | 22 +++-- benches/src/main.rs | 8 +- benches/src/merkle_proofs.rs | 128 ++++++++++++++++++++++++++++++ examples/basic/script/src/main.rs | 3 +- lib/e2e/src/account.rs | 2 +- lib/e2e/src/deploy.rs | 3 +- 10 files changed, 159 insertions(+), 17 deletions(-) create mode 100644 benches/src/merkle_proofs.rs diff --git a/Cargo.lock b/Cargo.lock index e26344d9..025b39db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2194,9 +2194,9 @@ checksum = "57d8d8ce877200136358e0bbff3a77965875db3af755a11e1fa6b1b3e2df13ea" [[package]] name = "koba" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb251e0817a500aa5ef72e7e4e25c063a33b7e849d14ad1d7221ebe48893d7a7" +checksum = "f31de3702e0ac9b1f6927d12a8157af9c5796bc1caa9e89e43bda20b98af6685" dependencies = [ "alloy", "brotli2", diff --git a/Cargo.toml b/Cargo.toml index bdf039b5..6c7d64e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,7 +76,7 @@ alloy-sol-types = { version = "0.3.1", default-features = false } const-hex = { version = "1.11.1", default-features = false } eyre = "0.6.8" -koba = "0.1.0" +koba = "0.1.2" once_cell = "1.19.0" rand = "0.8.5" regex = "1.10.4" diff --git a/benches/src/access_control.rs b/benches/src/access_control.rs index b51be753..f8ab77f7 100644 --- a/benches/src/access_control.rs +++ b/benches/src/access_control.rs @@ -129,5 +129,5 @@ pub async fn bench() -> eyre::Result<()> { async fn deploy(account: &Account) -> Address { let args = AccessControl::constructorCall {}; let args = alloy::hex::encode(args.abi_encode()); - crate::deploy(account, "access-control", &args).await + crate::deploy(account, "access-control", Some(args)).await } diff --git a/benches/src/erc20.rs b/benches/src/erc20.rs index 9a05dbbe..cef7c75f 100644 --- a/benches/src/erc20.rs +++ b/benches/src/erc20.rs @@ -153,5 +153,5 @@ async fn deploy(account: &Account) -> Address { cap_: CAP, }; let args = alloy::hex::encode(args.abi_encode()); - crate::deploy(account, "erc20", &args).await + crate::deploy(account, "erc20", Some(args)).await } diff --git a/benches/src/lib.rs b/benches/src/lib.rs index e12633a2..32e67c15 100644 --- a/benches/src/lib.rs +++ b/benches/src/lib.rs @@ -6,6 +6,7 @@ use serde::Deserialize; pub mod access_control; pub mod erc20; +pub mod merkle_proofs; const RPC_URL: &str = "http://localhost:8547"; @@ -18,7 +19,11 @@ struct ArbOtherFields { l1_block_number: String, } -async fn deploy(account: &Account, contract_name: &str, args: &str) -> Address { +async fn deploy( + account: &Account, + contract_name: &str, + args: Option, +) -> Address { let manifest_dir = std::env::current_dir().expect("should get current dir from env"); @@ -27,18 +32,20 @@ async fn deploy(account: &Account, contract_name: &str, args: &str) -> Address { .join("wasm32-unknown-unknown") .join("release") .join(format!("{}_example.wasm", contract_name.replace('-', "_"))); - let sol_path = manifest_dir - .join("examples") - .join(format!("{}", contract_name)) - .join("src") - .join("constructor.sol"); + let sol_path = args.as_ref().map(|_| { + manifest_dir + .join("examples") + .join(format!("{}", contract_name)) + .join("src") + .join("constructor.sol") + }); let pk = account.pk(); let config = Deploy { generate_config: Generate { wasm: wasm_path.clone(), sol: sol_path, - args: Some(args.to_owned()), + args, legacy: false, }, auth: PrivateKey { @@ -49,6 +56,7 @@ async fn deploy(account: &Account, contract_name: &str, args: &str) -> Address { }, endpoint: RPC_URL.to_owned(), deploy_only: false, + quiet: true, }; koba::deploy(&config).await.expect("should deploy contract") diff --git a/benches/src/main.rs b/benches/src/main.rs index c80ed4fe..beb2d951 100644 --- a/benches/src/main.rs +++ b/benches/src/main.rs @@ -1,8 +1,12 @@ -use benches::{access_control, erc20}; +use benches::{access_control, erc20, merkle_proofs}; #[tokio::main] async fn main() -> eyre::Result<()> { - let _ = tokio::join!(erc20::bench(), access_control::bench()); + let _ = tokio::join!( + erc20::bench(), + access_control::bench(), + merkle_proofs::bench() + ); Ok(()) } diff --git a/benches/src/merkle_proofs.rs b/benches/src/merkle_proofs.rs new file mode 100644 index 00000000..f2869cb7 --- /dev/null +++ b/benches/src/merkle_proofs.rs @@ -0,0 +1,128 @@ +use alloy::{ + hex, + network::{AnyNetwork, EthereumWallet}, + primitives::Address, + providers::ProviderBuilder, + sol, +}; +use e2e::{receipt, Account}; + +use crate::ArbOtherFields; + +sol!( + #[sol(rpc)] + contract Verifier { + function verify(bytes32[] proof, bytes32 root, bytes32 leaf) external pure returns (bool); + + function multiProofVerify( + bytes32[] memory proof, + bool[] memory proofFlags, + bytes32 root, + bytes32[] memory leaves + ) external pure returns (bool); + } +); + +/// Shorthand for converting from an array of hex literals to an array of +/// fixed 32-bytes slices. +macro_rules! bytes_array { + ($($s:literal),* $(,)?) => { + [ + $(alloy::hex!($s),)* + ] + }; +} + +pub async fn bench() -> eyre::Result<()> { + let alice = Account::new().await?; + let alice_wallet = ProviderBuilder::new() + .network::() + .with_recommended_fillers() + .wallet(EthereumWallet::from(alice.signer.clone())) + .on_http(alice.url().parse()?); + + let contract_addr = deploy(&alice).await; + let contract = Verifier::new(contract_addr, &alice_wallet); + + let root = hex!( + "bb439f2cd52c20cd5a0d2a9fc43acc94e44c58b6b8907626f15d43e2b6fa4599" + ); + let leaf = hex!( + "ae5a6b19bb2927169dcf59f1e0fab3ef5a58264f24afc950c8921dd0018613e1" + ); + let proof = bytes_array! { + "ae59450ef5e421fa6543f57de8ac9a2e71072669e7dfa4b939b3cb08040c3172", + "812723c39e2874b3ee1e91b19c0ae36cec8f16968292e5cd8d63dc820e4c880e", + "206c4b19563946e4049473bfa67040a9ae95be0afdb147074759501ad1b7cd99", + "9e7c6c195b5e5ea9ed0bf2a03cb9807ad610ad09d8bd19f7f2ee4972829e8e98", + "74e58acd9bfd0778bf2d85fb4bc5532078a9519e84cae147a1794d4be857a476", + "c4a1dadd851264dc68a0143b92933885ee987ce9bd88592fb7ef283d7e4d9b38", + "740787cebe5fdf6a4696191d58e90964022bdc07bbcb4da85fcba3a25a310cfb", + "06ca1871c30b7a4dea60f7739a75b3b376d4277dd15827780e4474b17cb8d42f", + "06acc609483fc476b7b6e81b185ad5380135b3166a092b489b760f05424b8bec", + "7164e2347a3b0349f77cdcdba42de9496fb7da1f40666a7f2e862a0ced0cf687", + "a6e400012f156c6bf518255107bc0eefb8678d8ae4bd35b820b397edd21b45f4", + "9cd2fab756b8e5b4a4749c472d35107d520a841c4f4a5c5c7cdebf61b299f981", + "30b7394a87d2cb2a4fb5530a0bc78bda42d55075019f5c210c43167ba8138393", + "af02b07c5c611f8aa1609e9962668de34a571a32d16e95bf0c90bb15cb78f019", + "dff6a4f635ef79dec68385c4246179534dbd031e7f6ab527a25c73e46b40a7ca", + "fd47b6c292f51911e8dfdc3e4f8bd127773b17f25b7a554beaa8741e99c41208", + } + .map(|h| h.into()) + .to_vec(); + + let receipts = vec![( + "verify()", + receipt!(contract.verify(proof, root.into(), leaf.into()))?, + )]; + + // Calculate the width of the longest function name. + let max_name_width = receipts + .iter() + .max_by_key(|x| x.0.len()) + .expect("should at least bench one function") + .0 + .len(); + let name_width = max_name_width.max("Merkle Proofs".len()); + + // Calculate the total width of the table. + let total_width = name_width + 3 + 6 + 3 + 6 + 3 + 20 + 4; // 3 for padding, 4 for outer borders + + // Print the table header. + println!("+{}+", "-".repeat(total_width - 2)); + println!( + "| {:(); + let effective_gas = l2_gas - l1_gas; + + println!( + "| {:6} | {:>6} | {:>20} |", + func_name, + l2_gas, + l1_gas, + effective_gas, + width = name_width + ); + } + + // Print the table footer. + println!("+{}+", "-".repeat(total_width - 2)); + + Ok(()) +} + +async fn deploy(account: &Account) -> Address { + crate::deploy(account, "merkle-proofs", None).await +} diff --git a/examples/basic/script/src/main.rs b/examples/basic/script/src/main.rs index d0cdabf6..0003a369 100644 --- a/examples/basic/script/src/main.rs +++ b/examples/basic/script/src/main.rs @@ -76,7 +76,7 @@ async fn deploy() -> Address { let config = Deploy { generate_config: koba::config::Generate { wasm: wasm_path.clone(), - sol: sol_path, + sol: Some(sol_path), args: Some(args), legacy: false, }, @@ -88,6 +88,7 @@ async fn deploy() -> Address { }, endpoint: RPC_URL.to_owned(), deploy_only: false, + quiet: false, }; koba::deploy(&config).await.expect("should deploy contract") diff --git a/lib/e2e/src/account.rs b/lib/e2e/src/account.rs index 98291388..cb12a0ad 100644 --- a/lib/e2e/src/account.rs +++ b/lib/e2e/src/account.rs @@ -75,7 +75,7 @@ impl AccountFactory { let signer = PrivateKeySigner::random(); let addr = signer.address(); - fund_account(addr, "10")?; + fund_account(addr, "100")?; let rpc_url = std::env::var(RPC_URL_ENV_VAR_NAME) .expect("failed to load RPC_URL var from env") diff --git a/lib/e2e/src/deploy.rs b/lib/e2e/src/deploy.rs index 83e4ae42..5bb8a8a6 100644 --- a/lib/e2e/src/deploy.rs +++ b/lib/e2e/src/deploy.rs @@ -25,7 +25,7 @@ pub async fn deploy( let config = Deploy { generate_config: koba::config::Generate { wasm: wasm_path.clone(), - sol: sol_path, + sol: Some(sol_path), args, legacy: false, }, @@ -37,6 +37,7 @@ pub async fn deploy( }, endpoint: rpc_url.to_owned(), deploy_only: false, + quiet: false, }; let address = koba::deploy(&config).await?; From 35c2fa73de222839f28cb4f4d07db0db2e98f3ae Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Tue, 16 Jul 2024 23:37:15 +0400 Subject: [PATCH 58/95] add e2e tests for consecutive --- .../token/erc721/extensions/consecutive.rs | 39 ++-- contracts/src/utils/structs/checkpoints.rs | 4 +- .../erc721-consecutive/src/constructor.sol | 14 +- examples/erc721-consecutive/src/lib.rs | 4 + examples/erc721-consecutive/tests/abi.rs | 16 ++ .../tests/erc721-consecutive.rs | 196 +++++++++++++++++- 6 files changed, 244 insertions(+), 29 deletions(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index 13bda885..fa7e5945 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -57,8 +57,7 @@ sol_storage! { Erc721 erc721; Trace160 _sequential_ownership; BitMap _sequentian_burn; - /// Initialization marker. If true this means that the constructor was - /// called. + /// Initialization marker. If true this means that consecutive mint was already triggered. bool _initialized } } @@ -134,12 +133,13 @@ impl MethodError for checkpoints::Error { // TODO: add option to override these constants -// 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. +/// 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. pub const MAX_BATCH_SIZE: U96 = uint!(5000_U96); -// Used to offset the first token id in {_nextConsecutiveId} +/// Used to offset the first token id in +/// [`Erc721Consecutive::_next_consecutive_id`] pub const FIRST_CONSECUTIVE_ID: U96 = uint!(0_U96); /// Consecutive extension related implementation: @@ -308,7 +308,7 @@ impl IErc721 for Erc721Consecutive { type Error = Error; fn balance_of(&self, owner: Address) -> Result { - self.erc721.balance_of(owner).map_err(|e| e.into()) + Ok(self.erc721.balance_of(owner)?) } fn owner_of(&self, token_id: U256) -> Result { @@ -888,7 +888,6 @@ mod test { #[motsu::test] fn mints(contract: Erc721Consecutive) { let alice = msg::sender(); - let token_id = random_token_id(); let initial_balance = contract .balance_of(alice) @@ -902,6 +901,8 @@ mod test { .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) @@ -921,9 +922,9 @@ mod test { init(contract, vec![alice], vec![uint!(10_U96)]); - let err = contract._mint_consecutive(BOB, uint!(11_U96)).expect_err( - "should not mint consecutive when consecutive mint is finalised", - ); + let err = contract + ._mint_consecutive(BOB, uint!(11_U96)) + .expect_err("should not mint consecutive"); assert!(matches!( err, Error::ForbiddenBatchMint(ERC721ForbiddenBatchMint {}) @@ -934,9 +935,7 @@ mod 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 when consecutive mint is finalised", - ); + .expect_err("should not mint consecutive"); assert!(matches!( err, Error::Erc721(erc721::Error::InvalidReceiver( @@ -949,9 +948,9 @@ mod test { fn error_when_exceed_batch_size(contract: Erc721Consecutive) { let alice = msg::sender(); let batch_size = MAX_BATCH_SIZE + uint!(1_U96); - let err = contract._mint_consecutive(alice, batch_size).expect_err( - "should not mint consecutive when consecutive mint is finalised", - ); + let err = contract + ._mint_consecutive(alice, batch_size) + .expect_err("should not mint consecutive"); assert!(matches!( err, Error::ExceededMaxBatchMint(ERC721ExceededMaxBatchMint { @@ -995,7 +994,7 @@ mod test { contract.balance_of(bob).expect("should return the balance of Bob"); assert_eq!(bob_balance, uint!(1000_U256) + uint!(1_U256)); - // Test non-consecutive mint + // 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 @@ -1003,7 +1002,7 @@ mod test { .expect("should return the balance of Alice"); assert_eq!(alice_balance, uint!(1000_U256)); - // Test transfer of the token that wasn't minted consecutive + // 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"); @@ -1050,7 +1049,7 @@ mod test { .expect("should mint a token to Alice"); let owner = contract .owner_of(non_consecutive_token_id) - .expect("should return the balance of Alice"); + .expect("should return owner of the token"); assert_eq!(owner, alice); let alice_balance = contract .balance_of(alice) diff --git a/contracts/src/utils/structs/checkpoints.rs b/contracts/src/utils/structs/checkpoints.rs index bf9a03d2..f64d0349 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/erc721-consecutive/src/constructor.sol b/examples/erc721-consecutive/src/constructor.sol index 48e4431b..f235c4ae 100644 --- a/examples/erc721-consecutive/src/constructor.sol +++ b/examples/erc721-consecutive/src/constructor.sol @@ -7,13 +7,17 @@ contract Erc721Example { mapping(uint256 tokenId => address) private _tokenApprovals; mapping(address owner => mapping(address operator => bool)) private _operatorApprovals; + + mapping(uint256 => uint256) private _data; + Checkpoint160[] private _checkpoints; + bool _initialized; - mapping(address owner => mapping(uint256 index => uint256)) - private _ownedTokens; - mapping(uint256 tokenId => uint256) private _ownedTokensIndex; - uint256[] private _allTokens; - mapping(uint256 tokenId => uint256) private _allTokensIndex; + struct Checkpoint160 { + uint96 _key; + uint160 _value; + } constructor() { + _initialized = false; } } diff --git a/examples/erc721-consecutive/src/lib.rs b/examples/erc721-consecutive/src/lib.rs index 5ebec1eb..56b1a3a1 100644 --- a/examples/erc721-consecutive/src/lib.rs +++ b/examples/erc721-consecutive/src/lib.rs @@ -41,4 +41,8 @@ impl Erc721ConsecutiveExample { self.erc721_consecutive._stop_mint_consecutive(); Ok(()) } + + 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 index eb4d2c48..2f1f340d 100644 --- a/examples/erc721-consecutive/tests/abi.rs +++ b/examples/erc721-consecutive/tests/abi.rs @@ -4,7 +4,9 @@ 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; @@ -13,6 +15,7 @@ sol!( 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; function init(address[] memory receivers, uint256[] memory amounts) external; @@ -26,6 +29,11 @@ sol!( 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); @@ -34,5 +42,13 @@ sol!( #[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 index 3e965f0c..e1f4d130 100644 --- a/examples/erc721-consecutive/tests/erc721-consecutive.rs +++ b/examples/erc721-consecutive/tests/erc721-consecutive.rs @@ -6,7 +6,10 @@ use alloy::{ sol_types::SolConstructor, }; use alloy_primitives::uint; -use e2e::{watch, Account, EventExt, Revert}; +use e2e::{receipt, send, watch, Account, EventExt, Revert}; +use openzeppelin_stylus::token::erc721::extensions::consecutive::{ + FIRST_CONSECUTIVE_ID, MAX_BATCH_SIZE, +}; use crate::abi::Erc721; @@ -39,4 +42,193 @@ async fn constructs(alice: Account) -> eyre::Result<()> { Ok(()) } -// TODO#q: add erc721 implementation related tests +#[e2e::test] +async fn mints(alice: Account) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let Erc721::balanceOfReturn { balance: initial_balance } = + contract.balanceOf(alice.address()).call().await?; + + let batch_size = uint!(10_U256); + let receipt = + receipt!(contract.init(vec![alice.address()], vec![batch_size]))?; + let from_token_id = U256::from(FIRST_CONSECUTIVE_ID); + let to_token_id = from_token_id + batch_size - uint!(1_U256); + assert!(receipt.emits(Erc721::ConsecutiveTransfer { + fromTokenId: from_token_id, + toTokenId: to_token_id, + fromAddress: Address::ZERO, + toAddress: alice.address(), + })); + + let Erc721::balanceOfReturn { balance: balance1 } = + contract.balanceOf(alice.address()).call().await?; + assert_eq!(balance1, initial_balance + 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_not_minted_consecutive(alice: Account) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let _ = watch!(contract.init(vec![alice.address()], vec![uint!(10_U256)]))?; + + let err = send!(contract.init(vec![alice.address()], vec![uint!(11_U256)])) + .expect_err("should not mint consecutive"); + + assert!(err.reverted_with(Erc721::ERC721ForbiddenBatchMint {})); + Ok(()) +} + +#[e2e::test] +async fn error_when_to_is_zero(alice: Account) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let err = send!(contract.init(vec![Address::ZERO], vec![uint!(10_U256)])) + .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 contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + let batch_size = U256::from(MAX_BATCH_SIZE + uint!(1_U96)); + let err = send!(contract.init(vec![alice.address()], vec![batch_size])) + .expect_err("should not mint consecutive"); + + assert!(err.reverted_with(Erc721::ERC721ExceededMaxBatchMint { + batchSize: U256::from(batch_size), + maxBatch: U256::from(MAX_BATCH_SIZE), + })); + Ok(()) +} + +#[e2e::test] +async fn transfers_from(alice: Account, bob: Account) -> eyre::Result<()> { + let contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + // Mint batches of 1000 tokens to Alice and Bob + let _ = watch!(contract.init( + vec![alice.address(), bob.address()], + vec![uint!(1000_U256), uint!(1000_U256)] + ))?; + 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 contract_addr = deploy(alice.url(), &alice.pk()).await?; + let contract = Erc721::new(contract_addr, &alice.wallet); + + // Mint batch of 1000 tokens to Alice + let _ = + watch!(contract.init(vec![alice.address()], vec![uint!(1000_U256)]))?; + 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(()) +} From cfafef0c867f1165d11568dd9a7f5dbc388c0ace Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Wed, 17 Jul 2024 12:50:47 +0400 Subject: [PATCH 59/95] update docs --- .../token/erc721/extensions/consecutive.rs | 72 ++++++++++--------- examples/erc721-consecutive/src/lib.rs | 2 +- 2 files changed, 41 insertions(+), 33 deletions(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index fa7e5945..a65a5bfc 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -1,8 +1,8 @@ //! 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 of large batches of tokens, during -//! contract construction only. For upgradeable contracts this implies that +//! 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. @@ -11,10 +11,16 @@ //! contract construction. This ability is regained after construction. During //! construction, only batch minting is allowed. //! -//! IMPORTANT: This extension does not call the {_update} function for tokens -//! minted in batch. Any logic added to this function through overrides will not -//! be triggered when token are minted in batch. You may want to also override -//! [`Erc721Consecutive::_increaseBalance`] or +//! IMPORTANT: Function [`Erc721Consecutive::_mint_consecutive`] is not suitable +//! to be called from constructor. Because of stylus sdk limitation. Function +//! [`Erc721Consecutive::_stop_mint_consecutive`] should be called to end +//! consecutive mint of tokens. After that, minting a token +//! with [`Erc721Consecutive::_mint`] will be possible. +//! +//! IMPORTANT: This extension does not call the [`Erc721::_update`] function for +//! tokens minted in batch. Any logic added to this function through overrides +//! will not be triggered when token are minted in batch. You may want to also +//! override [`Erc721Consecutive::_increaseBalance`] or //! [`Erc721Consecutive::_mintConsecutive`] to account for these mints. //! //! IMPORTANT: When overriding [`Erc721Consecutive::_mintConsecutive`], be @@ -131,7 +137,7 @@ impl MethodError for checkpoints::Error { } } -// TODO: add option to override these constants +// TODO: add an option to override these constants /// 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 @@ -147,6 +153,11 @@ impl Erc721Consecutive { /// Override of [`Erc721::_owner_of_inner`] 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_inner(&self, token_id: U256) -> Address { let owner = self.__owner_of_inner(token_id); // If token is owned by the core, or beyond consecutive range, return @@ -261,6 +272,28 @@ impl Erc721Consecutive { /// After construction,[`Erc721Consecutive::_mint_consecutive`] is no /// longer available and minting through [`Erc721Consecutive::_update`] /// becomes possible. + /// + /// # 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. + /// If consecutive mint wasn't finished yet (function + /// [`Self::_stop_mint_consecutive`] wasn't called) error + /// [`Error::ERC721ForbiddenMint`] is returned. + /// + /// # Events + /// + /// Emits a [`Transfer`] event. pub fn _update( &mut self, to: Address, @@ -405,17 +438,11 @@ impl Erc721Consecutive { /// 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`. - /// - /// # Arguments - /// - /// * `&self` - Read access to the contract's state. - /// * `token_id` - Token id as a number. #[must_use] fn __owner_of_inner(&self, token_id: U256) -> Address { self.erc721._owners.get(token_id) } - // TODO#q: move arguments and errors documentation to the public function /// 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. @@ -426,25 +453,6 @@ impl Erc721Consecutive { /// /// NOTE: If overriding this function in a way that tracks balances, see /// also [`Self::_increase_balance`]. - /// - /// # 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 [`Error::NonexistentToken`] is returned. - /// If `auth` is not `Address::ZERO` and `auth` does not have a right to - /// approve this token, then the error - /// [`Error::InsufficientApproval`] is returned. - /// - /// # Events - /// - /// Emits a [`Transfer`] event. fn __update( &mut self, to: Address, diff --git a/examples/erc721-consecutive/src/lib.rs b/examples/erc721-consecutive/src/lib.rs index 56b1a3a1..aa2b87db 100644 --- a/examples/erc721-consecutive/src/lib.rs +++ b/examples/erc721-consecutive/src/lib.rs @@ -34,7 +34,7 @@ impl Erc721ConsecutiveExample { for i in 0..len { let receiver = receivers[i]; let batch = batches[i]; - let token_id = self + let _ = self .erc721_consecutive ._mint_consecutive(receiver, U96::from(batch))?; } From 303996cd37699cd01a3722f718ee1219a9c3c418 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Wed, 17 Jul 2024 13:12:01 +0400 Subject: [PATCH 60/95] ++ --- contracts/src/token/erc721/extensions/consecutive.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index a65a5bfc..c36f5faa 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -71,8 +71,8 @@ sol_storage! { sol! { /// Emitted when the tokens from `fromTokenId` to `toTokenId` are transferred from `fromAddress` to `toAddress`. /// - /// * `fromTokenId` - First token being transfered. - /// * `toTokenId` - Last token being transfered. + /// * `fromTokenId` - First token being transferred. + /// * `toTokenId` - Last token being transferred. /// * `fromAddress` - Address from which tokens will be transferred. /// * `toAddress` - Address where the tokens will be transferred to. event ConsecutiveTransfer( From 33a6c48d2f5de1e7d61c0774c725f542589fa460 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Wed, 17 Jul 2024 13:14:14 +0400 Subject: [PATCH 61/95] ++ --- contracts/src/token/erc721/extensions/consecutive.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index c36f5faa..5a9c0ca5 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -44,7 +44,7 @@ use crate::{ erc721::{ Approval, ERC721IncorrectOwner, ERC721InvalidApprover, ERC721InvalidReceiver, ERC721InvalidSender, ERC721NonexistentToken, - Erc721, IERC721Receiver, IErc721, Transfer, + Erc721, IErc721, Transfer, }, }, utils::{ From c6cc2cba0710a96ea36ba95c11b56ece8cb73690 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Fri, 19 Jul 2024 16:08:54 +0400 Subject: [PATCH 62/95] ++ --- .../token/erc721/extensions/consecutive.rs | 47 +++---------------- 1 file changed, 7 insertions(+), 40 deletions(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index 5a9c0ca5..7d2d26ef 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -59,10 +59,11 @@ use crate::{ sol_storage! { /// State of an [`Erc72Erc721Consecutive`] token. + #[cfg_attr(all(test, feature = "std"), derive(motsu::DefaultStorageLayout))] pub struct Erc721Consecutive { Erc721 erc721; Trace160 _sequential_ownership; - BitMap _sequentian_burn; + BitMap _sequential_burn; /// Initialization marker. If true this means that consecutive mint was already triggered. bool _initialized } @@ -173,7 +174,7 @@ impl Erc721Consecutive { // Otherwise, check the token was not burned, and fetch ownership from // the anchors. // NOTE: no need for safe cast, - if self._sequentian_burn.get(token_id) { + if self._sequential_burn.get(token_id) { Address::ZERO } else { self._sequential_ownership.lower_lookup(U96::from(token_id)).into() @@ -230,7 +231,7 @@ impl Erc721Consecutive { .into()); } - if batch_size > MAX_BATCH_SIZE.to() { + if batch_size > MAX_BATCH_SIZE { return Err(ERC721ExceededMaxBatchMint { batchSize: U256::from(batch_size), maxBatch: U256::from(MAX_BATCH_SIZE), @@ -239,8 +240,7 @@ impl Erc721Consecutive { } let last = next + batch_size - uint!(1_U96); - self._sequential_ownership - .push(last, U160::from_be_bytes(to.into_array()))?; + self._sequential_ownership.push(last, to.into())?; self.erc721._increase_balance(to, U128::from(batch_size)); evm::log(ConsecutiveTransfer { @@ -310,10 +310,10 @@ impl Erc721Consecutive { // record burn if to == Address::ZERO // if we burn && token_id < U256::from(self._next_consecutive_id()) // and the tokenId was minted in a batch - && !self._sequentian_burn.get(token_id) + && !self._sequential_burn.get(token_id) // and the token was never marked as burnt { - self._sequentian_burn.set(token_id); + self._sequential_burn.set(token_id); } Ok(previous_owner) @@ -840,40 +840,7 @@ mod test { }, }; - // TODO#q: add support for complex contracts creation for motsu - impl Default for Erc721Consecutive { - fn default() -> Self { - #[derive(Default)] - struct StorageTypeFactory { - root: U256, - } - impl StorageTypeFactory { - fn create(&mut self) -> T { - let instance = unsafe { T::new(self.root, 0) }; - self.root += U256::from(T::SLOT_BYTES); - instance - } - } - - let mut factory = StorageTypeFactory::default(); - Erc721Consecutive { - erc721: Erc721 { - _owners: factory.create(), - _balances: factory.create(), - _token_approvals: factory.create(), - _operator_approvals: factory.create(), - }, - _sequential_ownership: Trace160 { - _checkpoints: factory.create(), - }, - _sequentian_burn: BitMap { _data: factory.create() }, - _initialized: factory.create(), - } - } - } - const BOB: Address = address!("F4EaCDAbEf3c8f1EdE91b6f2A6840bc2E4DD3526"); - const DAVE: Address = address!("0BB78F7e7132d1651B4Fd884B7624394e92156F1"); fn init( contract: &mut Erc721Consecutive, From ee6d83ae4a506b66c29866a7a3e04a04d6bfc265 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Fri, 19 Jul 2024 16:11:06 +0400 Subject: [PATCH 63/95] ++ --- contracts/src/token/erc721/extensions/consecutive.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index 7d2d26ef..7646cf31 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -173,10 +173,10 @@ impl Erc721Consecutive { // Otherwise, check the token was not burned, and fetch ownership from // the anchors. - // NOTE: no need for safe cast, 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() } } From fcd0b183467cc531163e68a5a2d35cf879b71d5e Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Fri, 19 Jul 2024 16:13:49 +0400 Subject: [PATCH 64/95] ++ --- contracts/src/token/erc721/extensions/consecutive.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index 7646cf31..6f4d0ad7 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -219,6 +219,7 @@ impl Erc721Consecutive { ) -> Result { let next = self._next_consecutive_id(); + // Minting a batch of size 0 is a no-op. if batch_size > U96::ZERO { if self._initialized.get() { return Err(ERC721ForbiddenBatchMint {}.into()); @@ -239,10 +240,15 @@ impl Erc721Consecutive { .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 sequentialOwnership checkpoint is attributing + // ownership of `batchSize` new tokens to account `to`. self.erc721._increase_balance(to, U128::from(batch_size)); + evm::log(ConsecutiveTransfer { fromTokenId: next.to::(), toTokenId: last.to::(), From cdd3507edf2df74901997a78d301397aef0b714a Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Fri, 19 Jul 2024 16:16:13 +0400 Subject: [PATCH 65/95] ++ --- contracts/src/token/erc721/extensions/consecutive.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index 6f4d0ad7..60625f29 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -61,8 +61,11 @@ sol_storage! { /// State of an [`Erc72Erc721Consecutive`] token. #[cfg_attr(all(test, feature = "std"), derive(motsu::DefaultStorageLayout))] 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; /// Initialization marker. If true this means that consecutive mint was already triggered. bool _initialized From e7daf7ec50be7ec11580e6efd044393971e01678 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Fri, 19 Jul 2024 16:32:29 +0400 Subject: [PATCH 66/95] ++ --- contracts/src/token/erc721/extensions/consecutive.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index 60625f29..2f919719 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -52,7 +52,7 @@ use crate::{ structs::{ bitmap::BitMap, checkpoints, - checkpoints::{Trace160, U160, U96}, + checkpoints::{Trace160, U96}, }, }, }; From 0d7d5095e6f367174318367cc5892c3940893310 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Fri, 19 Jul 2024 16:34:02 +0400 Subject: [PATCH 67/95] ++ --- contracts/src/token/erc721/extensions/consecutive.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index 2f919719..b94b516b 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -167,7 +167,6 @@ impl Erc721Consecutive { // If token is owned by the core, or beyond consecutive range, return // base value if owner != Address::ZERO - || token_id > U256::from(U96::MAX) || token_id > U256::from(U96::MAX) || token_id < U256::from(FIRST_CONSECUTIVE_ID) { From ce6202bb95e8fde19e0da8b4574296181ba3f3da Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Fri, 19 Jul 2024 16:38:58 +0400 Subject: [PATCH 68/95] ++ --- contracts/src/token/erc721/extensions/consecutive.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index b94b516b..2fddbd0e 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -167,8 +167,8 @@ impl Erc721Consecutive { // If token is owned by the core, or beyond consecutive range, return // base value if owner != Address::ZERO - || token_id > U256::from(U96::MAX) || token_id < U256::from(FIRST_CONSECUTIVE_ID) + || token_id > U256::from(U96::MAX) { return owner; } From fc7e6a0f6a7e3e1a0e3cb3ca80c48cef714b02c8 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Tue, 23 Jul 2024 13:56:01 +0400 Subject: [PATCH 69/95] ++ --- .../token/erc721/extensions/consecutive.rs | 76 +++++++++---------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index 2fddbd0e..34a9efa4 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -58,8 +58,7 @@ use crate::{ }; sol_storage! { - /// State of an [`Erc72Erc721Consecutive`] token. - #[cfg_attr(all(test, feature = "std"), derive(motsu::DefaultStorageLayout))] + /// State of an [`Erc721Consecutive`] token. pub struct Erc721Consecutive { /// Erc721 contract storage. Erc721 erc721; @@ -73,17 +72,17 @@ sol_storage! { } sol! { - /// Emitted when the tokens from `fromTokenId` to `toTokenId` are transferred from `fromAddress` to `toAddress`. + /// Emitted when the tokens from `from_token_id` to `to_token_id` are transferred from `from_address` to `to_address`. /// - /// * `fromTokenId` - First token being transferred. - /// * `toTokenId` - Last token being transferred. - /// * `fromAddress` - Address from which tokens will be transferred. - /// * `toAddress` - Address where the tokens will be transferred to. + /// * `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 fromTokenId, - uint256 toTokenId, - address indexed fromAddress, - address indexed toAddress + uint256 indexed from_token_id, + uint256 to_token_id, + address indexed from_address, + address indexed to_address ); } @@ -111,11 +110,12 @@ sol! { error ERC721ForbiddenBatchBurn(); } +/// An [`Erc721Consecutive`] error. #[derive(SolidityError, Debug)] pub enum Error { - /// Error type from erc721 contract [`erc721::Error`] + /// Error type from [`Erc721`] contract [`erc721::Error`]. Erc721(erc721::Error), - /// Error type from checkpoint contract [`checkpoints::Error`] + /// Error type from checkpoint contract [`checkpoints::Error`]. Checkpoints(checkpoints::Error), /// Batch mint is restricted to the constructor. /// Any batch mint not emitting the [`IERC721::Transfer`] event outside of @@ -149,7 +149,7 @@ impl MethodError for checkpoints::Error { pub const MAX_BATCH_SIZE: U96 = uint!(5000_U96); /// Used to offset the first token id in -/// [`Erc721Consecutive::_next_consecutive_id`] +/// [`Erc721Consecutive::_next_consecutive_id`]. pub const FIRST_CONSECUTIVE_ID: U96 = uint!(0_U96); /// Consecutive extension related implementation: @@ -165,7 +165,7 @@ impl Erc721Consecutive { pub fn _owner_of_inner(&self, token_id: U256) -> Address { let owner = self.__owner_of_inner(token_id); // If token is owned by the core, or beyond consecutive range, return - // base value + // base value. if owner != Address::ZERO || token_id < U256::from(FIRST_CONSECUTIVE_ID) || token_id > U256::from(U96::MAX) @@ -206,9 +206,9 @@ impl Erc721Consecutive { /// /// # Errors /// - /// If to is [`Address::ZERO`] error [`rc721::Error::InvalidReceiver`] is - /// returned. - /// If batch size exceeds [`MAX_BATCH_SIZE`] error + /// If `to` is [`Address::ZERO`], then the error + /// [`rc721::Error::InvalidReceiver`] is returned. + /// If `batch_size` exceeds [`MAX_BATCH_SIZE`], then the error /// [`Error::ERC721ExceededMaxBatchMint`] is returned. /// /// # Events @@ -248,14 +248,14 @@ impl Erc721Consecutive { // The invariant required by this function is preserved because the // new sequentialOwnership checkpoint is attributing - // ownership of `batchSize` new tokens to account `to`. + // ownership of `batch_size` new tokens to account `to`. self.erc721._increase_balance(to, U128::from(batch_size)); evm::log(ConsecutiveTransfer { - fromTokenId: next.to::(), - toTokenId: last.to::(), - fromAddress: Address::ZERO, - toAddress: to, + from_token_id: next.to::(), + to_token_id: last.to::(), + from_address: Address::ZERO, + to_address: to, }); }; Ok(next) @@ -310,16 +310,16 @@ impl Erc721Consecutive { ) -> Result { let previous_owner = self.__update(to, token_id, auth)?; - // only mint after construction + // only mint after construction. if previous_owner == Address::ZERO && !self._initialized.get() { return Err(ERC721ForbiddenMint {}.into()); } - // record burn - if to == Address::ZERO // if we burn - && token_id < U256::from(self._next_consecutive_id()) // and the tokenId was minted in a batch + // record burn. + if to == Address::ZERO // if we burn. + && token_id < U256::from(self._next_consecutive_id()) // and the tokenId was minted in a batch. && !self._sequential_burn.get(token_id) - // and the token was never marked as burnt + // and the token was never marked as burnt. { self._sequential_burn.set(token_id); } @@ -435,7 +435,7 @@ impl IErc721 for Erc721Consecutive { } } -// erc721 related implementation: +// ERC-721 related implementation: impl Erc721Consecutive { /// Returns the owner of the `token_id`. Does NOT revert if the token /// doesn't exist. @@ -826,7 +826,7 @@ impl Erc721Consecutive { } #[cfg(all(test, feature = "std"))] -mod test { +mod tests { use alloy_primitives::{address, uint, Address, U256}; use stylus_sdk::{msg, prelude::StorageType}; @@ -884,7 +884,7 @@ mod test { .expect("should return the balance of Alice"); assert_eq!(balance1, initial_balance + U256::from(init_tokens_count)); - // Check non-consecutive mint + // 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 @@ -949,7 +949,7 @@ mod test { let alice = msg::sender(); let bob = BOB; - // Mint batches of 1000 tokens to Alice and Bob + // Mint batches of 1000 tokens to Alice and Bob. let [first_consecutive_token_id, _] = init( contract, vec![alice, bob], @@ -958,7 +958,7 @@ mod test { .try_into() .expect("should have two elements in return vec"); - // Transfer first consecutive token from Alice to Bob + // 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"); @@ -968,7 +968,7 @@ mod test { .expect("token should be owned"); assert_eq!(owner, bob); - // Check that balances changed + // Check that balances changed. let alice_balance = contract .balance_of(alice) .expect("should return the balance of Alice"); @@ -985,7 +985,7 @@ mod test { .expect("should return the balance of Alice"); assert_eq!(alice_balance, uint!(1000_U256)); - // Check transfer of the token that wasn't minted consecutive + // 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"); @@ -999,13 +999,13 @@ mod test { fn burns(contract: Erc721Consecutive) { let alice = msg::sender(); - // Mint batch of 1000 tokens to Alice + // 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 + // Check consecutive token burn. contract ._burn(U256::from(first_consecutive_token_id)) .expect("should burn token"); @@ -1025,7 +1025,7 @@ mod test { if token_id == U256::from(first_consecutive_token_id) )); - // Check non-consecutive token burn + // Check non-consecutive token burn. let non_consecutive_token_id = random_token_id(); contract ._mint(alice, non_consecutive_token_id) From ef270cb235bc49307fb14b2ca87c2fec8ac37ebf Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Tue, 23 Jul 2024 14:48:55 +0400 Subject: [PATCH 70/95] inline __owner_of_inner function --- .../src/token/erc721/extensions/consecutive.rs | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index 34a9efa4..19e7a563 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -163,7 +163,7 @@ impl Erc721Consecutive { /// * `&self` - Read access to the contract's state. /// * `token_id` - Token id as a number. pub fn _owner_of_inner(&self, token_id: U256) -> Address { - let owner = self.__owner_of_inner(token_id); + let owner = self.erc721._owner_of_inner(token_id); // If token is owned by the core, or beyond consecutive range, return // base value. if owner != Address::ZERO @@ -437,20 +437,6 @@ impl IErc721 for Erc721Consecutive { // ERC-721 related implementation: impl Erc721Consecutive { - /// Returns the owner of the `token_id`. Does NOT revert if the token - /// doesn't exist. - /// - /// IMPORTANT: Any overrides to this function that add ownership of tokens - /// 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`. - #[must_use] - fn __owner_of_inner(&self, token_id: U256) -> Address { - self.erc721._owners.get(token_id) - } - /// 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. From 7e618de31b40ddeb7e7cac44a4eb6a07943c29be Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Tue, 23 Jul 2024 16:16:33 +0400 Subject: [PATCH 71/95] ++ --- .../token/erc721/extensions/consecutive.rs | 251 +++++++++--------- contracts/src/token/erc721/mod.rs | 4 +- 2 files changed, 128 insertions(+), 127 deletions(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index 19e7a563..c9816cf6 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -129,6 +129,8 @@ pub enum Error { ForbiddenBatchBurn(ERC721ForbiddenBatchBurn), } +unsafe impl TopLevelStorage for Erc721Consecutive {} + impl MethodError for erc721::Error { fn encode(self) -> alloc::vec::Vec { self.into() @@ -141,6 +143,98 @@ impl MethodError for checkpoints::Error { } } +// ************** 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 != 0`). 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_inner(token_id)) + } + + fn is_approved_for_all(&self, owner: Address, operator: Address) -> bool { + self.erc721.is_approved_for_all(owner, operator) + } +} + // TODO: add an option to override these constants /// Maximum size of a batch of consecutive tokens. This is designed to limit @@ -152,7 +246,7 @@ pub const MAX_BATCH_SIZE: U96 = uint!(5000_U96); /// [`Erc721Consecutive::_next_consecutive_id`]. pub const FIRST_CONSECUTIVE_ID: U96 = uint!(0_U96); -/// Consecutive extension related implementation: +// ************** Consecutive ************** impl Erc721Consecutive { /// Override of [`Erc721::_owner_of_inner`] that checks the sequential /// ownership structure for tokens that have been minted as part of a @@ -207,9 +301,9 @@ impl Erc721Consecutive { /// # Errors /// /// If `to` is [`Address::ZERO`], then the error - /// [`rc721::Error::InvalidReceiver`] is returned. + /// [`erc721::Error::InvalidReceiver`] is returned. /// If `batch_size` exceeds [`MAX_BATCH_SIZE`], then the error - /// [`Error::ERC721ExceededMaxBatchMint`] is returned. + /// [`Error::ExceededMaxBatchMint`] is returned. /// /// # Events /// @@ -297,7 +391,7 @@ impl Erc721Consecutive { /// [`erc721::Error::InsufficientApproval`] is returned. /// If consecutive mint wasn't finished yet (function /// [`Self::_stop_mint_consecutive`] wasn't called) error - /// [`Error::ERC721ForbiddenMint`] is returned. + /// [`Error::ForbiddenMint`] is returned. /// /// # Events /// @@ -342,100 +436,7 @@ impl Erc721Consecutive { } } -unsafe impl TopLevelStorage for Erc721Consecutive {} - -#[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 != 0`). 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_inner(token_id)) - } - - fn is_approved_for_all(&self, owner: Address, operator: Address) -> bool { - self.erc721.is_approved_for_all(owner, operator) - } -} - -// ERC-721 related implementation: +// ************** 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`. @@ -446,7 +447,7 @@ impl Erc721Consecutive { /// token, or approved to operate on the token (by the owner). /// /// NOTE: If overriding this function in a way that tracks balances, see - /// also [`Self::_increase_balance`]. + /// also [`Erc721::_increase_balance`]. fn __update( &mut self, to: Address, @@ -497,9 +498,9 @@ impl Erc721Consecutive { /// # Errors /// /// If `token_id` already exists, then the error - /// [`Error::InvalidSender`] is returned. + /// [`erc721::Error::InvalidSender`] is returned. /// If `to` is `Address::ZERO`, then the error - /// [`Error::InvalidReceiver`] is returned. + /// [`erc721::Error::InvalidReceiver`] is returned. /// /// # Requirements: /// @@ -531,7 +532,7 @@ impl Erc721Consecutive { /// and checks for `to`'s acceptance. /// /// An additional `data` parameter is forwarded to - /// [`IERC721Receiver::on_erc_721_received`] to contract recipients. + /// [`erc721::IERC721Receiver::on_erc_721_received`] to contract recipients. /// /// # Arguments /// @@ -539,24 +540,24 @@ impl Erc721Consecutive { /// * `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 `token_id` already exists, then the error - /// [`Error::InvalidSender`] is returned. + /// [`erc721::Error::InvalidSender`] is returned. /// If `to` is `Address::ZERO`, then the error - /// [`Error::InvalidReceiver`] is returned. - /// If [`IERC721Receiver::on_erc_721_received`] hasn't returned its + /// [`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 - /// [`Error::InvalidReceiver`] is returned. + /// [`erc721::Error::InvalidReceiver`] is returned. /// /// # Requirements: /// /// * `token_id` must not exist. /// * If `to` refers to a smart contract, it must implement - /// [`IERC721Receiver::on_erc_721_received`], which is called upon a - /// `safe_transfer`. + /// [`erc721::IERC721Receiver::on_erc_721_received`], which is called upon + /// a `safe_transfer`. /// /// # Events /// @@ -591,7 +592,7 @@ impl Erc721Consecutive { /// # Errors /// /// If token does not exist, then the error - /// [`Error::NonexistentToken`] is returned. + /// [`erc721::Error::NonexistentToken`] is returned. /// /// # Requirements: /// @@ -627,11 +628,11 @@ impl Erc721Consecutive { /// # Errors /// /// If `to` is `Address::ZERO`, then the error - /// [`Error::InvalidReceiver`] is returned. + /// [`erc721::Error::InvalidReceiver`] is returned. /// If `token_id` does not exist, then the error - /// [`Error::ERC721NonexistentToken`] is returned. + /// [`erc721::Error::NonexistentToken`] is returned. /// If the previous owner is not `from`, then the error - /// [`Error::IncorrectOwner`] is returned. + /// [`erc721::Error::IncorrectOwner`] is returned. /// /// # Requirements: /// @@ -679,9 +680,9 @@ impl Erc721Consecutive { /// `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 [`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. + /// 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 /// @@ -690,16 +691,16 @@ impl Erc721Consecutive { /// * `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. + /// [`erc721::Error::InvalidReceiver`] is returned. /// If `token_id` does not exist, then the error - /// [`Error::ERC721NonexistentToken`] is returned. + /// [`erc721::Error::NonexistentToken`] is returned. /// If the previous owner is not `from`, then the error - /// [`Error::IncorrectOwner`] is returned. + /// [`erc721::Error::IncorrectOwner`] is returned. /// /// # Requirements: /// @@ -707,8 +708,8 @@ impl Erc721Consecutive { /// * `to` cannot be the zero address. /// * `from` cannot be the zero address. /// * If `to` refers to a smart contract, it must implement - /// [`IERC721Receiver::on_erc_721_received`], which is called upon a - /// `safe_transfer`. + /// [`erc721::IERC721Receiver::on_erc_721_received`], which is called upon + /// a `safe_transfer`. /// /// # Events /// @@ -745,9 +746,9 @@ impl Erc721Consecutive { /// # Errors /// /// If the token does not exist, then the error - /// [`Error::NonexistentToken`] is returned. + /// [`erc721::Error::NonexistentToken`] is returned. /// If `auth` does not have a right to approve this token, then the error - /// [`Error::InvalidApprover`] is returned. + /// [`erc721::Error::InvalidApprover`] is returned. /// /// # Events /// @@ -793,7 +794,7 @@ impl Erc721Consecutive { /// # Errors /// /// If token does not exist, then the error - /// [`Error::NonexistentToken`] is returned. + /// [`erc721::Error::NonexistentToken`] is returned. /// /// # Arguments /// diff --git a/contracts/src/token/erc721/mod.rs b/contracts/src/token/erc721/mod.rs index 4115c596..00ebcb77 100644 --- a/contracts/src/token/erc721/mod.rs +++ b/contracts/src/token/erc721/mod.rs @@ -852,7 +852,7 @@ impl Erc721 { /// If `to` is `Address::ZERO`, then the error /// [`Error::InvalidReceiver`] is returned. /// If `token_id` does not exist, then the error - /// [`Error::ERC721NonexistentToken`] is returned. + /// [`Error::NonexistentToken`] is returned. /// If the previous owner is not `from`, then the error /// [`Error::IncorrectOwner`] is returned. /// @@ -916,7 +916,7 @@ impl Erc721 { /// If `to` is `Address::ZERO`, then the error /// [`Error::InvalidReceiver`] is returned. /// If `token_id` does not exist, then the error - /// [`Error::ERC721NonexistentToken`] is returned. + /// [`Error::NonexistentToken`] is returned. /// If the previous owner is not `from`, then the error /// [`Error::IncorrectOwner`] is returned. /// From c468cdf85b5b5b2c2ea1a1a0271de5060cee31f7 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Tue, 23 Jul 2024 17:10:20 +0400 Subject: [PATCH 72/95] ++ --- contracts/src/token/erc721/extensions/consecutive.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index c9816cf6..307849e9 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -409,12 +409,14 @@ impl Erc721Consecutive { return Err(ERC721ForbiddenMint {}.into()); } - // record burn. - if to == Address::ZERO // if we burn. - && token_id < U256::from(self._next_consecutive_id()) // and the tokenId was minted in a batch. + // if we burn. + if to == Address::ZERO + // and the tokenId 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) - // and the token was never marked as burnt. { + // record burn. self._sequential_burn.set(token_id); } From 56d5291018ec62e19aa43b091dd99f516dfa0050 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Tue, 23 Jul 2024 17:10:59 +0400 Subject: [PATCH 73/95] ++ --- contracts/src/token/erc721/extensions/consecutive.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index 307849e9..3393b70c 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -144,6 +144,7 @@ impl MethodError for checkpoints::Error { } // ************** ERC-721 External ************** + #[external] impl IErc721 for Erc721Consecutive { type Error = Error; @@ -247,6 +248,7 @@ pub const MAX_BATCH_SIZE: U96 = uint!(5000_U96); pub const FIRST_CONSECUTIVE_ID: U96 = uint!(0_U96); // ************** Consecutive ************** + impl Erc721Consecutive { /// Override of [`Erc721::_owner_of_inner`] that checks the sequential /// ownership structure for tokens that have been minted as part of a @@ -439,6 +441,7 @@ impl Erc721Consecutive { } // ************** 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`. From fe9b0cbb890f2029aa27ed8157de95a4142e0ecd Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Tue, 23 Jul 2024 17:25:12 +0400 Subject: [PATCH 74/95] ++ --- contracts/src/token/erc721/extensions/consecutive.rs | 12 ++++++------ contracts/src/token/erc721/mod.rs | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index 3393b70c..f39aed78 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -20,12 +20,12 @@ //! IMPORTANT: This extension does not call the [`Erc721::_update`] function for //! tokens minted in batch. Any logic added to this function through overrides //! will not be triggered when token are minted in batch. You may want to also -//! override [`Erc721Consecutive::_increaseBalance`] or -//! [`Erc721Consecutive::_mintConsecutive`] to account for these mints. +//! override [`Erc721Consecutive::_increase_balance`] or +//! [`Erc721Consecutive::_mint_consecutive`] to account for these mints. //! -//! IMPORTANT: When overriding [`Erc721Consecutive::_mintConsecutive`], be +//! IMPORTANT: When overriding [`Erc721Consecutive::_mint_consecutive`], be //! careful about call ordering. [`Erc721Consecutive::owner_of`] may return -//! invalid values during the [`Erc721Consecutive::_mintConsecutive`] +//! invalid values during the [`Erc721Consecutive::_mint_consecutive`] //! execution if the super call is not called first. To be safe, execute the //! super call before your custom logic. @@ -88,7 +88,7 @@ sol! { sol! { /// Batch mint is restricted to the constructor. - /// Any batch mint not emitting the [`IERC721::Transfer`] event outside of the constructor + /// Any batch mint not emitting the [`Transfer`] event outside of the constructor /// is non ERC-721 compliant. #[derive(Debug)] #[allow(missing_docs)] @@ -118,7 +118,7 @@ pub enum Error { /// Error type from checkpoint contract [`checkpoints::Error`]. Checkpoints(checkpoints::Error), /// Batch mint is restricted to the constructor. - /// Any batch mint not emitting the [`IERC721::Transfer`] event outside of + /// 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. diff --git a/contracts/src/token/erc721/mod.rs b/contracts/src/token/erc721/mod.rs index 00ebcb77..717e9ee9 100644 --- a/contracts/src/token/erc721/mod.rs +++ b/contracts/src/token/erc721/mod.rs @@ -272,7 +272,7 @@ pub trait IErc721 { /// * `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 /// @@ -294,7 +294,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 [`Self::_approve`] or [`Self::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`. @@ -370,7 +370,7 @@ pub trait IErc721 { /// /// If the token does not exist, then the error /// [`Error::NonexistentToken`] is returned. - /// If `auth` (param of [`Self::_approve`]) does not have a right to + /// If `auth` (param of [`Erc721::_approve`]) does not have a right to /// approve this token, then the error /// [`Error::InvalidApprover`] is returned. /// @@ -765,7 +765,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 /// @@ -909,7 +909,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 /// From 3995926aa7718be6242a8cd2b96c32d4e5d7c340 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Tue, 23 Jul 2024 17:40:16 +0400 Subject: [PATCH 75/95] ++ --- contracts/src/token/erc721/extensions/consecutive.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index f39aed78..d8ef35d8 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -17,12 +17,6 @@ //! consecutive mint of tokens. After that, minting a token //! with [`Erc721Consecutive::_mint`] will be possible. //! -//! IMPORTANT: This extension does not call the [`Erc721::_update`] function for -//! tokens minted in batch. Any logic added to this function through overrides -//! will not be triggered when token are minted in batch. You may want to also -//! override [`Erc721Consecutive::_increase_balance`] or -//! [`Erc721Consecutive::_mint_consecutive`] to account for these mints. -//! //! IMPORTANT: When overriding [`Erc721Consecutive::_mint_consecutive`], be //! careful about call ordering. [`Erc721Consecutive::owner_of`] may return //! invalid values during the [`Erc721Consecutive::_mint_consecutive`] From faccf8bca73c301c5bdc17d674b3e5a91e2e7a4f Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Tue, 23 Jul 2024 21:58:23 +0400 Subject: [PATCH 76/95] remove constructor.sol --- .../erc721-consecutive/src/constructor.sol | 23 ------------------- .../tests/erc721-consecutive.rs | 12 ++-------- lib/e2e/src/deploy.rs | 3 ++- 3 files changed, 4 insertions(+), 34 deletions(-) delete mode 100644 examples/erc721-consecutive/src/constructor.sol diff --git a/examples/erc721-consecutive/src/constructor.sol b/examples/erc721-consecutive/src/constructor.sol deleted file mode 100644 index f235c4ae..00000000 --- a/examples/erc721-consecutive/src/constructor.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.21; - -contract Erc721Example { - 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; - - mapping(uint256 => uint256) private _data; - Checkpoint160[] private _checkpoints; - bool _initialized; - - struct Checkpoint160 { - uint96 _key; - uint160 _value; - } - - constructor() { - _initialized = false; - } -} diff --git a/examples/erc721-consecutive/tests/erc721-consecutive.rs b/examples/erc721-consecutive/tests/erc721-consecutive.rs index e1f4d130..0e254e42 100644 --- a/examples/erc721-consecutive/tests/erc721-consecutive.rs +++ b/examples/erc721-consecutive/tests/erc721-consecutive.rs @@ -1,10 +1,6 @@ #![cfg(feature = "e2e")] -use alloy::{ - primitives::{Address, U256}, - sol, - sol_types::SolConstructor, -}; +use alloy::primitives::{Address, U256}; use alloy_primitives::uint; use e2e::{receipt, send, watch, Account, EventExt, Revert}; use openzeppelin_stylus::token::erc721::extensions::consecutive::{ @@ -15,17 +11,13 @@ use crate::abi::Erc721; mod abi; -sol!("src/constructor.sol"); - fn random_token_id() -> U256 { let num: u32 = rand::random(); U256::from(num) } 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, None).await } #[e2e::test] diff --git a/lib/e2e/src/deploy.rs b/lib/e2e/src/deploy.rs index 5bb8a8a6..a05e3b42 100644 --- a/lib/e2e/src/deploy.rs +++ b/lib/e2e/src/deploy.rs @@ -21,11 +21,12 @@ pub async fn deploy( 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, }, From 7b8590ffc30851bf259992066db20f70115da000 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Wed, 24 Jul 2024 14:20:59 +0400 Subject: [PATCH 77/95] use generics for params --- .../token/erc721/extensions/consecutive.rs | 90 ++++++++++--------- examples/erc721-consecutive/src/lib.rs | 17 +++- .../tests/erc721-consecutive.rs | 15 ++-- 3 files changed, 70 insertions(+), 52 deletions(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index d8ef35d8..46c5995b 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -24,6 +24,7 @@ //! super call before your custom logic. use alloc::vec; +use core::marker::PhantomData; use alloy_primitives::{uint, Address, U128, U256}; use alloy_sol_types::sol; @@ -51,9 +52,21 @@ use crate::{ }, }; +pub trait Erc721ConsecutiveParams { + /// 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. + const MAX_BATCH_SIZE: U96; + + /// Used to offset the first token id in + /// [`Erc721Consecutive::_next_consecutive_id`]. + const FIRST_CONSECUTIVE_ID: U96; +} + sol_storage! { /// State of an [`Erc721Consecutive`] token. - pub struct Erc721Consecutive { + pub struct Erc721Consecutive { /// Erc721 contract storage. Erc721 erc721; /// Checkpoint library contract for sequential ownership. @@ -61,7 +74,8 @@ sol_storage! { /// BitMap library contract for sequential burn of tokens. BitMap _sequential_burn; /// Initialization marker. If true this means that consecutive mint was already triggered. - bool _initialized + bool _initialized; + PhantomData phantom; } } @@ -123,7 +137,10 @@ pub enum Error { ForbiddenBatchBurn(ERC721ForbiddenBatchBurn), } -unsafe impl TopLevelStorage for Erc721Consecutive {} +unsafe impl TopLevelStorage + for Erc721Consecutive

+{ +} impl MethodError for erc721::Error { fn encode(self) -> alloc::vec::Vec { @@ -140,7 +157,7 @@ impl MethodError for checkpoints::Error { // ************** ERC-721 External ************** #[external] -impl IErc721 for Erc721Consecutive { +impl IErc721 for Erc721Consecutive { type Error = Error; fn balance_of(&self, owner: Address) -> Result { @@ -230,20 +247,9 @@ impl IErc721 for Erc721Consecutive { } } -// TODO: add an option to override these constants - -/// 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. -pub const MAX_BATCH_SIZE: U96 = uint!(5000_U96); - -/// Used to offset the first token id in -/// [`Erc721Consecutive::_next_consecutive_id`]. -pub const FIRST_CONSECUTIVE_ID: U96 = uint!(0_U96); - // ************** Consecutive ************** -impl Erc721Consecutive { +impl Erc721Consecutive { /// Override of [`Erc721::_owner_of_inner`] that checks the sequential /// ownership structure for tokens that have been minted as part of a /// batch, and not yet transferred. @@ -257,7 +263,7 @@ impl Erc721Consecutive { // If token is owned by the core, or beyond consecutive range, return // base value. if owner != Address::ZERO - || token_id < U256::from(FIRST_CONSECUTIVE_ID) + || token_id < U256::from(Params::FIRST_CONSECUTIVE_ID) || token_id > U256::from(U96::MAX) { return owner; @@ -279,7 +285,7 @@ impl Erc721Consecutive { /// /// Requirements: /// - /// - `batchSize` must not be greater than [`MAX_BATCH_SIZE`]. + /// - `batchSize` must not be greater than [`Params::MAX_BATCH_SIZE`]. /// - The function is called in the constructor of the contract (directly or /// indirectly). /// @@ -298,7 +304,7 @@ impl Erc721Consecutive { /// /// If `to` is [`Address::ZERO`], then the error /// [`erc721::Error::InvalidReceiver`] is returned. - /// If `batch_size` exceeds [`MAX_BATCH_SIZE`], then the error + /// If `batch_size` exceeds [`Params::MAX_BATCH_SIZE`], then the error /// [`Error::ExceededMaxBatchMint`] is returned. /// /// # Events @@ -324,10 +330,10 @@ impl Erc721Consecutive { .into()); } - if batch_size > MAX_BATCH_SIZE { + if batch_size > Params::MAX_BATCH_SIZE { return Err(ERC721ExceededMaxBatchMint { batchSize: U256::from(batch_size), - maxBatch: U256::from(MAX_BATCH_SIZE), + maxBatch: U256::from(Params::MAX_BATCH_SIZE), } .into()); } @@ -420,15 +426,15 @@ impl Erc721Consecutive { } /// Returns the next tokenId to mint using [`Self::_mint_consecutive`]. It - /// will return [`FIRST_CONSECUTIVE_ID`] if no consecutive tokenId has - /// been minted before. + /// will return [`Params::FIRST_CONSECUTIVE_ID`] if no consecutive tokenId + /// 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 => FIRST_CONSECUTIVE_ID, + None => Params::FIRST_CONSECUTIVE_ID, Some((latest_id, _)) => latest_id + uint!(1_U96), } } @@ -436,7 +442,7 @@ impl Erc721Consecutive { // ************** ERC-721 Internal ************** -impl Erc721Consecutive { +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. @@ -822,22 +828,26 @@ mod tests { erc721::{ extensions::consecutive::{ ERC721ExceededMaxBatchMint, ERC721ForbiddenBatchMint, - Erc721Consecutive, Error, MAX_BATCH_SIZE, + Erc721Consecutive, Erc721ConsecutiveParams, Error, }, tests::random_token_id, - ERC721InvalidReceiver, ERC721NonexistentToken, Erc721, IErc721, + ERC721InvalidReceiver, ERC721NonexistentToken, IErc721, }, }, - utils::structs::{ - bitmap::BitMap, - checkpoints::{Trace160, U96}, - }, + utils::structs::checkpoints::U96, }; + struct Params; + + impl Erc721ConsecutiveParams for Params { + const FIRST_CONSECUTIVE_ID: U96 = uint!(0_U96); + const MAX_BATCH_SIZE: U96 = uint!(5000_U96); + } + const BOB: Address = address!("F4EaCDAbEf3c8f1EdE91b6f2A6840bc2E4DD3526"); fn init( - contract: &mut Erc721Consecutive, + contract: &mut Erc721Consecutive, receivers: Vec

, batches: Vec, ) -> Vec { @@ -855,7 +865,7 @@ mod tests { } #[motsu::test] - fn mints(contract: Erc721Consecutive) { + fn mints(contract: Erc721Consecutive) { let alice = msg::sender(); let initial_balance = contract @@ -886,7 +896,7 @@ mod tests { } #[motsu::test] - fn error_when_not_minted_consecutive(contract: Erc721Consecutive) { + fn error_when_not_minted_consecutive(contract: Erc721Consecutive) { let alice = msg::sender(); init(contract, vec![alice], vec![uint!(10_U96)]); @@ -901,7 +911,7 @@ mod tests { } #[motsu::test] - fn error_when_to_is_zero(contract: Erc721Consecutive) { + fn error_when_to_is_zero(contract: Erc721Consecutive) { let err = contract ._mint_consecutive(Address::ZERO, uint!(11_U96)) .expect_err("should not mint consecutive"); @@ -914,9 +924,9 @@ mod tests { } #[motsu::test] - fn error_when_exceed_batch_size(contract: Erc721Consecutive) { + fn error_when_exceed_batch_size(contract: Erc721Consecutive) { let alice = msg::sender(); - let batch_size = MAX_BATCH_SIZE + uint!(1_U96); + let batch_size = Params::MAX_BATCH_SIZE + uint!(1_U96); let err = contract ._mint_consecutive(alice, batch_size) .expect_err("should not mint consecutive"); @@ -926,12 +936,12 @@ mod tests { batchSize, maxBatch }) - if batchSize == U256::from(batch_size) && maxBatch == U256::from(MAX_BATCH_SIZE) + if batchSize == U256::from(batch_size) && maxBatch == U256::from(Params::MAX_BATCH_SIZE) )); } #[motsu::test] - fn transfers_from(contract: Erc721Consecutive) { + fn transfers_from(contract: Erc721Consecutive) { let alice = msg::sender(); let bob = BOB; @@ -982,7 +992,7 @@ mod tests { } #[motsu::test] - fn burns(contract: Erc721Consecutive) { + fn burns(contract: Erc721Consecutive) { let alice = msg::sender(); // Mint batch of 1000 tokens to Alice. diff --git a/examples/erc721-consecutive/src/lib.rs b/examples/erc721-consecutive/src/lib.rs index aa2b87db..dd7a6654 100644 --- a/examples/erc721-consecutive/src/lib.rs +++ b/examples/erc721-consecutive/src/lib.rs @@ -3,23 +3,32 @@ extern crate alloc; use alloc::vec::Vec; -use alloy_primitives::{Address, U256}; +use alloy_primitives::{uint, Address, U256}; use openzeppelin_stylus::{ - token::erc721::extensions::consecutive::{Erc721Consecutive, Error}, + token::erc721::extensions::consecutive::{ + Erc721Consecutive, Erc721ConsecutiveParams, Error, + }, utils::structs::checkpoints::U96, }; use stylus_sdk::prelude::*; +pub struct Params; + +impl Erc721ConsecutiveParams for Params { + const FIRST_CONSECUTIVE_ID: U96 = uint!(0_U96); + const MAX_BATCH_SIZE: U96 = uint!(5000_U96); +} + sol_storage! { #[entrypoint] struct Erc721ConsecutiveExample { #[borrow] - Erc721Consecutive erc721_consecutive; + Erc721Consecutive erc721_consecutive; } } #[external] -#[inherit(Erc721Consecutive)] +#[inherit(Erc721Consecutive)] impl Erc721ConsecutiveExample { pub fn burn(&mut self, token_id: U256) -> Result<(), Error> { self.erc721_consecutive._burn(token_id) diff --git a/examples/erc721-consecutive/tests/erc721-consecutive.rs b/examples/erc721-consecutive/tests/erc721-consecutive.rs index 0e254e42..64842241 100644 --- a/examples/erc721-consecutive/tests/erc721-consecutive.rs +++ b/examples/erc721-consecutive/tests/erc721-consecutive.rs @@ -3,9 +3,8 @@ use alloy::primitives::{Address, U256}; use alloy_primitives::uint; use e2e::{receipt, send, watch, Account, EventExt, Revert}; -use openzeppelin_stylus::token::erc721::extensions::consecutive::{ - FIRST_CONSECUTIVE_ID, MAX_BATCH_SIZE, -}; +use erc721_consecutive_example::Params; +use openzeppelin_stylus::token::erc721::extensions::consecutive::Erc721ConsecutiveParams; use crate::abi::Erc721; @@ -45,7 +44,7 @@ async fn mints(alice: Account) -> eyre::Result<()> { let batch_size = uint!(10_U256); let receipt = receipt!(contract.init(vec![alice.address()], vec![batch_size]))?; - let from_token_id = U256::from(FIRST_CONSECUTIVE_ID); + let from_token_id = U256::from(Params::FIRST_CONSECUTIVE_ID); let to_token_id = from_token_id + batch_size - uint!(1_U256); assert!(receipt.emits(Erc721::ConsecutiveTransfer { fromTokenId: from_token_id, @@ -101,13 +100,13 @@ async fn error_when_exceed_batch_size(alice: Account) -> eyre::Result<()> { let contract_addr = deploy(alice.url(), &alice.pk()).await?; let contract = Erc721::new(contract_addr, &alice.wallet); - let batch_size = U256::from(MAX_BATCH_SIZE + uint!(1_U96)); + let batch_size = U256::from(Params::MAX_BATCH_SIZE + uint!(1_U96)); let err = send!(contract.init(vec![alice.address()], vec![batch_size])) .expect_err("should not mint consecutive"); assert!(err.reverted_with(Erc721::ERC721ExceededMaxBatchMint { batchSize: U256::from(batch_size), - maxBatch: U256::from(MAX_BATCH_SIZE), + maxBatch: U256::from(Params::MAX_BATCH_SIZE), })); Ok(()) } @@ -122,7 +121,7 @@ async fn transfers_from(alice: Account, bob: Account) -> eyre::Result<()> { vec![alice.address(), bob.address()], vec![uint!(1000_U256), uint!(1000_U256)] ))?; - let first_consecutive_token_id = U256::from(FIRST_CONSECUTIVE_ID); + let first_consecutive_token_id = U256::from(Params::FIRST_CONSECUTIVE_ID); // Transfer first consecutive token from Alice to Bob let _ = watch!(contract.transferFrom( @@ -170,7 +169,7 @@ async fn burns(alice: Account) -> eyre::Result<()> { // Mint batch of 1000 tokens to Alice let _ = watch!(contract.init(vec![alice.address()], vec![uint!(1000_U256)]))?; - let first_consecutive_token_id = U256::from(FIRST_CONSECUTIVE_ID); + let first_consecutive_token_id = U256::from(Params::FIRST_CONSECUTIVE_ID); // Check consecutive token burn let receipt = receipt!(contract.burn(first_consecutive_token_id))?; From e4a150b589fc9732e3c48538f813a572d6103a55 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Wed, 24 Jul 2024 14:43:44 +0400 Subject: [PATCH 78/95] ++ --- contracts/src/token/erc721/extensions/consecutive.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index 46c5995b..3e3814a7 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -285,7 +285,7 @@ impl Erc721Consecutive { /// /// Requirements: /// - /// - `batchSize` must not be greater than [`Params::MAX_BATCH_SIZE`]. + /// - `batchSize` must not be greater than [`Erc721ConsecutiveParams::MAX_BATCH_SIZE`]. /// - The function is called in the constructor of the contract (directly or /// indirectly). /// @@ -304,7 +304,7 @@ impl Erc721Consecutive { /// /// If `to` is [`Address::ZERO`], then the error /// [`erc721::Error::InvalidReceiver`] is returned. - /// If `batch_size` exceeds [`Params::MAX_BATCH_SIZE`], then the error + /// If `batch_size` exceeds [`Erc721ConsecutiveParams::MAX_BATCH_SIZE`], then the error /// [`Error::ExceededMaxBatchMint`] is returned. /// /// # Events @@ -426,7 +426,7 @@ impl Erc721Consecutive { } /// Returns the next tokenId to mint using [`Self::_mint_consecutive`]. It - /// will return [`Params::FIRST_CONSECUTIVE_ID`] if no consecutive tokenId + /// will return [`Erc721ConsecutiveParams::FIRST_CONSECUTIVE_ID`] if no consecutive tokenId /// has been minted before. /// /// # Arguments From 4f9e61b4e371a5c3f87abf8534be5ac88345e4f7 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Wed, 24 Jul 2024 15:01:11 +0400 Subject: [PATCH 79/95] ++ --- contracts/src/token/erc721/extensions/consecutive.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index 3e3814a7..49bc9f5a 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -285,7 +285,8 @@ impl Erc721Consecutive { /// /// Requirements: /// - /// - `batchSize` must not be greater than [`Erc721ConsecutiveParams::MAX_BATCH_SIZE`]. + /// - `batchSize` must not be greater than + /// [`Erc721ConsecutiveParams::MAX_BATCH_SIZE`]. /// - The function is called in the constructor of the contract (directly or /// indirectly). /// @@ -304,8 +305,8 @@ impl Erc721Consecutive { /// /// If `to` is [`Address::ZERO`], then the error /// [`erc721::Error::InvalidReceiver`] is returned. - /// If `batch_size` exceeds [`Erc721ConsecutiveParams::MAX_BATCH_SIZE`], then the error - /// [`Error::ExceededMaxBatchMint`] is returned. + /// If `batch_size` exceeds [`Erc721ConsecutiveParams::MAX_BATCH_SIZE`], + /// then the error [`Error::ExceededMaxBatchMint`] is returned. /// /// # Events /// @@ -426,8 +427,8 @@ impl Erc721Consecutive { } /// Returns the next tokenId to mint using [`Self::_mint_consecutive`]. It - /// will return [`Erc721ConsecutiveParams::FIRST_CONSECUTIVE_ID`] if no consecutive tokenId - /// has been minted before. + /// will return [`Erc721ConsecutiveParams::FIRST_CONSECUTIVE_ID`] if no + /// consecutive tokenId has been minted before. /// /// # Arguments /// From 8908bc9dd6e186473768de67c0a59d138dde55d2 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh <37006439+qalisander@users.noreply.github.com> Date: Tue, 6 Aug 2024 16:37:36 +0400 Subject: [PATCH 80/95] Use solidity constructor with consecutive erc721 (#220) Co-authored-by: alexfertel --- .github/workflows/e2e-tests.yml | 2 +- Cargo.lock | 5 +- Cargo.toml | 3 +- benches/src/lib.rs | 8 +- .../token/erc721/extensions/consecutive.rs | 171 +++++++----------- .../access-control/tests/access_control.rs | 11 +- examples/basic/script/src/main.rs | 14 +- examples/erc20/tests/erc20.rs | 10 +- .../erc721-consecutive/src/constructor.sol | 128 +++++++++++++ examples/erc721-consecutive/src/lib.rs | 39 +--- examples/erc721-consecutive/tests/abi.rs | 1 - .../tests/erc721-consecutive.rs | 126 +++++++------ examples/erc721-metadata/tests/erc721.rs | 6 +- examples/erc721/tests/erc721.rs | 6 +- examples/ownable/tests/ownable.rs | 7 +- lib/e2e/src/deploy.rs | 8 +- lib/e2e/src/error.rs | 24 ++- lib/e2e/src/lib.rs | 2 + lib/e2e/src/receipt.rs | 17 ++ 19 files changed, 350 insertions(+), 238 deletions(-) create mode 100644 examples/erc721-consecutive/src/constructor.sol create mode 100644 lib/e2e/src/receipt.rs 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 94af3c07..9b931715 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2211,9 +2211,8 @@ checksum = "57d8d8ce877200136358e0bbff3a77965875db3af755a11e1fa6b1b3e2df13ea" [[package]] name = "koba" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31de3702e0ac9b1f6927d12a8157af9c5796bc1caa9e89e43bda20b98af6685" +version = "0.1.3" +source = "git+https://github.com/OpenZeppelin/koba.git#ca595ab37cb34803b479fa89de7b3565e0c76e78" dependencies = [ "alloy", "brotli2", diff --git a/Cargo.toml b/Cargo.toml index 8267d69a..1a4f048f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,7 +78,8 @@ alloy-sol-types = { version = "0.3.1", default-features = false } const-hex = { version = "1.11.1", default-features = false } eyre = "0.6.8" -koba = "0.1.2" +# TODO#q: use crates.io published version of coba +koba = { version = "0.1.3", git = "https://github.com/OpenZeppelin/koba.git" } 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 index 49bc9f5a..f8496f6f 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -11,20 +11,18 @@ //! contract construction. This ability is regained after construction. During //! construction, only batch minting is allowed. //! -//! IMPORTANT: Function [`Erc721Consecutive::_mint_consecutive`] is not suitable -//! to be called from constructor. Because of stylus sdk limitation. Function -//! [`Erc721Consecutive::_stop_mint_consecutive`] should be called to end -//! consecutive mint of tokens. After that, minting a token -//! with [`Erc721Consecutive::_mint`] will be possible. +//! 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: When overriding [`Erc721Consecutive::_mint_consecutive`], be -//! careful about call ordering. [`Erc721Consecutive::owner_of`] may return -//! invalid values during the [`Erc721Consecutive::_mint_consecutive`] -//! execution if the super call is not called first. To be safe, execute the -//! super call before your custom logic. +//! 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 core::marker::PhantomData; use alloy_primitives::{uint, Address, U128, U256}; use alloy_sol_types::sol; @@ -52,30 +50,23 @@ use crate::{ }, }; -pub trait Erc721ConsecutiveParams { - /// 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. - const MAX_BATCH_SIZE: U96; - - /// Used to offset the first token id in - /// [`Erc721Consecutive::_next_consecutive_id`]. - const FIRST_CONSECUTIVE_ID: U96; -} - sol_storage! { /// State of an [`Erc721Consecutive`] token. - pub struct Erc721Consecutive { + 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; - /// Initialization marker. If true this means that consecutive mint was already triggered. - bool _initialized; - PhantomData phantom; + /// 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; } } @@ -137,10 +128,7 @@ pub enum Error { ForbiddenBatchBurn(ERC721ForbiddenBatchBurn), } -unsafe impl TopLevelStorage - for Erc721Consecutive

-{ -} +unsafe impl TopLevelStorage for Erc721Consecutive {} impl MethodError for erc721::Error { fn encode(self) -> alloc::vec::Vec { @@ -157,7 +145,7 @@ impl MethodError for checkpoints::Error { // ************** ERC-721 External ************** #[external] -impl IErc721 for Erc721Consecutive { +impl IErc721 for Erc721Consecutive { type Error = Error; fn balance_of(&self, owner: Address) -> Result { @@ -249,7 +237,7 @@ impl IErc721 for Erc721Consecutive { // ************** Consecutive ************** -impl Erc721Consecutive { +impl Erc721Consecutive { /// Override of [`Erc721::_owner_of_inner`] that checks the sequential /// ownership structure for tokens that have been minted as part of a /// batch, and not yet transferred. @@ -263,7 +251,7 @@ impl Erc721Consecutive { // If token is owned by the core, or beyond consecutive range, return // base value. if owner != Address::ZERO - || token_id < U256::from(Params::FIRST_CONSECUTIVE_ID) + || token_id < U256::from(self._first_consecutive_id()) || token_id > U256::from(U96::MAX) { return owner; @@ -286,7 +274,7 @@ impl Erc721Consecutive { /// Requirements: /// /// - `batchSize` must not be greater than - /// [`Erc721ConsecutiveParams::MAX_BATCH_SIZE`]. + /// [`Erc721Consecutive::_max_batch_size`]. /// - The function is called in the constructor of the contract (directly or /// indirectly). /// @@ -305,13 +293,14 @@ impl Erc721Consecutive { /// /// If `to` is [`Address::ZERO`], then the error /// [`erc721::Error::InvalidReceiver`] is returned. - /// If `batch_size` exceeds [`Erc721ConsecutiveParams::MAX_BATCH_SIZE`], + /// If `batch_size` exceeds [`Erc721Consecutive::_max_batch_size`], /// then the error [`Error::ExceededMaxBatchMint`] is returned. /// /// # Events /// /// Emits a [`ConsecutiveTransfer`] event. - pub fn _mint_consecutive( + #[cfg(all(test, feature = "std"))] + fn _mint_consecutive( &mut self, to: Address, batch_size: U96, @@ -320,10 +309,6 @@ impl Erc721Consecutive { // Minting a batch of size 0 is a no-op. if batch_size > U96::ZERO { - if self._initialized.get() { - return Err(ERC721ForbiddenBatchMint {}.into()); - } - if to.is_zero() { return Err(erc721::Error::InvalidReceiver( ERC721InvalidReceiver { receiver: Address::ZERO }, @@ -331,10 +316,10 @@ impl Erc721Consecutive { .into()); } - if batch_size > Params::MAX_BATCH_SIZE { + if batch_size > self._max_batch_size() { return Err(ERC721ExceededMaxBatchMint { batchSize: U256::from(batch_size), - maxBatch: U256::from(Params::MAX_BATCH_SIZE), + maxBatch: U256::from(self._max_batch_size()), } .into()); } @@ -358,26 +343,9 @@ impl Erc721Consecutive { Ok(next) } - /// Should be called to restrict consecutive mint after. - /// After this function being called, every call to - /// [`Self::_mint_consecutive`] will fail. - /// - /// # Arguments - /// - /// * `&self` - Write access to the contract's state. - pub fn _stop_mint_consecutive(&mut self) { - self._initialized.set(true); - } - /// Override of [`Erc721::_update`] that restricts normal minting to after /// construction. /// - /// WARNING: Using [`Erc721Consecutive`] prevents minting during - /// construction in favor of [`Erc721Consecutive::_mint_consecutive`]. - /// After construction,[`Erc721Consecutive::_mint_consecutive`] is no - /// longer available and minting through [`Erc721Consecutive::_update`] - /// becomes possible. - /// /// # Arguments /// /// * `&mut self` - Write access to the contract's state. @@ -392,9 +360,6 @@ impl Erc721Consecutive { /// 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. - /// If consecutive mint wasn't finished yet (function - /// [`Self::_stop_mint_consecutive`] wasn't called) error - /// [`Error::ForbiddenMint`] is returned. /// /// # Events /// @@ -405,12 +370,7 @@ impl Erc721Consecutive { token_id: U256, auth: Address, ) -> Result { - let previous_owner = self.__update(to, token_id, auth)?; - - // only mint after construction. - if previous_owner == Address::ZERO && !self._initialized.get() { - return Err(ERC721ForbiddenMint {}.into()); - } + let previous_owner = self._update_base(to, token_id, auth)?; // if we burn. if to == Address::ZERO @@ -427,7 +387,7 @@ impl Erc721Consecutive { } /// Returns the next tokenId to mint using [`Self::_mint_consecutive`]. It - /// will return [`Erc721ConsecutiveParams::FIRST_CONSECUTIVE_ID`] if no + /// will return [`Erc721Consecutive::_first_consecutive_id`] if no /// consecutive tokenId has been minted before. /// /// # Arguments @@ -435,15 +395,29 @@ impl Erc721Consecutive { /// * `&self` - Read access to the contract's state. fn _next_consecutive_id(&self) -> U96 { match self._sequential_ownership.latest_checkpoint() { - None => Params::FIRST_CONSECUTIVE_ID, + 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 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. + fn _max_batch_size(&self) -> U96 { + self._max_batch_size.get() + } } // ************** ERC-721 Internal ************** -impl Erc721Consecutive { +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. @@ -454,7 +428,7 @@ impl Erc721Consecutive { /// /// NOTE: If overriding this function in a way that tracks balances, see /// also [`Erc721::_increase_balance`]. - fn __update( + fn _update_base( &mut self, to: Address, token_id: U256, @@ -821,15 +795,14 @@ impl Erc721Consecutive { #[cfg(all(test, feature = "std"))] mod tests { use alloy_primitives::{address, uint, Address, U256}; - use stylus_sdk::{msg, prelude::StorageType}; + use stylus_sdk::msg; use crate::{ token::{ erc721, erc721::{ extensions::consecutive::{ - ERC721ExceededMaxBatchMint, ERC721ForbiddenBatchMint, - Erc721Consecutive, Erc721ConsecutiveParams, Error, + ERC721ExceededMaxBatchMint, Erc721Consecutive, Error, }, tests::random_token_id, ERC721InvalidReceiver, ERC721NonexistentToken, IErc721, @@ -838,21 +811,16 @@ mod tests { utils::structs::checkpoints::U96, }; - struct Params; - - impl Erc721ConsecutiveParams for Params { - const FIRST_CONSECUTIVE_ID: U96 = uint!(0_U96); - const MAX_BATCH_SIZE: U96 = uint!(5000_U96); - } - const BOB: Address = address!("F4EaCDAbEf3c8f1EdE91b6f2A6840bc2E4DD3526"); fn init( - contract: &mut Erc721Consecutive, + contract: &mut Erc721Consecutive, receivers: Vec

, batches: Vec, ) -> Vec { - let token_ids = receivers + 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)| { @@ -860,13 +828,11 @@ mod tests { ._mint_consecutive(to, batch_size) .expect("should mint consecutively") }) - .collect(); - contract._stop_mint_consecutive(); - token_ids + .collect() } #[motsu::test] - fn mints(contract: Erc721Consecutive) { + fn mints(contract: Erc721Consecutive) { let alice = msg::sender(); let initial_balance = contract @@ -897,22 +863,7 @@ mod tests { } #[motsu::test] - fn error_when_not_minted_consecutive(contract: Erc721Consecutive) { - let alice = msg::sender(); - - init(contract, vec![alice], vec![uint!(10_U96)]); - - let err = contract - ._mint_consecutive(BOB, uint!(11_U96)) - .expect_err("should not mint consecutive"); - assert!(matches!( - err, - Error::ForbiddenBatchMint(ERC721ForbiddenBatchMint {}) - )); - } - - #[motsu::test] - fn error_when_to_is_zero(contract: Erc721Consecutive) { + fn error_when_to_is_zero(contract: Erc721Consecutive) { let err = contract ._mint_consecutive(Address::ZERO, uint!(11_U96)) .expect_err("should not mint consecutive"); @@ -925,9 +876,9 @@ mod tests { } #[motsu::test] - fn error_when_exceed_batch_size(contract: Erc721Consecutive) { + fn error_when_exceed_batch_size(contract: Erc721Consecutive) { let alice = msg::sender(); - let batch_size = Params::MAX_BATCH_SIZE + uint!(1_U96); + let batch_size = contract._max_batch_size() + uint!(1_U96); let err = contract ._mint_consecutive(alice, batch_size) .expect_err("should not mint consecutive"); @@ -937,12 +888,12 @@ mod tests { batchSize, maxBatch }) - if batchSize == U256::from(batch_size) && maxBatch == U256::from(Params::MAX_BATCH_SIZE) + if batchSize == U256::from(batch_size) && maxBatch == U256::from(contract._max_batch_size()) )); } #[motsu::test] - fn transfers_from(contract: Erc721Consecutive) { + fn transfers_from(contract: Erc721Consecutive) { let alice = msg::sender(); let bob = BOB; @@ -993,7 +944,7 @@ mod tests { } #[motsu::test] - fn burns(contract: Erc721Consecutive) { + fn burns(contract: Erc721Consecutive) { let alice = msg::sender(); // Mint batch of 1000 tokens to Alice. 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/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/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 index dd7a6654..d847499e 100644 --- a/examples/erc721-consecutive/src/lib.rs +++ b/examples/erc721-consecutive/src/lib.rs @@ -1,56 +1,27 @@ #![cfg_attr(not(test), no_main, no_std)] extern crate alloc; -use alloc::vec::Vec; - -use alloy_primitives::{uint, Address, U256}; -use openzeppelin_stylus::{ - token::erc721::extensions::consecutive::{ - Erc721Consecutive, Erc721ConsecutiveParams, Error, - }, - utils::structs::checkpoints::U96, +use alloy_primitives::{Address, U256}; +use openzeppelin_stylus::token::erc721::extensions::consecutive::{ + Erc721Consecutive, Error, }; use stylus_sdk::prelude::*; -pub struct Params; - -impl Erc721ConsecutiveParams for Params { - const FIRST_CONSECUTIVE_ID: U96 = uint!(0_U96); - const MAX_BATCH_SIZE: U96 = uint!(5000_U96); -} - sol_storage! { #[entrypoint] struct Erc721ConsecutiveExample { #[borrow] - Erc721Consecutive erc721_consecutive; + Erc721Consecutive erc721_consecutive; } } #[external] -#[inherit(Erc721Consecutive)] +#[inherit(Erc721Consecutive)] impl Erc721ConsecutiveExample { pub fn burn(&mut self, token_id: U256) -> Result<(), Error> { self.erc721_consecutive._burn(token_id) } - pub fn init( - &mut self, - receivers: Vec
, - batches: Vec, - ) -> Result<(), Error> { - let len = batches.len(); - for i in 0..len { - let receiver = receivers[i]; - let batch = batches[i]; - let _ = self - .erc721_consecutive - ._mint_consecutive(receiver, U96::from(batch))?; - } - self.erc721_consecutive._stop_mint_consecutive(); - Ok(()) - } - 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 index 2f1f340d..55234523 100644 --- a/examples/erc721-consecutive/tests/abi.rs +++ b/examples/erc721-consecutive/tests/abi.rs @@ -18,7 +18,6 @@ sol!( function mint(address to, uint256 tokenId) external; function burn(uint256 tokenId) external; - function init(address[] memory receivers, uint256[] memory amounts) external; error ERC721InvalidOwner(address owner); error ERC721NonexistentToken(uint256 tokenId); diff --git a/examples/erc721-consecutive/tests/erc721-consecutive.rs b/examples/erc721-consecutive/tests/erc721-consecutive.rs index 64842241..8590daff 100644 --- a/examples/erc721-consecutive/tests/erc721-consecutive.rs +++ b/examples/erc721-consecutive/tests/erc721-consecutive.rs @@ -1,33 +1,53 @@ #![cfg(feature = "e2e")] -use alloy::primitives::{Address, U256}; +use alloy::{ + primitives::{Address, U256}, + rpc::types::TransactionReceipt, + sol, + sol_types::SolConstructor, +}; use alloy_primitives::uint; -use e2e::{receipt, send, watch, Account, EventExt, Revert}; -use erc721_consecutive_example::Params; -use openzeppelin_stylus::token::erc721::extensions::consecutive::Erc721ConsecutiveParams; +use e2e::{receipt, watch, Account, EventExt, ReceiptExt, Revert}; -use crate::abi::Erc721; +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(rpc_url: &str, private_key: &str) -> eyre::Result
{ - e2e::deploy(rpc_url, private_key, None).await +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 contract_addr = deploy(alice.url(), &alice.pk()).await?; - let contract = Erc721::new(contract_addr, &alice.wallet); - let alice_addr = alice.address(); let receivers = vec![alice_addr]; - let amounts = vec![uint!(10_U256)]; - let _ = watch!(contract.init(receivers, amounts))?; + 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(()) @@ -35,27 +55,22 @@ async fn constructs(alice: Account) -> eyre::Result<()> { #[e2e::test] async fn mints(alice: Account) -> eyre::Result<()> { - let contract_addr = deploy(alice.url(), &alice.pk()).await?; - let contract = Erc721::new(contract_addr, &alice.wallet); + 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); - let Erc721::balanceOfReturn { balance: initial_balance } = - contract.balanceOf(alice.address()).call().await?; - - let batch_size = uint!(10_U256); - let receipt = - receipt!(contract.init(vec![alice.address()], vec![batch_size]))?; - let from_token_id = U256::from(Params::FIRST_CONSECUTIVE_ID); - let to_token_id = from_token_id + batch_size - uint!(1_U256); assert!(receipt.emits(Erc721::ConsecutiveTransfer { - fromTokenId: from_token_id, - toTokenId: to_token_id, + 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, initial_balance + U256::from(batch_size)); + assert_eq!(balance1, U256::from(batch_size)); let token_id = random_token_id(); let _ = watch!(contract.mint(alice.address(), token_id))?; @@ -67,26 +82,12 @@ async fn mints(alice: Account) -> eyre::Result<()> { Ok(()) } -#[e2e::test] -async fn error_when_not_minted_consecutive(alice: Account) -> eyre::Result<()> { - let contract_addr = deploy(alice.url(), &alice.pk()).await?; - let contract = Erc721::new(contract_addr, &alice.wallet); - - let _ = watch!(contract.init(vec![alice.address()], vec![uint!(10_U256)]))?; - - let err = send!(contract.init(vec![alice.address()], vec![uint!(11_U256)])) - .expect_err("should not mint consecutive"); - - assert!(err.reverted_with(Erc721::ERC721ForbiddenBatchMint {})); - Ok(()) -} - #[e2e::test] async fn error_when_to_is_zero(alice: Account) -> eyre::Result<()> { - let contract_addr = deploy(alice.url(), &alice.pk()).await?; - let contract = Erc721::new(contract_addr, &alice.wallet); - - let err = send!(contract.init(vec![Address::ZERO], vec![uint!(10_U256)])) + 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 { @@ -97,31 +98,28 @@ async fn error_when_to_is_zero(alice: Account) -> eyre::Result<()> { #[e2e::test] async fn error_when_exceed_batch_size(alice: Account) -> eyre::Result<()> { - let contract_addr = deploy(alice.url(), &alice.pk()).await?; - let contract = Erc721::new(contract_addr, &alice.wallet); - - let batch_size = U256::from(Params::MAX_BATCH_SIZE + uint!(1_U96)); - let err = send!(contract.init(vec![alice.address()], vec![batch_size])) + 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(batch_size), - maxBatch: U256::from(Params::MAX_BATCH_SIZE), + 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 contract_addr = deploy(alice.url(), &alice.pk()).await?; - let contract = Erc721::new(contract_addr, &alice.wallet); + 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); - // Mint batches of 1000 tokens to Alice and Bob - let _ = watch!(contract.init( - vec![alice.address(), bob.address()], - vec![uint!(1000_U256), uint!(1000_U256)] - ))?; - let first_consecutive_token_id = U256::from(Params::FIRST_CONSECUTIVE_ID); + let first_consecutive_token_id = U256::from(FIRST_CONSECUTIVE_ID); // Transfer first consecutive token from Alice to Bob let _ = watch!(contract.transferFrom( @@ -163,13 +161,13 @@ async fn transfers_from(alice: Account, bob: Account) -> eyre::Result<()> { #[e2e::test] async fn burns(alice: Account) -> eyre::Result<()> { - let contract_addr = deploy(alice.url(), &alice.pk()).await?; - let contract = Erc721::new(contract_addr, &alice.wallet); - + let receivers = vec![alice.address()]; + let amounts = vec![1000_u128]; // Mint batch of 1000 tokens to Alice - let _ = - watch!(contract.init(vec![alice.address()], vec![uint!(1000_U256)]))?; - let first_consecutive_token_id = U256::from(Params::FIRST_CONSECUTIVE_ID); + 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))?; 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 64ec3924..666800c4 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 a05e3b42..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,7 +17,7 @@ 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; @@ -41,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") + } +} From 529548dd6a8b8b6dcaab5630bc8aa5b1a696c466 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Tue, 6 Aug 2024 16:56:26 +0400 Subject: [PATCH 81/95] ++ --- Cargo.lock | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2138460a..3408a7c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2226,8 +2226,9 @@ checksum = "57d8d8ce877200136358e0bbff3a77965875db3af755a11e1fa6b1b3e2df13ea" [[package]] name = "koba" -version = "0.1.3" -source = "git+https://github.com/OpenZeppelin/koba.git#ca595ab37cb34803b479fa89de7b3565e0c76e78" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e92e1148d087df999396266311bece9e5311c352821872cccaf5dc67117cfc" dependencies = [ "alloy", "brotli2", From cfcdbb90f27ac1b15f93e79223a596ff4ac86750 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Tue, 6 Aug 2024 18:08:16 +0400 Subject: [PATCH 82/95] ++ --- examples/ecdsa/tests/ecdsa.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 = From 8bdf906aa89a1b8110de2642355549e888aebb63 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Tue, 6 Aug 2024 18:09:26 +0400 Subject: [PATCH 83/95] ++ --- contracts/src/token/erc721/extensions/consecutive.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index f8496f6f..32a8a5f2 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -24,7 +24,7 @@ use alloc::vec; -use alloy_primitives::{uint, Address, U128, U256}; +use alloy_primitives::{uint, Address, U256}; use alloy_sol_types::sol; use stylus_proc::{external, sol_storage, SolidityError}; use stylus_sdk::{ From b5e37522ddb36efb2f344a86d2ad44fb676fb9fb Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Tue, 6 Aug 2024 18:16:03 +0400 Subject: [PATCH 84/95] ++ --- contracts/src/token/erc721/extensions/consecutive.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index 32a8a5f2..adde0c2a 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -24,7 +24,7 @@ use alloc::vec; -use alloy_primitives::{uint, Address, U256}; +use alloy_primitives::{uint, Address, U128, U256}; use alloy_sol_types::sol; use stylus_proc::{external, sol_storage, SolidityError}; use stylus_sdk::{ @@ -289,7 +289,7 @@ impl Erc721Consecutive { /// * `&self` - Write access to the contract's state. /// * `token_id` - Token id as a number. /// - /// # Errors + /// # Errors` /// /// If `to` is [`Address::ZERO`], then the error /// [`erc721::Error::InvalidReceiver`] is returned. From 3217d43682ca751d8dca10a3ad25b7af3386733c Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Tue, 6 Aug 2024 18:21:46 +0400 Subject: [PATCH 85/95] ++ --- contracts/src/token/erc721/extensions/consecutive.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index adde0c2a..aecf788e 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -24,7 +24,7 @@ use alloc::vec; -use alloy_primitives::{uint, Address, U128, U256}; +use alloy_primitives::{uint, Address, U256}; use alloy_sol_types::sol; use stylus_proc::{external, sol_storage, SolidityError}; use stylus_sdk::{ @@ -331,7 +331,10 @@ impl Erc721Consecutive { // The invariant required by this function is preserved because the // new sequentialOwnership checkpoint is attributing // ownership of `batch_size` new tokens to account `to`. - self.erc721._increase_balance(to, U128::from(batch_size)); + self.erc721._increase_balance( + to, + alloy_primitives::U128::from(batch_size), + ); evm::log(ConsecutiveTransfer { from_token_id: next.to::(), From 8c4267af6280cba119dc3bf9af9bfe8ab77d6f77 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Tue, 6 Aug 2024 19:16:16 +0400 Subject: [PATCH 86/95] ++ --- .../src/token/erc721/extensions/consecutive.rs | 6 +++--- .../erc721-consecutive/tests/erc721-consecutive.rs | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index aecf788e..cccb9c2a 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -131,13 +131,13 @@ pub enum Error { unsafe impl TopLevelStorage for Erc721Consecutive {} impl MethodError for erc721::Error { - fn encode(self) -> alloc::vec::Vec { + fn encode(self) -> Vec { self.into() } } impl MethodError for checkpoints::Error { - fn encode(self) -> alloc::vec::Vec { + fn encode(self) -> Vec { self.into() } } @@ -928,7 +928,7 @@ mod tests { 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 + // 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 diff --git a/examples/erc721-consecutive/tests/erc721-consecutive.rs b/examples/erc721-consecutive/tests/erc721-consecutive.rs index 8590daff..f60ae965 100644 --- a/examples/erc721-consecutive/tests/erc721-consecutive.rs +++ b/examples/erc721-consecutive/tests/erc721-consecutive.rs @@ -121,7 +121,7 @@ async fn transfers_from(alice: Account, bob: Account) -> eyre::Result<()> { let first_consecutive_token_id = U256::from(FIRST_CONSECUTIVE_ID); - // Transfer first consecutive token from Alice to Bob + // Transfer first consecutive token from Alice to Bob. let _ = watch!(contract.transferFrom( alice.address(), bob.address(), @@ -132,7 +132,7 @@ async fn transfers_from(alice: Account, bob: Account) -> eyre::Result<()> { contract.ownerOf(first_consecutive_token_id).call().await?; assert_eq!(ownerOf, bob.address()); - // Check that balances changed + // 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)); @@ -140,14 +140,14 @@ async fn transfers_from(alice: Account, bob: Account) -> eyre::Result<()> { contract.balanceOf(bob.address()).call().await?; assert_eq!(bob_balance, uint!(1000_U256) + uint!(1_U256)); - // Test non-consecutive mint + // 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 + // Test transfer of the token that wasn't minted consecutive. let _ = watch!(contract.transferFrom( alice.address(), bob.address(), @@ -163,13 +163,13 @@ async fn transfers_from(alice: Account, bob: Account) -> eyre::Result<()> { async fn burns(alice: Account) -> eyre::Result<()> { let receivers = vec![alice.address()]; let amounts = vec![1000_u128]; - // Mint batch of 1000 tokens to Alice + // 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 + // Check consecutive token burn. let receipt = receipt!(contract.burn(first_consecutive_token_id))?; assert!(receipt.emits(Erc721::Transfer { @@ -192,7 +192,7 @@ async fn burns(alice: Account) -> eyre::Result<()> { tokenId: first_consecutive_token_id })); - // Check non-consecutive token burn + // 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 } = From dc34b9332a9bdd948192ded95e3b2eb2722f982b Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Tue, 6 Aug 2024 20:51:32 +0400 Subject: [PATCH 87/95] ++ --- .../token/erc721/extensions/consecutive.rs | 26 +++++----- contracts/src/token/erc721/mod.rs | 52 ++++++++++--------- 2 files changed, 41 insertions(+), 37 deletions(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index cccb9c2a..9cb8db7d 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -131,13 +131,13 @@ pub enum Error { unsafe impl TopLevelStorage for Erc721Consecutive {} impl MethodError for erc721::Error { - fn encode(self) -> Vec { + fn encode(self) -> alloc::vec::Vec { self.into() } } impl MethodError for checkpoints::Error { - fn encode(self) -> Vec { + fn encode(self) -> alloc::vec::Vec { self.into() } } @@ -227,7 +227,7 @@ impl IErc721 for Erc721Consecutive { fn get_approved(&self, token_id: U256) -> Result { self._require_owned(token_id)?; - Ok(self.erc721._get_approved_inner(token_id)) + Ok(self.erc721._get_approved(token_id)) } fn is_approved_for_all(&self, owner: Address, operator: Address) -> bool { @@ -238,7 +238,7 @@ impl IErc721 for Erc721Consecutive { // ************** Consecutive ************** impl Erc721Consecutive { - /// Override of [`Erc721::_owner_of_inner`] that checks the sequential + /// 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. /// @@ -246,8 +246,8 @@ impl Erc721Consecutive { /// /// * `&self` - Read access to the contract's state. /// * `token_id` - Token id as a number. - pub fn _owner_of_inner(&self, token_id: U256) -> Address { - let owner = self.erc721._owner_of_inner(token_id); + 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 != Address::ZERO @@ -437,7 +437,7 @@ impl Erc721Consecutive { 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() { @@ -714,9 +714,11 @@ impl Erc721Consecutive { )?) } - /// 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 /// @@ -772,7 +774,7 @@ impl Erc721Consecutive { /// 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 /// @@ -784,7 +786,7 @@ impl Erc721Consecutive { /// * `&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(erc721::Error::NonexistentToken( ERC721NonexistentToken { token_id }, diff --git a/contracts/src/token/erc721/mod.rs b/contracts/src/token/erc721/mod.rs index 7e1b59e8..03d370fc 100644 --- a/contracts/src/token/erc721/mod.rs +++ b/contracts/src/token/erc721/mod.rs @@ -533,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 { @@ -549,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) } @@ -569,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) } @@ -595,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 @@ -645,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. /// @@ -696,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() { @@ -950,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 /// @@ -1041,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 /// @@ -1053,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()); } @@ -1860,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(); @@ -1902,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(); @@ -1916,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); } From 9ccd744ef6b21a9559fbdd3655e7996945133a50 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Thu, 8 Aug 2024 17:56:52 +0400 Subject: [PATCH 88/95] ++ --- contracts/src/token/erc721/mod.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/src/token/erc721/mod.rs b/contracts/src/token/erc721/mod.rs index 03d370fc..e9cebc44 100644 --- a/contracts/src/token/erc721/mod.rs +++ b/contracts/src/token/erc721/mod.rs @@ -1862,7 +1862,7 @@ mod tests { } #[motsu::test] - fn _owner_of_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"); @@ -1871,21 +1871,21 @@ mod tests { } #[motsu::test] - fn _owner_of_nonexistent_token(contract: Erc721) { + fn owner_of_nonexistent_token(contract: Erc721) { let token_id = random_token_id(); let owner = contract._owner_of(token_id); assert_eq!(Address::ZERO, owner); } #[motsu::test] - fn _get_approved_nonexistent_token(contract: Erc721) { + fn get_approved_nonexistent_token(contract: Erc721) { let token_id = random_token_id(); let approved = contract._get_approved(token_id); assert_eq!(Address::ZERO, approved); } #[motsu::test] - fn _get_approved_token_without_approval(contract: Erc721) { + fn get_approved_token_without_approval(contract: Erc721) { let alice = msg::sender(); let token_id = random_token_id(); @@ -1895,7 +1895,7 @@ mod tests { } #[motsu::test] - fn _get_approved_token_with_approval(contract: Erc721) { + fn get_approved_token_with_approval(contract: Erc721) { let alice = msg::sender(); let token_id = random_token_id(); @@ -1909,7 +1909,7 @@ mod tests { } #[motsu::test] - fn _get_approved_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(); From 22d096142830c0e63cefb8622f47a6c8d0a16005 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Thu, 8 Aug 2024 21:42:00 +0400 Subject: [PATCH 89/95] ++ --- .../src/token/erc721/extensions/consecutive.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index 9cb8db7d..b9d1d767 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -96,7 +96,7 @@ sol! { /// Exceeds the max number of mints per batch. #[derive(Debug)] #[allow(missing_docs)] - error ERC721ExceededMaxBatchMint(uint256 batchSize, uint256 maxBatch); + error ERC721ExceededMaxBatchMint(uint256 batch_size, uint256 max_batch); /// Individual minting is not allowed. #[derive(Debug)] @@ -268,14 +268,14 @@ impl Erc721Consecutive { } /// Mint a batch of tokens of length `batch_size` for `to`. Returns the - /// token id of the first token minted in the batch; if `batchSize` is + /// 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: /// - /// - `batchSize` must not be greater than + /// * `batch_size` must not be greater than /// [`Erc721Consecutive::_max_batch_size`]. - /// - The function is called in the constructor of the contract (directly or + /// * 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 @@ -318,8 +318,8 @@ impl Erc721Consecutive { if batch_size > self._max_batch_size() { return Err(ERC721ExceededMaxBatchMint { - batchSize: U256::from(batch_size), - maxBatch: U256::from(self._max_batch_size()), + batch_size: U256::from(batch_size), + max_batch: U256::from(self._max_batch_size()), } .into()); } @@ -890,10 +890,10 @@ mod tests { assert!(matches!( err, Error::ExceededMaxBatchMint(ERC721ExceededMaxBatchMint { - batchSize, - maxBatch + batch_size, + max_batch }) - if batchSize == U256::from(batch_size) && maxBatch == U256::from(contract._max_batch_size()) + if batch_size == U256::from(batch_size) && max_batch == U256::from(contract._max_batch_size()) )); } From 90029a4fb351e8c8f558f36878bb273cf39a48e2 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Thu, 8 Aug 2024 21:45:38 +0400 Subject: [PATCH 90/95] ++ --- contracts/src/token/erc721/extensions/consecutive.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index b9d1d767..98092105 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -329,7 +329,7 @@ impl Erc721Consecutive { self._sequential_ownership.push(last, to.into())?; // The invariant required by this function is preserved because the - // new sequentialOwnership checkpoint is attributing + // new sequential_ownership checkpoint is attributing // ownership of `batch_size` new tokens to account `to`. self.erc721._increase_balance( to, @@ -375,14 +375,14 @@ impl Erc721Consecutive { ) -> Result { let previous_owner = self._update_base(to, token_id, auth)?; - // if we burn. + // if we burn if to == Address::ZERO - // and the tokenId was minted in a batch. + // and the tokenId was minted in a batch && token_id < U256::from(self._next_consecutive_id()) - // and the token was never marked as burnt. + // and the token was never marked as burnt && !self._sequential_burn.get(token_id) { - // record burn. + // record burn self._sequential_burn.set(token_id); } From 5609163b3adc6f01b0b6b0c82fd8a821d5d3f6d6 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Thu, 8 Aug 2024 21:48:50 +0400 Subject: [PATCH 91/95] ++ --- contracts/src/token/erc721/extensions/consecutive.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index 98092105..0a00f959 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -377,7 +377,7 @@ impl Erc721Consecutive { // if we burn if to == Address::ZERO - // and the tokenId was minted in a batch + // 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) @@ -389,9 +389,9 @@ impl Erc721Consecutive { Ok(previous_owner) } - /// Returns the next tokenId to mint using [`Self::_mint_consecutive`]. It + /// Returns the next token_id to mint using [`Self::_mint_consecutive`]. It /// will return [`Erc721Consecutive::_first_consecutive_id`] if no - /// consecutive tokenId has been minted before. + /// consecutive token_id has been minted before. /// /// # Arguments /// From 503c1ebb93d7a8832e89cc57b7c60e8852c06c2f Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Thu, 8 Aug 2024 22:02:34 +0400 Subject: [PATCH 92/95] ++ --- .../token/erc721/extensions/consecutive.rs | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index 0a00f959..3fb4fc7d 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -199,7 +199,7 @@ impl IErc721 for Erc721Consecutive { } // Setting an "auth" argument enables the `_is_authorized` check which - // verifies that the token exists (`from != 0`). Therefore, it is + // 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 { @@ -250,7 +250,7 @@ impl Erc721Consecutive { let owner = self.erc721._owner_of(token_id); // If token is owned by the core, or beyond consecutive range, return // base value. - if owner != Address::ZERO + if !owner.is_zero() || token_id < U256::from(self._first_consecutive_id()) || token_id > U256::from(U96::MAX) { @@ -267,9 +267,10 @@ impl Erc721Consecutive { } } - /// Mint a batch of tokens of 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. + /// 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: /// @@ -278,18 +279,20 @@ impl Erc721Consecutive { /// * 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 + /// 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 `onERC721Received` on the receiver. + /// 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` + /// # Errors /// /// If `to` is [`Address::ZERO`], then the error /// [`erc721::Error::InvalidReceiver`] is returned. @@ -376,7 +379,7 @@ impl Erc721Consecutive { let previous_owner = self._update_base(to, token_id, auth)?; // if we burn - if to == Address::ZERO + 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 @@ -409,10 +412,10 @@ impl Erc721Consecutive { self._first_consecutive_id.get() } - /// 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. + /// 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() } From 4fb347f4c20d9990f29c6dc9b6351038060b2ece Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Mon, 12 Aug 2024 13:35:08 +0400 Subject: [PATCH 93/95] ++ --- .../src/token/erc721/extensions/consecutive.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index 3fb4fc7d..bfdc5c78 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -294,7 +294,7 @@ impl Erc721Consecutive { /// /// # Errors /// - /// If `to` is [`Address::ZERO`], then the error + /// 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. @@ -491,7 +491,7 @@ impl Erc721Consecutive { /// # Requirements: /// /// * `token_id` must not exist. - /// * `to` cannot be the zero address. + /// * `to` cannot be `Address::ZERO`. /// /// # Events /// @@ -622,7 +622,7 @@ impl Erc721Consecutive { /// /// # Requirements: /// - /// * `to` cannot be the zero address. + /// * `to` cannot be `Address::ZERO`. /// * The `token_id` token must be owned by `from`. /// /// # Events @@ -691,8 +691,8 @@ impl Erc721Consecutive { /// # Requirements: /// /// * The `token_id` token must exist and be owned by `from`. - /// * `to` cannot be the zero address. - /// * `from` cannot be the zero address. + /// * `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`. @@ -719,9 +719,10 @@ impl Erc721Consecutive { /// 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. + /// 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 /// From 04b836aa14ff74d5d10cf1bb76705c7240a8bcc6 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Mon, 12 Aug 2024 13:41:58 +0400 Subject: [PATCH 94/95] ++ --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 869ed867..ab2ae278 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1583,32 +1583,32 @@ dependencies = [ ] [[package]] -name = "erc721-consecutive-example" +name = "erc20-permit-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 = "erc20-permit-example" +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", From af0ce930850902df9348fad07154bdf12da09641 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Mon, 12 Aug 2024 14:25:47 +0400 Subject: [PATCH 95/95] ++ --- examples/erc20-permit/tests/erc20permit.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 {