From e8f70ca9c8b46e314561448d1467983caabcdb8e Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 9 Oct 2024 13:18:16 +0200 Subject: [PATCH 001/138] Implement basic VestingWallet data structures --- contracts/src/finance/mod.rs | 2 + contracts/src/finance/vesting_wallet.rs | 88 +++++++++++++++++++++++++ contracts/src/lib.rs | 1 + 3 files changed, 91 insertions(+) create mode 100644 contracts/src/finance/mod.rs create mode 100644 contracts/src/finance/vesting_wallet.rs diff --git a/contracts/src/finance/mod.rs b/contracts/src/finance/mod.rs new file mode 100644 index 000000000..0abc750be --- /dev/null +++ b/contracts/src/finance/mod.rs @@ -0,0 +1,2 @@ +//! Primitives for financial systems +pub mod vesting_wallet; diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs new file mode 100644 index 000000000..908e6f5e9 --- /dev/null +++ b/contracts/src/finance/vesting_wallet.rs @@ -0,0 +1,88 @@ +//! A vesting wallet is an ownable contract that can receive native currency and ERC20 tokens, and release these +//! assets to the wallet owner, also referred to as "beneficiary", according to a vesting schedule. +//! +//! Any assets transferred to this contract will follow the vesting schedule as if they were locked from the beginning. +//! Consequently, if the vesting has already started, any amount of tokens sent to this contract will (at least partly) +//! be immediately releasable. +//! +//! By setting the duration to 0, one can configure this contract to behave like an asset timelock that hold tokens for +//! a beneficiary until a specified time. +//! +//! NOTE: Since the wallet is [`crate::access::ownable::Ownable`], and ownership can be transferred, it is possible to sell unvested tokens. +//! Preventing this in a smart contract is difficult, considering that: 1) a beneficiary address could be a +//! counterfactually deployed contract, 2) there is likely to be a migration path for EOAs to become contracts in the +//! near future. +//! +//! NOTE: When using this contract with any token whose balance is adjusted automatically (i.e. a rebase token), make +//! sure to account the supply/balance adjustment in the vesting schedule to ensure the vested amount is as intended. +use alloc::vec::Vec; + +use alloy_primitives::{Address, U256}; +use alloy_sol_types::{sol, SolValue}; +use stylus_sdk::{ + call::{Call, RawCall}, + contract::address, + evm::gas_left, + function_selector, + storage::TopLevelStorage, + stylus_proc::{public, sol_interface, sol_storage, SolidityError}, + types::AddressVM, +}; + +use crate::token::erc20; + +sol! { + /// Emitted when `amount` of ether has been released. + #[allow(missing_docs)] + event EtherReleased(uint256 amount); + + /// Emitted when `amount` of ERC20 `token` has been released. + #[allow(missing_docs)] + event ERC20Released(address indexed token, uint256 amount); +} + +/// A VestingWallet error +#[derive(SolidityError, Debug)] +pub enum Error { + /// Error type from [`erc20::Erc20`] contract [`erc20::Error`]. + Erc20(erc20::Error), +} + +sol_interface! { + /// Interface of the [`erc20::Erc20`] standard as defined in the ERC. + interface IERC20 { + /// Moves a `value` amount of tokens from the caller's account to `to`. + /// + /// Returns a boolean value indicating whether the operation succeeded. + /// + /// Emits a [`erc20::Transfer`] event. + function transfer(address recipient, uint256 amount) external returns (bool); + } +} + +sol_storage! { + /// Wrappers around ERC-20 operations that throw on failure (when the token + /// contract returns false). Tokens that return no value (and instead revert or + /// throw on failure) are also supported, non-reverting calls are assumed to be + /// successful. + /// To use this library you can add a `#[inherit(SafeErc20)]` attribute to + /// your contract, which allows you to call the safe operations as + /// `contract.safe_transfer(token_addr, ...)`, etc. + #[allow(clippy::pub_underscore_fields)] + pub struct VestingWallet { + /// Amount of eth already released. + uint256 _released; + /// Amount of ERC20 tokens already released. + mapping(address => uint256) _erc20_released; + /// Start timestamp. + uint64 _start; + /// Vesting duration. + uint64 _duration; + } +} + +#[public] +impl VestingWallet { + #[payable] + fn receive_ether(&self) {} +} diff --git a/contracts/src/lib.rs b/contracts/src/lib.rs index dd4c8c73f..65c7f1808 100644 --- a/contracts/src/lib.rs +++ b/contracts/src/lib.rs @@ -51,6 +51,7 @@ extern crate alloc; static ALLOC: mini_alloc::MiniAlloc = mini_alloc::MiniAlloc::INIT; pub mod access; +pub mod finance; pub mod token; pub mod utils; From 4f2774097311bd34e6da0b72c4eee23869e14a00 Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 9 Oct 2024 13:39:28 +0200 Subject: [PATCH 002/138] Add getters --- contracts/src/finance/vesting_wallet.rs | 30 ++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 908e6f5e9..66196ab02 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -83,6 +83,34 @@ sol_storage! { #[public] impl VestingWallet { + /// The contract should be able to receive Eth. #[payable] - fn receive_ether(&self) {} + pub fn receive_ether(&self) {} + + /// Getter for the start timestamp. + pub fn start(&self) -> U256 { + U256::from(self._start.get()) + } + + /// Getter for the vesting duration. + pub fn duration(&self) -> U256 { + U256::from(self._duration.get()) + } + + /// Getter for the end timestamp. + pub fn end(&self) -> U256 { + self.start() + self.duration() + } + + /// Amount of eth already released + #[selector(name = "released")] + pub fn released_eth(&self) -> U256 { + self._released.get() + } + + /// Amount of token already released + #[selector(name = "released")] + pub fn released_token(&self, token: Address) -> U256 { + self._erc20_released.get(token) + } } From c7be5ed2fd5a7e5d8463c98c2c735b4f27d5f27a Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 9 Oct 2024 15:06:18 +0200 Subject: [PATCH 003/138] Add vestingAmount methods --- contracts/src/finance/vesting_wallet.rs | 56 +++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 66196ab02..72dcf90c5 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -15,13 +15,11 @@ //! //! NOTE: When using this contract with any token whose balance is adjusted automatically (i.e. a rebase token), make //! sure to account the supply/balance adjustment in the vesting schedule to ensure the vested amount is as intended. -use alloc::vec::Vec; - use alloy_primitives::{Address, U256}; use alloy_sol_types::{sol, SolValue}; use stylus_sdk::{ call::{Call, RawCall}, - contract::address, + contract::{self, address}, evm::gas_left, function_selector, storage::TopLevelStorage, @@ -51,6 +49,9 @@ pub enum Error { sol_interface! { /// Interface of the [`erc20::Erc20`] standard as defined in the ERC. interface IERC20 { + /// Returns the value of tokens owned by `account`. + function balanceOf(address account) external view returns (uint256); + /// Moves a `value` amount of tokens from the caller's account to `to`. /// /// Returns a boolean value indicating whether the operation succeeded. @@ -81,6 +82,11 @@ sol_storage! { } } +/// NOTE: Implementation of [`TopLevelStorage`] to be able use `&mut self` when +/// calling other contracts and not `&mut (impl TopLevelStorage + +/// BorrowMut)`. Should be fixed in the future by the Stylus team. +unsafe impl TopLevelStorage for VestingWallet {} + #[public] impl VestingWallet { /// The contract should be able to receive Eth. @@ -113,4 +119,48 @@ impl VestingWallet { pub fn released_token(&self, token: Address) -> U256 { self._erc20_released.get(token) } + + /// Calculates the amount of ether that has already vested. Default implementation is a linear vesting curve. + #[selector(name = "vestedAmount")] + pub fn vested_amount_eth(&self, timestamp: u64) -> U256 { + self._vesting_schedule( + contract::balance() + self.released_eth(), + timestamp, + ) + } + + /// Calculates the amount of tokens that has already vested. Default implementation is a linear vesting curve. + #[selector(name = "vestedAmount")] + pub fn vested_amount_token( + &mut self, + token: Address, + timestamp: u64, + ) -> U256 { + let erc20 = IERC20::new(token); + let call = Call::new_in(self); + let balance = erc20 + .balance_of(call, contract::address()) + .expect("should return the balance"); + + self._vesting_schedule(balance + self.released_token(token), timestamp) + } +} + +impl VestingWallet { + /// Virtual implementation of the vesting formula. This returns the amount vested, as a function of time, for + /// an asset given its total historical allocation. + pub fn _vesting_schedule( + &self, + total_allocation: U256, + timestamp: u64, + ) -> U256 { + if U256::from(timestamp) < self.start() { + U256::ZERO + } else if U256::from(timestamp) >= self.end() { + total_allocation + } else { + (total_allocation * (U256::from(timestamp) - self.start())) + / self.duration() + } + } } From c9d79c73c36eac35416175783f8d41a0dfdf548e Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 9 Oct 2024 15:12:07 +0200 Subject: [PATCH 004/138] add releasable methods --- contracts/src/finance/vesting_wallet.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 72dcf90c5..a1a35e78b 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -18,6 +18,7 @@ use alloy_primitives::{Address, U256}; use alloy_sol_types::{sol, SolValue}; use stylus_sdk::{ + block, call::{Call, RawCall}, contract::{self, address}, evm::gas_left, @@ -120,6 +121,20 @@ impl VestingWallet { self._erc20_released.get(token) } + /// Getter for the amount of releasable eth. + #[selector(name = "releasable")] + pub fn releasable_eth(&self) -> U256 { + self.vested_amount_eth(block::timestamp()) - self.released_eth() + } + + /// Getter for the amount of releasable `token` tokens. `token` should be the address of an + /// [`erc20::ERC20`] contract. + #[selector(name = "releasable")] + pub fn releasable_token(&mut self, token: Address) -> U256 { + self.vested_amount_token(token, block::timestamp()) + - self.released_token(token) + } + /// Calculates the amount of ether that has already vested. Default implementation is a linear vesting curve. #[selector(name = "vestedAmount")] pub fn vested_amount_eth(&self, timestamp: u64) -> U256 { From 7a5b39052c2b133bc0c05fd768a0ddf5a62177bc Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 9 Oct 2024 15:49:21 +0200 Subject: [PATCH 005/138] Add release_eth/token --- contracts/src/finance/vesting_wallet.rs | 68 +++++++++++++++++++------ 1 file changed, 53 insertions(+), 15 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index a1a35e78b..e6192cc90 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -15,20 +15,18 @@ //! //! NOTE: When using this contract with any token whose balance is adjusted automatically (i.e. a rebase token), make //! sure to account the supply/balance adjustment in the vesting schedule to ensure the vested amount is as intended. +use alloc::vec::Vec; use alloy_primitives::{Address, U256}; -use alloy_sol_types::{sol, SolValue}; +use alloy_sol_types::sol; use stylus_sdk::{ block, - call::{Call, RawCall}, - contract::{self, address}, - evm::gas_left, - function_selector, + call::{call, Call}, + contract, evm, function_selector, storage::TopLevelStorage, - stylus_proc::{public, sol_interface, sol_storage, SolidityError}, - types::AddressVM, + stylus_proc::{public, sol_interface, sol_storage}, }; -use crate::token::erc20; +use crate::access::ownable::Ownable; sol! { /// Emitted when `amount` of ether has been released. @@ -40,13 +38,6 @@ sol! { event ERC20Released(address indexed token, uint256 amount); } -/// A VestingWallet error -#[derive(SolidityError, Debug)] -pub enum Error { - /// Error type from [`erc20::Erc20`] contract [`erc20::Error`]. - Erc20(erc20::Error), -} - sol_interface! { /// Interface of the [`erc20::Erc20`] standard as defined in the ERC. interface IERC20 { @@ -80,6 +71,9 @@ sol_storage! { uint64 _start; /// Vesting duration. uint64 _duration; + /// Ownable contract + #[borrow] + Ownable ownable; } } @@ -89,6 +83,7 @@ sol_storage! { unsafe impl TopLevelStorage for VestingWallet {} #[public] +#[inherit(Ownable)] impl VestingWallet { /// The contract should be able to receive Eth. #[payable] @@ -135,6 +130,49 @@ impl VestingWallet { - self.released_token(token) } + /// Release the native token (ether) that have already vested. + /// + /// Emits a [`EtherReleased`] event. + #[selector(name = "release")] + pub fn release_eth(&mut self) -> Result<(), Vec> { + let amount = self.releasable_eth(); + let released = self + .released_eth() + .checked_add(amount) + .expect("should not exceed `U256::MAX` for `_released`"); + self._released.set(released); + + evm::log(EtherReleased { amount }); + + let owner = self.ownable.owner(); + call(Call::new_in(self).value(amount), owner, &[])?; + + Ok(()) + } + + /// Release the tokens that have already vested. + /// + /// Emits a [`ERC20Released`] event. + #[selector(name = "release")] + pub fn release_token( + &mut self, + token: Address, + ) -> Result { + let amount = self.releasable_token(token); + let released = self + .released_token(token) + .checked_add(amount) + .expect("should not exceed `U256::MAX` for `_erc20Released`"); + self._erc20_released.setter(token).set(released); + + evm::log(ERC20Released { token, amount }); + + let erc20 = IERC20::new(token); + let owner = self.ownable.owner(); + let call = Call::new_in(self); + erc20.transfer(call, owner, amount) + } + /// Calculates the amount of ether that has already vested. Default implementation is a linear vesting curve. #[selector(name = "vestedAmount")] pub fn vested_amount_eth(&self, timestamp: u64) -> U256 { From 39a55351d979982cad04d1effb8cecb6db094c92 Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 9 Oct 2024 15:51:06 +0200 Subject: [PATCH 006/138] fix comment grammar --- contracts/src/finance/vesting_wallet.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index e6192cc90..9c6d4a32a 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -132,7 +132,7 @@ impl VestingWallet { /// Release the native token (ether) that have already vested. /// - /// Emits a [`EtherReleased`] event. + /// Emits an [`EtherReleased`] event. #[selector(name = "release")] pub fn release_eth(&mut self) -> Result<(), Vec> { let amount = self.releasable_eth(); @@ -152,7 +152,7 @@ impl VestingWallet { /// Release the tokens that have already vested. /// - /// Emits a [`ERC20Released`] event. + /// Emits an [`ERC20Released`] event. #[selector(name = "release")] pub fn release_token( &mut self, From b8e006ad9ec07b262624f3812581fe92010e58d4 Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 9 Oct 2024 20:33:32 +0200 Subject: [PATCH 007/138] Add stylus_sdk errors to expected errors for 'release' --- contracts/src/finance/vesting_wallet.rs | 33 +++++++++++++++++++------ 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 9c6d4a32a..c3c721bd6 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -15,9 +15,9 @@ //! //! NOTE: When using this contract with any token whose balance is adjusted automatically (i.e. a rebase token), make //! sure to account the supply/balance adjustment in the vesting schedule to ensure the vested amount is as intended. -use alloc::vec::Vec; use alloy_primitives::{Address, U256}; use alloy_sol_types::sol; +use stylus_proc::SolidityError; use stylus_sdk::{ block, call::{call, Call}, @@ -28,6 +28,13 @@ use stylus_sdk::{ use crate::access::ownable::Ownable; +sol! { + /// Indicates an error related to the underlying ERC20 transfer. + #[derive(Debug)] + #[allow(missing_docs)] + error ReleaseTokenFailed(address token); +} + sol! { /// Emitted when `amount` of ether has been released. #[allow(missing_docs)] @@ -36,6 +43,7 @@ sol! { /// Emitted when `amount` of ERC20 `token` has been released. #[allow(missing_docs)] event ERC20Released(address indexed token, uint256 amount); + } sol_interface! { @@ -53,6 +61,15 @@ sol_interface! { } } +/// A Permit error. +#[derive(SolidityError, Debug)] +pub enum Error { + /// Error type from [`stylus_sdk::call::Call`] contract [`stylus_sdk::call::Error`]. + StylusError(stylus_sdk::call::Error), + /// Indicates an error related to the underlying ERC20 transfer. + ReleaseTokenFailed(ReleaseTokenFailed), +} + sol_storage! { /// Wrappers around ERC-20 operations that throw on failure (when the token /// contract returns false). Tokens that return no value (and instead revert or @@ -134,7 +151,7 @@ impl VestingWallet { /// /// Emits an [`EtherReleased`] event. #[selector(name = "release")] - pub fn release_eth(&mut self) -> Result<(), Vec> { + pub fn release_eth(&mut self) -> Result<(), Error> { let amount = self.releasable_eth(); let released = self .released_eth() @@ -154,10 +171,7 @@ impl VestingWallet { /// /// Emits an [`ERC20Released`] event. #[selector(name = "release")] - pub fn release_token( - &mut self, - token: Address, - ) -> Result { + pub fn release_token(&mut self, token: Address) -> Result<(), Error> { let amount = self.releasable_token(token); let released = self .released_token(token) @@ -170,7 +184,12 @@ impl VestingWallet { let erc20 = IERC20::new(token); let owner = self.ownable.owner(); let call = Call::new_in(self); - erc20.transfer(call, owner, amount) + let succeeded = erc20.transfer(call, owner, amount)?; + if !succeeded { + return Err(ReleaseTokenFailed { token }.into()); + } + + Ok(()) } /// Calculates the amount of ether that has already vested. Default implementation is a linear vesting curve. From aea79ea00e4e172bb7fb827cd3313dafeb07553c Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 9 Oct 2024 20:33:58 +0200 Subject: [PATCH 008/138] Format comments --- contracts/src/finance/vesting_wallet.rs | 49 +++++++++++++++---------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index c3c721bd6..b4e164fb9 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -1,20 +1,25 @@ -//! A vesting wallet is an ownable contract that can receive native currency and ERC20 tokens, and release these -//! assets to the wallet owner, also referred to as "beneficiary", according to a vesting schedule. +//! A vesting wallet is an ownable contract that can receive native currency and +//! ERC20 tokens, and release these assets to the wallet owner, also referred to +//! as "beneficiary", according to a vesting schedule. //! -//! Any assets transferred to this contract will follow the vesting schedule as if they were locked from the beginning. -//! Consequently, if the vesting has already started, any amount of tokens sent to this contract will (at least partly) -//! be immediately releasable. +//! Any assets transferred to this contract will follow the vesting schedule as +//! if they were locked from the beginning. Consequently, if the vesting has +//! already started, any amount of tokens sent to this contract will (at least +//! partly) be immediately releasable. //! -//! By setting the duration to 0, one can configure this contract to behave like an asset timelock that hold tokens for -//! a beneficiary until a specified time. +//! By setting the duration to 0, one can configure this contract to behave like +//! an asset timelock that hold tokens for a beneficiary until a specified time. //! -//! NOTE: Since the wallet is [`crate::access::ownable::Ownable`], and ownership can be transferred, it is possible to sell unvested tokens. -//! Preventing this in a smart contract is difficult, considering that: 1) a beneficiary address could be a -//! counterfactually deployed contract, 2) there is likely to be a migration path for EOAs to become contracts in the -//! near future. +//! NOTE: Since the wallet is [`crate::access::ownable::Ownable`], and ownership +//! can be transferred, it is possible to sell unvested tokens. Preventing this +//! in a smart contract is difficult, considering that: 1) a beneficiary address +//! could be a counterfactually deployed contract, 2) there is likely to be a +//! migration path for EOAs to become contracts in the near future. //! -//! NOTE: When using this contract with any token whose balance is adjusted automatically (i.e. a rebase token), make -//! sure to account the supply/balance adjustment in the vesting schedule to ensure the vested amount is as intended. +//! NOTE: When using this contract with any token whose balance is adjusted +//! automatically (i.e. a rebase token), make sure to account the supply/balance +//! adjustment in the vesting schedule to ensure the vested amount is as +//! intended. use alloy_primitives::{Address, U256}; use alloy_sol_types::sol; use stylus_proc::SolidityError; @@ -64,7 +69,8 @@ sol_interface! { /// A Permit error. #[derive(SolidityError, Debug)] pub enum Error { - /// Error type from [`stylus_sdk::call::Call`] contract [`stylus_sdk::call::Error`]. + /// Error type from [`stylus_sdk::call::Call`] contract + /// [`stylus_sdk::call::Error`]. StylusError(stylus_sdk::call::Error), /// Indicates an error related to the underlying ERC20 transfer. ReleaseTokenFailed(ReleaseTokenFailed), @@ -139,8 +145,8 @@ impl VestingWallet { self.vested_amount_eth(block::timestamp()) - self.released_eth() } - /// Getter for the amount of releasable `token` tokens. `token` should be the address of an - /// [`erc20::ERC20`] contract. + /// Getter for the amount of releasable `token` tokens. `token` should be + /// the address of an [`erc20::ERC20`] contract. #[selector(name = "releasable")] pub fn releasable_token(&mut self, token: Address) -> U256 { self.vested_amount_token(token, block::timestamp()) @@ -192,7 +198,8 @@ impl VestingWallet { Ok(()) } - /// Calculates the amount of ether that has already vested. Default implementation is a linear vesting curve. + /// Calculates the amount of ether that has already vested. Default + /// implementation is a linear vesting curve. #[selector(name = "vestedAmount")] pub fn vested_amount_eth(&self, timestamp: u64) -> U256 { self._vesting_schedule( @@ -201,7 +208,8 @@ impl VestingWallet { ) } - /// Calculates the amount of tokens that has already vested. Default implementation is a linear vesting curve. + /// Calculates the amount of tokens that has already vested. Default + /// implementation is a linear vesting curve. #[selector(name = "vestedAmount")] pub fn vested_amount_token( &mut self, @@ -219,8 +227,9 @@ impl VestingWallet { } impl VestingWallet { - /// Virtual implementation of the vesting formula. This returns the amount vested, as a function of time, for - /// an asset given its total historical allocation. + /// Virtual implementation of the vesting formula. This returns the amount + /// vested, as a function of time, for an asset given its total + /// historical allocation. pub fn _vesting_schedule( &self, total_allocation: U256, From 6048de3bc8a490c42c4502197d8ef84fe5fbe00c Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 10 Oct 2024 07:35:18 +0200 Subject: [PATCH 009/138] Add simple getter tests --- contracts/src/finance/vesting_wallet.rs | 52 +++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index b4e164fb9..0b80e2644 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -245,3 +245,55 @@ impl VestingWallet { } } } + +#[cfg(all(test, feature = "std"))] +mod tests { + use alloy_primitives::{address, uint, Address, U256}; + use stylus_sdk::msg; + + use super::{Error, VestingWallet}; + + const ALICE: Address = address!("A11CEacF9aa32246d767FCCD72e02d6bCbcC375d"); + + #[motsu::test] + fn reads_start(contract: Ownable) { + let expected = uint!(1000_U64); + contract._start.set(expected); + let start = contract.start(); + assert_eq!(U256::from(expected), start); + } + + #[motsu::test] + fn reads_duration(contract: Ownable) { + let expected = uint!(1000_U64); + contract._duration.set(expected); + let duration = contract.duration(); + assert_eq!(U256::from(expected), duration); + } + + #[motsu::test] + fn reads_end(contract: Ownable) { + let start = uint!(1000_U64); + let duration = uint!(1000_U64); + contract._start.set(start); + contract._duration.set(duration); + let end = contract.end(); + assert_eq!(U256::from(start + duration), end); + } + + #[motsu::test] + fn reads_released_eth(contract: Ownable) { + let one = uint!(1_U256); + contract._released.set(one); + let released = contract.released_eth(); + assert_eq!(one, released); + } + + #[motsu::test] + fn reads_released_token(contract: Ownable) { + let one = uint!(1_U256); + contract._erc20_released.set(one); + let released = contract.released_token(); + assert_eq!(one, released); + } +} From f4fd2c0ff9db040036cdf83ed9b912cf5f51a74d Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 10 Oct 2024 07:58:46 +0200 Subject: [PATCH 010/138] Fix tests --- contracts/src/finance/vesting_wallet.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 0b80e2644..fd1be77e8 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -253,10 +253,10 @@ mod tests { use super::{Error, VestingWallet}; - const ALICE: Address = address!("A11CEacF9aa32246d767FCCD72e02d6bCbcC375d"); + const TOKEN: Address = address!("A11CEacF9aa32246d767FCCD72e02d6bCbcC375d"); #[motsu::test] - fn reads_start(contract: Ownable) { + fn reads_start(contract: VestingWallet) { let expected = uint!(1000_U64); contract._start.set(expected); let start = contract.start(); @@ -264,7 +264,7 @@ mod tests { } #[motsu::test] - fn reads_duration(contract: Ownable) { + fn reads_duration(contract: VestingWallet) { let expected = uint!(1000_U64); contract._duration.set(expected); let duration = contract.duration(); @@ -272,7 +272,7 @@ mod tests { } #[motsu::test] - fn reads_end(contract: Ownable) { + fn reads_end(contract: VestingWallet) { let start = uint!(1000_U64); let duration = uint!(1000_U64); contract._start.set(start); @@ -282,7 +282,7 @@ mod tests { } #[motsu::test] - fn reads_released_eth(contract: Ownable) { + fn reads_released_eth(contract: VestingWallet) { let one = uint!(1_U256); contract._released.set(one); let released = contract.released_eth(); @@ -290,10 +290,10 @@ mod tests { } #[motsu::test] - fn reads_released_token(contract: Ownable) { + fn reads_released_token(contract: VestingWallet) { let one = uint!(1_U256); - contract._erc20_released.set(one); - let released = contract.released_token(); + contract._erc20_released.setter(TOKEN).set(one); + let released = contract.released_token(TOKEN); assert_eq!(one, released); } } From fce3c5144dd26892924c61f7269e7918a94d318c Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 10 Oct 2024 09:51:19 +0200 Subject: [PATCH 011/138] refactor tests --- contracts/src/finance/vesting_wallet.rs | 65 ++++++++++++++----------- 1 file changed, 37 insertions(+), 28 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index fd1be77e8..8f55815e7 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -248,52 +248,61 @@ impl VestingWallet { #[cfg(all(test, feature = "std"))] mod tests { - use alloy_primitives::{address, uint, Address, U256}; - use stylus_sdk::msg; + use alloy_primitives::{address, uint, Address, U256, U64}; + use stylus_sdk::block; - use super::{Error, VestingWallet}; + use super::VestingWallet; - const TOKEN: Address = address!("A11CEacF9aa32246d767FCCD72e02d6bCbcC375d"); + const DURATION: u64 = 4 * 365 * 86400; // 4 years + + fn start() -> U64 { + U64::from(block::timestamp() + 3600) // 1 hour + } #[motsu::test] fn reads_start(contract: VestingWallet) { - let expected = uint!(1000_U64); - contract._start.set(expected); - let start = contract.start(); - assert_eq!(U256::from(expected), start); + let start = start(); + contract._start.set(start); + assert_eq!(U256::from(start), contract.start()); } #[motsu::test] fn reads_duration(contract: VestingWallet) { - let expected = uint!(1000_U64); - contract._duration.set(expected); - let duration = contract.duration(); - assert_eq!(U256::from(expected), duration); + contract._duration.set(U64::from(DURATION)); + assert_eq!(U256::from(DURATION), contract.duration()); } #[motsu::test] fn reads_end(contract: VestingWallet) { - let start = uint!(1000_U64); - let duration = uint!(1000_U64); + let start = start(); + let duration = U64::from(DURATION); contract._start.set(start); contract._duration.set(duration); - let end = contract.end(); - assert_eq!(U256::from(start + duration), end); + assert_eq!(U256::from(start + duration), contract.end()); } - #[motsu::test] - fn reads_released_eth(contract: VestingWallet) { - let one = uint!(1_U256); - contract._released.set(one); - let released = contract.released_eth(); - assert_eq!(one, released); + mod eth_vesting { + use super::*; + + #[motsu::test] + fn reads_released_eth(contract: VestingWallet) { + let one = uint!(1_U256); + contract._released.set(one); + assert_eq!(one, contract.released_eth()); + } } - #[motsu::test] - fn reads_released_token(contract: VestingWallet) { - let one = uint!(1_U256); - contract._erc20_released.setter(TOKEN).set(one); - let released = contract.released_token(TOKEN); - assert_eq!(one, released); + mod erc20_vesting { + use super::*; + + const TOKEN: Address = + address!("A11CEacF9aa32246d767FCCD72e02d6bCbcC375d"); + + #[motsu::test] + fn reads_released_token(contract: VestingWallet) { + let one = uint!(1_U256); + contract._erc20_released.setter(TOKEN).set(one); + assert_eq!(one, contract.released_token(TOKEN)); + } } } From 7d002ce0d8395bf681e9540d88b716188e19eeb9 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 10 Oct 2024 10:15:46 +0200 Subject: [PATCH 012/138] revert test format --- contracts/src/finance/vesting_wallet.rs | 32 +++++++++---------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 8f55815e7..149b96f12 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -253,6 +253,7 @@ mod tests { use super::VestingWallet; + const TOKEN: Address = address!("A11CEacF9aa32246d767FCCD72e02d6bCbcC375d"); const DURATION: u64 = 4 * 365 * 86400; // 4 years fn start() -> U64 { @@ -281,28 +282,17 @@ mod tests { assert_eq!(U256::from(start + duration), contract.end()); } - mod eth_vesting { - use super::*; - - #[motsu::test] - fn reads_released_eth(contract: VestingWallet) { - let one = uint!(1_U256); - contract._released.set(one); - assert_eq!(one, contract.released_eth()); - } + #[motsu::test] + fn reads_released_eth(contract: VestingWallet) { + let one = uint!(1_U256); + contract._released.set(one); + assert_eq!(one, contract.released_eth()); } - mod erc20_vesting { - use super::*; - - const TOKEN: Address = - address!("A11CEacF9aa32246d767FCCD72e02d6bCbcC375d"); - - #[motsu::test] - fn reads_released_token(contract: VestingWallet) { - let one = uint!(1_U256); - contract._erc20_released.setter(TOKEN).set(one); - assert_eq!(one, contract.released_token(TOKEN)); - } + #[motsu::test] + fn reads_released_token(contract: VestingWallet) { + let one = uint!(1_U256); + contract._erc20_released.setter(TOKEN).set(one); + assert_eq!(one, contract.released_token(TOKEN)); } } From ae68003693767da8d7ac39f3d84c1246d3e92ffc Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 10 Oct 2024 10:29:22 +0200 Subject: [PATCH 013/138] Fix comment links + update VestingWallet comment --- contracts/src/finance/vesting_wallet.rs | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 149b96f12..b9d0f7853 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -52,7 +52,7 @@ sol! { } sol_interface! { - /// Interface of the [`erc20::Erc20`] standard as defined in the ERC. + /// Interface of the [`crate::token::erc20::Erc20`] standard as defined in the ERC. interface IERC20 { /// Returns the value of tokens owned by `account`. function balanceOf(address account) external view returns (uint256); @@ -61,7 +61,7 @@ sol_interface! { /// /// Returns a boolean value indicating whether the operation succeeded. /// - /// Emits a [`erc20::Transfer`] event. + /// Emits a [`crate::token::erc20::Transfer`] event. function transfer(address recipient, uint256 amount) external returns (bool); } } @@ -77,14 +77,7 @@ pub enum Error { } sol_storage! { - /// Wrappers around ERC-20 operations that throw on failure (when the token - /// contract returns false). Tokens that return no value (and instead revert or - /// throw on failure) are also supported, non-reverting calls are assumed to be - /// successful. - /// To use this library you can add a `#[inherit(SafeErc20)]` attribute to - /// your contract, which allows you to call the safe operations as - /// `contract.safe_transfer(token_addr, ...)`, etc. - #[allow(clippy::pub_underscore_fields)] + /// State of a VestingWallet Contract. pub struct VestingWallet { /// Amount of eth already released. uint256 _released; @@ -146,7 +139,7 @@ impl VestingWallet { } /// Getter for the amount of releasable `token` tokens. `token` should be - /// the address of an [`erc20::ERC20`] contract. + /// the address of an [`crate::token::erc20::Erc20`] contract. #[selector(name = "releasable")] pub fn releasable_token(&mut self, token: Address) -> U256 { self.vested_amount_token(token, block::timestamp()) From 4dc658f61badf70d43263cd515a550768b9b49c6 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 10 Oct 2024 10:31:44 +0200 Subject: [PATCH 014/138] put events before errors --- contracts/src/finance/vesting_wallet.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index b9d0f7853..ffd627053 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -33,13 +33,6 @@ use stylus_sdk::{ use crate::access::ownable::Ownable; -sol! { - /// Indicates an error related to the underlying ERC20 transfer. - #[derive(Debug)] - #[allow(missing_docs)] - error ReleaseTokenFailed(address token); -} - sol! { /// Emitted when `amount` of ether has been released. #[allow(missing_docs)] @@ -48,7 +41,13 @@ sol! { /// Emitted when `amount` of ERC20 `token` has been released. #[allow(missing_docs)] event ERC20Released(address indexed token, uint256 amount); +} +sol! { + /// Indicates an error related to the underlying ERC20 transfer. + #[derive(Debug)] + #[allow(missing_docs)] + error ReleaseTokenFailed(address token); } sol_interface! { From b38960ce92de24288c14458d24c292bd58a3863f Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 10 Oct 2024 10:36:55 +0200 Subject: [PATCH 015/138] Fix 'transfer' comment --- contracts/src/finance/vesting_wallet.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index ffd627053..45c657414 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -45,6 +45,8 @@ sol! { sol! { /// Indicates an error related to the underlying ERC20 transfer. + /// + /// * `token` - Address of the token being released. #[derive(Debug)] #[allow(missing_docs)] error ReleaseTokenFailed(address token); @@ -61,7 +63,7 @@ sol_interface! { /// Returns a boolean value indicating whether the operation succeeded. /// /// Emits a [`crate::token::erc20::Transfer`] event. - function transfer(address recipient, uint256 amount) external returns (bool); + function transfer(address to, uint256 value) external returns (bool); } } From 56d68053635ed92bbdcdaf4ddefb654b8dec237f Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 10 Oct 2024 10:38:52 +0200 Subject: [PATCH 016/138] Fix Error comment --- contracts/src/finance/vesting_wallet.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 45c657414..e9920291e 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -67,7 +67,7 @@ sol_interface! { } } -/// A Permit error. +/// An error that occurred in the [`VestingWallet`] contract. #[derive(SolidityError, Debug)] pub enum Error { /// Error type from [`stylus_sdk::call::Call`] contract From 0d4d35315fde0986176e84d3c1f98f5993f5c5bc Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 10 Oct 2024 10:41:26 +0200 Subject: [PATCH 017/138] update _token suffix -> _erc20 --- contracts/src/finance/vesting_wallet.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index e9920291e..aa25199b2 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -129,7 +129,7 @@ impl VestingWallet { /// Amount of token already released #[selector(name = "released")] - pub fn released_token(&self, token: Address) -> U256 { + pub fn released_erc20(&self, token: Address) -> U256 { self._erc20_released.get(token) } @@ -142,9 +142,9 @@ impl VestingWallet { /// Getter for the amount of releasable `token` tokens. `token` should be /// the address of an [`crate::token::erc20::Erc20`] contract. #[selector(name = "releasable")] - pub fn releasable_token(&mut self, token: Address) -> U256 { - self.vested_amount_token(token, block::timestamp()) - - self.released_token(token) + pub fn releasable_erc20(&mut self, token: Address) -> U256 { + self.vested_amount_erc20(token, block::timestamp()) + - self.released_erc20(token) } /// Release the native token (ether) that have already vested. @@ -171,10 +171,10 @@ impl VestingWallet { /// /// Emits an [`ERC20Released`] event. #[selector(name = "release")] - pub fn release_token(&mut self, token: Address) -> Result<(), Error> { - let amount = self.releasable_token(token); + pub fn release_erc20(&mut self, token: Address) -> Result<(), Error> { + let amount = self.releasable_erc20(token); let released = self - .released_token(token) + .released_erc20(token) .checked_add(amount) .expect("should not exceed `U256::MAX` for `_erc20Released`"); self._erc20_released.setter(token).set(released); @@ -205,7 +205,7 @@ impl VestingWallet { /// Calculates the amount of tokens that has already vested. Default /// implementation is a linear vesting curve. #[selector(name = "vestedAmount")] - pub fn vested_amount_token( + pub fn vested_amount_erc20( &mut self, token: Address, timestamp: u64, @@ -216,7 +216,7 @@ impl VestingWallet { .balance_of(call, contract::address()) .expect("should return the balance"); - self._vesting_schedule(balance + self.released_token(token), timestamp) + self._vesting_schedule(balance + self.released_erc20(token), timestamp) } } @@ -284,9 +284,9 @@ mod tests { } #[motsu::test] - fn reads_released_token(contract: VestingWallet) { + fn reads_released_erc20(contract: VestingWallet) { let one = uint!(1_U256); contract._erc20_released.setter(TOKEN).set(one); - assert_eq!(one, contract.released_token(TOKEN)); + assert_eq!(one, contract.released_erc20(TOKEN)); } } From f5230d709e0480502c29b316111eabe121f01d3a Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 10 Oct 2024 14:46:36 +0200 Subject: [PATCH 018/138] add test for _vested_schedule --- contracts/src/finance/vesting_wallet.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index aa25199b2..ebcfb9b6f 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -289,4 +289,19 @@ mod tests { contract._erc20_released.setter(TOKEN).set(one); assert_eq!(one, contract.released_erc20(TOKEN)); } + + #[motsu::test] + fn reads_vested_allocation(contract: VestingWallet) { + let start = start(); + let duration = U64::from(DURATION); + contract._start.set(start); + contract._duration.set(duration); + + let one = uint!(1_U256); + let two = uint!(2_U256); + + assert_eq!(U256::ZERO, contract._vesting_schedule(two, start - 1)); + assert_eq!(one, contract._vesting_schedule(two, start + duration / 2)); + assert_eq!(two, contract._vesting_schedule(two, start + duration)); + } } From 75bce620678c85ba4781d5c44f51f7b27538e314 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 10 Oct 2024 14:52:37 +0200 Subject: [PATCH 019/138] update _vesting_schedule to accept U64 + fix tests --- contracts/src/finance/vesting_wallet.rs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index ebcfb9b6f..21ead6995 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -20,7 +20,7 @@ //! automatically (i.e. a rebase token), make sure to account the supply/balance //! adjustment in the vesting schedule to ensure the vested amount is as //! intended. -use alloy_primitives::{Address, U256}; +use alloy_primitives::{Address, U256, U64}; use alloy_sol_types::sol; use stylus_proc::SolidityError; use stylus_sdk::{ @@ -198,7 +198,7 @@ impl VestingWallet { pub fn vested_amount_eth(&self, timestamp: u64) -> U256 { self._vesting_schedule( contract::balance() + self.released_eth(), - timestamp, + U64::from(timestamp), ) } @@ -216,7 +216,10 @@ impl VestingWallet { .balance_of(call, contract::address()) .expect("should return the balance"); - self._vesting_schedule(balance + self.released_erc20(token), timestamp) + self._vesting_schedule( + balance + self.released_erc20(token), + U64::from(timestamp), + ) } } @@ -227,7 +230,7 @@ impl VestingWallet { pub fn _vesting_schedule( &self, total_allocation: U256, - timestamp: u64, + timestamp: U64, ) -> U256 { if U256::from(timestamp) < self.start() { U256::ZERO @@ -300,8 +303,14 @@ mod tests { let one = uint!(1_U256); let two = uint!(2_U256); - assert_eq!(U256::ZERO, contract._vesting_schedule(two, start - 1)); - assert_eq!(one, contract._vesting_schedule(two, start + duration / 2)); + assert_eq!( + U256::ZERO, + contract._vesting_schedule(two, start - U64::from(1)) + ); + assert_eq!( + one, + contract._vesting_schedule(two, start + duration / U64::from(2)) + ); assert_eq!(two, contract._vesting_schedule(two, start + duration)); } } From 88abbb2a8625333fdc6eed4a6e3a687d9c4b9768 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 10 Oct 2024 18:17:59 +0200 Subject: [PATCH 020/138] Set up integration test scaffold --- Cargo.lock | 15 ++++++ Cargo.toml | 2 + examples/vesting-wallet/Cargo.toml | 26 ++++++++++ examples/vesting-wallet/src/constructor.sol | 33 ++++++++++++ examples/vesting-wallet/src/lib.rs | 21 ++++++++ examples/vesting-wallet/tests/abi/mod.rs | 26 ++++++++++ .../vesting-wallet/tests/vesting_wallet.rs | 50 +++++++++++++++++++ scripts/e2e-tests.sh | 2 +- 8 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 examples/vesting-wallet/Cargo.toml create mode 100644 examples/vesting-wallet/src/constructor.sol create mode 100644 examples/vesting-wallet/src/lib.rs create mode 100644 examples/vesting-wallet/tests/abi/mod.rs create mode 100644 examples/vesting-wallet/tests/vesting_wallet.rs diff --git a/Cargo.lock b/Cargo.lock index 0629f7021..91b4610da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3964,6 +3964,21 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "vesting-wallet-example" +version = "0.0.0" +dependencies = [ + "alloy", + "alloy-primitives", + "e2e", + "eyre", + "mini-alloc", + "openzeppelin-stylus", + "stylus-proc", + "stylus-sdk", + "tokio", +] + [[package]] name = "wait-timeout" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 1886c2956..1fdfcb3cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ members = [ "examples/erc721-metadata", "examples/merkle-proofs", "examples/ownable", + "examples/vesting-wallet", "examples/access-control", "examples/basic/token", "examples/basic/script", @@ -34,6 +35,7 @@ default-members = [ "examples/erc721-metadata", "examples/merkle-proofs", "examples/ownable", + "examples/vesting-wallet", "examples/access-control", "examples/basic/token", "examples/ecdsa", diff --git a/examples/vesting-wallet/Cargo.toml b/examples/vesting-wallet/Cargo.toml new file mode 100644 index 000000000..fb67048f4 --- /dev/null +++ b/examples/vesting-wallet/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "vesting-wallet-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 +eyre.workspace = true +tokio.workspace = true +e2e = { path = "../../lib/e2e" } + +[features] +e2e = [] + +[lib] +crate-type = ["lib", "cdylib"] diff --git a/examples/vesting-wallet/src/constructor.sol b/examples/vesting-wallet/src/constructor.sol new file mode 100644 index 000000000..b8b28df05 --- /dev/null +++ b/examples/vesting-wallet/src/constructor.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +contract VestingWalletExample { + uint256 private _released; + mapping(address token => uint256) private _erc20Released; + uint64 private immutable _start; + uint64 private immutable _duration; + + address private _owner; + + error OwnableInvalidOwner(address owner); + event OwnershipTransferred( + address indexed previousOwner, + address indexed newOwner + ); + + constructor(address beneficiary, uint64 startTimestamp, uint64 durationSeconds) payable { + _start = startTimestamp; + _duration = durationSeconds; + + if (beneficiary == address(0)) { + revert OwnableInvalidOwner(address(0)); + } + _transferOwnership(beneficiary); + } + + function _transferOwnership(address newOwner) internal virtual { + address oldOwner = _owner; + _owner = newOwner; + emit OwnershipTransferred(oldOwner, newOwner); + } +} diff --git a/examples/vesting-wallet/src/lib.rs b/examples/vesting-wallet/src/lib.rs new file mode 100644 index 000000000..6396629a5 --- /dev/null +++ b/examples/vesting-wallet/src/lib.rs @@ -0,0 +1,21 @@ +#![cfg_attr(not(test), no_main, no_std)] +extern crate alloc; + +use openzeppelin_stylus::{ + access::ownable::Ownable, finance::vesting_wallet::VestingWallet, +}; +use stylus_sdk::prelude::{entrypoint, public, sol_storage}; + +sol_storage! { + #[entrypoint] + struct VestingWalletExample { + #[borrow] + Ownable ownable; + #[borrow] + VestingWallet vesting_wallet; + } +} + +#[public] +#[inherit(VestingWallet, Ownable)] +impl VestingWalletExample {} diff --git a/examples/vesting-wallet/tests/abi/mod.rs b/examples/vesting-wallet/tests/abi/mod.rs new file mode 100644 index 000000000..4b890e576 --- /dev/null +++ b/examples/vesting-wallet/tests/abi/mod.rs @@ -0,0 +1,26 @@ +#![allow(dead_code)] +use alloy::sol; + +sol!( + #[sol(rpc)] + contract VestingWallet { + function start() external view returns (uint256 start); + function duration() external view returns (uint256 duration); + function end() external view returns (uint256 end); + function released() external view returns (uint256 released); + function released(address token) external view returns (uint256 released); + function releasable() external view returns (uint256 releasable); + function releasable(address token) external view returns (uint256 releasable); + function release() external; + function release(address token) external; + function vestedAmount(uint64 timestamp) external view returns (uint256 vestedAmount); + function vestedAmount(address token, uint64 timestamp) external view returns (uint256 vestedAmount); + + error ReleaseTokenFailed(address token); + + #[derive(Debug, PartialEq)] + event EtherReleased(uint256 amount); + #[derive(Debug, PartialEq)] + event ERC20Released(address indexed token, uint256 amount); + } +); diff --git a/examples/vesting-wallet/tests/vesting_wallet.rs b/examples/vesting-wallet/tests/vesting_wallet.rs new file mode 100644 index 000000000..33d33a67c --- /dev/null +++ b/examples/vesting-wallet/tests/vesting_wallet.rs @@ -0,0 +1,50 @@ +#![cfg(feature = "e2e")] + +use alloy::sol; +use alloy_primitives::{Address, U256}; +use e2e::{Account, ReceiptExt}; + +use crate::{abi::VestingWallet, VestingWalletExample::constructorCall}; + +mod abi; + +sol!("src/constructor.sol"); + +const HOUR: u64 = 3600; +const YEAR: u64 = 365 * 86400; + +fn ctr( + beneficiary: Address, + start_timestamp: u64, + duration_seconds: u64, +) -> constructorCall { + constructorCall { + beneficiary, + startTimestamp: start_timestamp, + durationSeconds: duration_seconds, + } +} + +#[e2e::test] +async fn constructs(alice: Account) -> eyre::Result<()> { + let start = HOUR; + let contract_addr = alice + .as_deployer() + .with_constructor(ctr(alice.address(), start, YEAR)) + .deploy() + .await? + .address()?; + let contract = VestingWallet::new(contract_addr, &alice.wallet); + + let VestingWallet::startReturn { start: _start } = + contract.start().call().await?; + let VestingWallet::durationReturn { duration } = + contract.duration().call().await?; + let VestingWallet::endReturn { end } = contract.end().call().await?; + + assert_eq!(U256::from(start), _start); + assert_eq!(U256::from(YEAR), duration); + assert_eq!(end, U256::from(start + YEAR)); + + Ok(()) +} diff --git a/scripts/e2e-tests.sh b/scripts/e2e-tests.sh index f7b9cdeb1..ec92a3c88 100755 --- a/scripts/e2e-tests.sh +++ b/scripts/e2e-tests.sh @@ -11,4 +11,4 @@ cargo +"$NIGHTLY_TOOLCHAIN" build --release --target wasm32-unknown-unknown -Z b export RPC_URL=http://localhost:8547 # We should use stable here once nitro-testnode is updated and the contracts fit # the size limit. Work tracked [here](https://github.com/OpenZeppelin/rust-contracts-stylus/issues/87) -cargo +"$NIGHTLY_TOOLCHAIN" test --features std,e2e --test "*" +cargo +"$NIGHTLY_TOOLCHAIN" test --features std,e2e --test "vesting_wallet" From 59b34b61f3f5f7ad8afed7087f68684ec426a5db Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 11 Oct 2024 06:15:54 +0200 Subject: [PATCH 021/138] Revert e2e-tests.sh change --- scripts/e2e-tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/e2e-tests.sh b/scripts/e2e-tests.sh index ec92a3c88..f7b9cdeb1 100755 --- a/scripts/e2e-tests.sh +++ b/scripts/e2e-tests.sh @@ -11,4 +11,4 @@ cargo +"$NIGHTLY_TOOLCHAIN" build --release --target wasm32-unknown-unknown -Z b export RPC_URL=http://localhost:8547 # We should use stable here once nitro-testnode is updated and the contracts fit # the size limit. Work tracked [here](https://github.com/OpenZeppelin/rust-contracts-stylus/issues/87) -cargo +"$NIGHTLY_TOOLCHAIN" test --features std,e2e --test "vesting_wallet" +cargo +"$NIGHTLY_TOOLCHAIN" test --features std,e2e --test "*" From e06f498a5afa052e8e1502035e8f7423aea52279 Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 11 Oct 2024 08:04:07 +0200 Subject: [PATCH 022/138] Redefine methods in VestingWalletExample --- contracts/src/finance/vesting_wallet.rs | 107 +++++++++----------- examples/vesting-wallet/src/constructor.sol | 10 +- examples/vesting-wallet/src/lib.rs | 71 ++++++++++++- scripts/e2e-tests.sh | 2 +- 4 files changed, 125 insertions(+), 65 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 21ead6995..3b4fb3e95 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -10,6 +10,11 @@ //! By setting the duration to 0, one can configure this contract to behave like //! an asset timelock that hold tokens for a beneficiary until a specified time. //! +//! Fields `_start` (indicates the timestamp when the vesting start), +//! `_duration` (indicates how long the vesting lasts), and `_owner` (the +//! beneficiary of the wallet) can be set during construction with `koba` (stylus +//! construction tooling) within solidity constructor file. +//! //! NOTE: Since the wallet is [`crate::access::ownable::Ownable`], and ownership //! can be transferred, it is possible to sell unvested tokens. Preventing this //! in a smart contract is difficult, considering that: 1) a beneficiary address @@ -31,8 +36,6 @@ use stylus_sdk::{ stylus_proc::{public, sol_interface, sol_storage}, }; -use crate::access::ownable::Ownable; - sol! { /// Emitted when `amount` of ether has been released. #[allow(missing_docs)] @@ -88,9 +91,6 @@ sol_storage! { uint64 _start; /// Vesting duration. uint64 _duration; - /// Ownable contract - #[borrow] - Ownable ownable; } } @@ -100,7 +100,6 @@ sol_storage! { unsafe impl TopLevelStorage for VestingWallet {} #[public] -#[inherit(Ownable)] impl VestingWallet { /// The contract should be able to receive Eth. #[payable] @@ -122,79 +121,29 @@ impl VestingWallet { } /// Amount of eth already released - #[selector(name = "released")] pub fn released_eth(&self) -> U256 { self._released.get() } /// Amount of token already released - #[selector(name = "released")] pub fn released_erc20(&self, token: Address) -> U256 { self._erc20_released.get(token) } /// Getter for the amount of releasable eth. - #[selector(name = "releasable")] pub fn releasable_eth(&self) -> U256 { self.vested_amount_eth(block::timestamp()) - self.released_eth() } /// Getter for the amount of releasable `token` tokens. `token` should be /// the address of an [`crate::token::erc20::Erc20`] contract. - #[selector(name = "releasable")] pub fn releasable_erc20(&mut self, token: Address) -> U256 { self.vested_amount_erc20(token, block::timestamp()) - self.released_erc20(token) } - /// Release the native token (ether) that have already vested. - /// - /// Emits an [`EtherReleased`] event. - #[selector(name = "release")] - pub fn release_eth(&mut self) -> Result<(), Error> { - let amount = self.releasable_eth(); - let released = self - .released_eth() - .checked_add(amount) - .expect("should not exceed `U256::MAX` for `_released`"); - self._released.set(released); - - evm::log(EtherReleased { amount }); - - let owner = self.ownable.owner(); - call(Call::new_in(self).value(amount), owner, &[])?; - - Ok(()) - } - - /// Release the tokens that have already vested. - /// - /// Emits an [`ERC20Released`] event. - #[selector(name = "release")] - pub fn release_erc20(&mut self, token: Address) -> Result<(), Error> { - let amount = self.releasable_erc20(token); - let released = self - .released_erc20(token) - .checked_add(amount) - .expect("should not exceed `U256::MAX` for `_erc20Released`"); - self._erc20_released.setter(token).set(released); - - evm::log(ERC20Released { token, amount }); - - let erc20 = IERC20::new(token); - let owner = self.ownable.owner(); - let call = Call::new_in(self); - let succeeded = erc20.transfer(call, owner, amount)?; - if !succeeded { - return Err(ReleaseTokenFailed { token }.into()); - } - - Ok(()) - } - /// Calculates the amount of ether that has already vested. Default /// implementation is a linear vesting curve. - #[selector(name = "vestedAmount")] pub fn vested_amount_eth(&self, timestamp: u64) -> U256 { self._vesting_schedule( contract::balance() + self.released_eth(), @@ -204,7 +153,6 @@ impl VestingWallet { /// Calculates the amount of tokens that has already vested. Default /// implementation is a linear vesting curve. - #[selector(name = "vestedAmount")] pub fn vested_amount_erc20( &mut self, token: Address, @@ -241,6 +189,51 @@ impl VestingWallet { / self.duration() } } + + /// Release the native token (ether) that have already vested. + /// + /// Emits an [`EtherReleased`] event. + pub fn _release_eth(&mut self, to: Address) -> Result<(), Error> { + let amount = self.releasable_eth(); + let released = self + .released_eth() + .checked_add(amount) + .expect("should not exceed `U256::MAX` for `_released`"); + self._released.set(released); + + evm::log(EtherReleased { amount }); + + call(Call::new_in(self).value(amount), to, &[])?; + + Ok(()) + } + + /// Release the tokens that have already vested. + /// + /// Emits an [`ERC20Released`] event. + pub fn _release_erc20( + &mut self, + to: Address, + token: Address, + ) -> Result<(), Error> { + let amount = self.releasable_erc20(token); + let released = self + .released_erc20(token) + .checked_add(amount) + .expect("should not exceed `U256::MAX` for `_erc20Released`"); + self._erc20_released.setter(token).set(released); + + evm::log(ERC20Released { token, amount }); + + let erc20 = IERC20::new(token); + let call = Call::new_in(self); + let succeeded = erc20.transfer(call, to, amount)?; + if !succeeded { + return Err(ReleaseTokenFailed { token }.into()); + } + + Ok(()) + } } #[cfg(all(test, feature = "std"))] diff --git a/examples/vesting-wallet/src/constructor.sol b/examples/vesting-wallet/src/constructor.sol index b8b28df05..01c9ac987 100644 --- a/examples/vesting-wallet/src/constructor.sol +++ b/examples/vesting-wallet/src/constructor.sol @@ -14,17 +14,17 @@ contract VestingWalletExample { address indexed previousOwner, address indexed newOwner ); - + constructor(address beneficiary, uint64 startTimestamp, uint64 durationSeconds) payable { - _start = startTimestamp; - _duration = durationSeconds; - if (beneficiary == address(0)) { revert OwnableInvalidOwner(address(0)); } _transferOwnership(beneficiary); + + _start = startTimestamp; + _duration = durationSeconds; } - + function _transferOwnership(address newOwner) internal virtual { address oldOwner = _owner; _owner = newOwner; diff --git a/examples/vesting-wallet/src/lib.rs b/examples/vesting-wallet/src/lib.rs index 6396629a5..89a80c74b 100644 --- a/examples/vesting-wallet/src/lib.rs +++ b/examples/vesting-wallet/src/lib.rs @@ -1,8 +1,10 @@ #![cfg_attr(not(test), no_main, no_std)] extern crate alloc; +use alloy_primitives::{Address, U256}; use openzeppelin_stylus::{ - access::ownable::Ownable, finance::vesting_wallet::VestingWallet, + access::ownable::Ownable, + finance::vesting_wallet::{Error, VestingWallet}, }; use stylus_sdk::prelude::{entrypoint, public, sol_storage}; @@ -18,4 +20,69 @@ sol_storage! { #[public] #[inherit(VestingWallet, Ownable)] -impl VestingWalletExample {} +impl VestingWalletExample { + /// The contract should be able to receive Eth. + #[payable] + pub fn receive_ether(&self) {} + + /// Getter for the start timestamp. + pub fn start(&self) -> U256 { + self.vesting_wallet.start() + } + + /// Getter for the vesting duration. + pub fn duration(&self) -> U256 { + self.vesting_wallet.duration() + } + + /// Getter for the end timestamp. + pub fn end(&self) -> U256 { + self.vesting_wallet.end() + } + + #[selector(name = "released")] + pub fn released_eth(&self) -> U256 { + self.vesting_wallet.released_eth() + } + + #[selector(name = "released")] + pub fn released_erc20(&self, token: Address) -> U256 { + self.vesting_wallet.released_erc20(token) + } + + #[selector(name = "releasable")] + pub fn releasable_eth(&self) -> U256 { + self.vesting_wallet.releasable_eth() + } + + #[selector(name = "releasable")] + pub fn releasable_erc20(&mut self, token: Address) -> U256 { + self.vesting_wallet.releasable_erc20(token) + } + + #[selector(name = "vestedAmount")] + pub fn vested_amount_eth(&self, timestamp: u64) -> U256 { + self.vesting_wallet.vested_amount_eth(timestamp) + } + + #[selector(name = "vestedAmount")] + pub fn vested_amount_erc20( + &mut self, + token: Address, + timestamp: u64, + ) -> U256 { + self.vesting_wallet.vested_amount_erc20(token, timestamp) + } + + #[selector(name = "release")] + pub fn release_eth(&mut self) -> Result<(), Error> { + let owner = self.ownable.owner(); + self.vesting_wallet._release_eth(owner) + } + + #[selector(name = "release")] + pub fn release_erc20(&mut self, token: Address) -> Result<(), Error> { + let owner = self.ownable.owner(); + self.vesting_wallet._release_erc20(owner, token) + } +} diff --git a/scripts/e2e-tests.sh b/scripts/e2e-tests.sh index f7b9cdeb1..ec92a3c88 100755 --- a/scripts/e2e-tests.sh +++ b/scripts/e2e-tests.sh @@ -11,4 +11,4 @@ cargo +"$NIGHTLY_TOOLCHAIN" build --release --target wasm32-unknown-unknown -Z b export RPC_URL=http://localhost:8547 # We should use stable here once nitro-testnode is updated and the contracts fit # the size limit. Work tracked [here](https://github.com/OpenZeppelin/rust-contracts-stylus/issues/87) -cargo +"$NIGHTLY_TOOLCHAIN" test --features std,e2e --test "*" +cargo +"$NIGHTLY_TOOLCHAIN" test --features std,e2e --test "vesting_wallet" From e33722d0f9744208f320eab569b21d2d00f2006b Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 11 Oct 2024 10:59:52 +0200 Subject: [PATCH 023/138] Revert "Redefine methods in VestingWalletExample" This reverts commit 339058d20e0a79a2111a5763fcdbec24fde0194c. --- contracts/src/finance/vesting_wallet.rs | 107 +++++++++++--------- examples/vesting-wallet/src/constructor.sol | 10 +- examples/vesting-wallet/src/lib.rs | 71 +------------ scripts/e2e-tests.sh | 2 +- 4 files changed, 65 insertions(+), 125 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 3b4fb3e95..21ead6995 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -10,11 +10,6 @@ //! By setting the duration to 0, one can configure this contract to behave like //! an asset timelock that hold tokens for a beneficiary until a specified time. //! -//! Fields `_start` (indicates the timestamp when the vesting start), -//! `_duration` (indicates how long the vesting lasts), and `_owner` (the -//! beneficiary of the wallet) can be set during construction with `koba` (stylus -//! construction tooling) within solidity constructor file. -//! //! NOTE: Since the wallet is [`crate::access::ownable::Ownable`], and ownership //! can be transferred, it is possible to sell unvested tokens. Preventing this //! in a smart contract is difficult, considering that: 1) a beneficiary address @@ -36,6 +31,8 @@ use stylus_sdk::{ stylus_proc::{public, sol_interface, sol_storage}, }; +use crate::access::ownable::Ownable; + sol! { /// Emitted when `amount` of ether has been released. #[allow(missing_docs)] @@ -91,6 +88,9 @@ sol_storage! { uint64 _start; /// Vesting duration. uint64 _duration; + /// Ownable contract + #[borrow] + Ownable ownable; } } @@ -100,6 +100,7 @@ sol_storage! { unsafe impl TopLevelStorage for VestingWallet {} #[public] +#[inherit(Ownable)] impl VestingWallet { /// The contract should be able to receive Eth. #[payable] @@ -121,29 +122,79 @@ impl VestingWallet { } /// Amount of eth already released + #[selector(name = "released")] pub fn released_eth(&self) -> U256 { self._released.get() } /// Amount of token already released + #[selector(name = "released")] pub fn released_erc20(&self, token: Address) -> U256 { self._erc20_released.get(token) } /// Getter for the amount of releasable eth. + #[selector(name = "releasable")] pub fn releasable_eth(&self) -> U256 { self.vested_amount_eth(block::timestamp()) - self.released_eth() } /// Getter for the amount of releasable `token` tokens. `token` should be /// the address of an [`crate::token::erc20::Erc20`] contract. + #[selector(name = "releasable")] pub fn releasable_erc20(&mut self, token: Address) -> U256 { self.vested_amount_erc20(token, block::timestamp()) - self.released_erc20(token) } + /// Release the native token (ether) that have already vested. + /// + /// Emits an [`EtherReleased`] event. + #[selector(name = "release")] + pub fn release_eth(&mut self) -> Result<(), Error> { + let amount = self.releasable_eth(); + let released = self + .released_eth() + .checked_add(amount) + .expect("should not exceed `U256::MAX` for `_released`"); + self._released.set(released); + + evm::log(EtherReleased { amount }); + + let owner = self.ownable.owner(); + call(Call::new_in(self).value(amount), owner, &[])?; + + Ok(()) + } + + /// Release the tokens that have already vested. + /// + /// Emits an [`ERC20Released`] event. + #[selector(name = "release")] + pub fn release_erc20(&mut self, token: Address) -> Result<(), Error> { + let amount = self.releasable_erc20(token); + let released = self + .released_erc20(token) + .checked_add(amount) + .expect("should not exceed `U256::MAX` for `_erc20Released`"); + self._erc20_released.setter(token).set(released); + + evm::log(ERC20Released { token, amount }); + + let erc20 = IERC20::new(token); + let owner = self.ownable.owner(); + let call = Call::new_in(self); + let succeeded = erc20.transfer(call, owner, amount)?; + if !succeeded { + return Err(ReleaseTokenFailed { token }.into()); + } + + Ok(()) + } + /// Calculates the amount of ether that has already vested. Default /// implementation is a linear vesting curve. + #[selector(name = "vestedAmount")] pub fn vested_amount_eth(&self, timestamp: u64) -> U256 { self._vesting_schedule( contract::balance() + self.released_eth(), @@ -153,6 +204,7 @@ impl VestingWallet { /// Calculates the amount of tokens that has already vested. Default /// implementation is a linear vesting curve. + #[selector(name = "vestedAmount")] pub fn vested_amount_erc20( &mut self, token: Address, @@ -189,51 +241,6 @@ impl VestingWallet { / self.duration() } } - - /// Release the native token (ether) that have already vested. - /// - /// Emits an [`EtherReleased`] event. - pub fn _release_eth(&mut self, to: Address) -> Result<(), Error> { - let amount = self.releasable_eth(); - let released = self - .released_eth() - .checked_add(amount) - .expect("should not exceed `U256::MAX` for `_released`"); - self._released.set(released); - - evm::log(EtherReleased { amount }); - - call(Call::new_in(self).value(amount), to, &[])?; - - Ok(()) - } - - /// Release the tokens that have already vested. - /// - /// Emits an [`ERC20Released`] event. - pub fn _release_erc20( - &mut self, - to: Address, - token: Address, - ) -> Result<(), Error> { - let amount = self.releasable_erc20(token); - let released = self - .released_erc20(token) - .checked_add(amount) - .expect("should not exceed `U256::MAX` for `_erc20Released`"); - self._erc20_released.setter(token).set(released); - - evm::log(ERC20Released { token, amount }); - - let erc20 = IERC20::new(token); - let call = Call::new_in(self); - let succeeded = erc20.transfer(call, to, amount)?; - if !succeeded { - return Err(ReleaseTokenFailed { token }.into()); - } - - Ok(()) - } } #[cfg(all(test, feature = "std"))] diff --git a/examples/vesting-wallet/src/constructor.sol b/examples/vesting-wallet/src/constructor.sol index 01c9ac987..b8b28df05 100644 --- a/examples/vesting-wallet/src/constructor.sol +++ b/examples/vesting-wallet/src/constructor.sol @@ -14,17 +14,17 @@ contract VestingWalletExample { address indexed previousOwner, address indexed newOwner ); - + constructor(address beneficiary, uint64 startTimestamp, uint64 durationSeconds) payable { + _start = startTimestamp; + _duration = durationSeconds; + if (beneficiary == address(0)) { revert OwnableInvalidOwner(address(0)); } _transferOwnership(beneficiary); - - _start = startTimestamp; - _duration = durationSeconds; } - + function _transferOwnership(address newOwner) internal virtual { address oldOwner = _owner; _owner = newOwner; diff --git a/examples/vesting-wallet/src/lib.rs b/examples/vesting-wallet/src/lib.rs index 89a80c74b..6396629a5 100644 --- a/examples/vesting-wallet/src/lib.rs +++ b/examples/vesting-wallet/src/lib.rs @@ -1,10 +1,8 @@ #![cfg_attr(not(test), no_main, no_std)] extern crate alloc; -use alloy_primitives::{Address, U256}; use openzeppelin_stylus::{ - access::ownable::Ownable, - finance::vesting_wallet::{Error, VestingWallet}, + access::ownable::Ownable, finance::vesting_wallet::VestingWallet, }; use stylus_sdk::prelude::{entrypoint, public, sol_storage}; @@ -20,69 +18,4 @@ sol_storage! { #[public] #[inherit(VestingWallet, Ownable)] -impl VestingWalletExample { - /// The contract should be able to receive Eth. - #[payable] - pub fn receive_ether(&self) {} - - /// Getter for the start timestamp. - pub fn start(&self) -> U256 { - self.vesting_wallet.start() - } - - /// Getter for the vesting duration. - pub fn duration(&self) -> U256 { - self.vesting_wallet.duration() - } - - /// Getter for the end timestamp. - pub fn end(&self) -> U256 { - self.vesting_wallet.end() - } - - #[selector(name = "released")] - pub fn released_eth(&self) -> U256 { - self.vesting_wallet.released_eth() - } - - #[selector(name = "released")] - pub fn released_erc20(&self, token: Address) -> U256 { - self.vesting_wallet.released_erc20(token) - } - - #[selector(name = "releasable")] - pub fn releasable_eth(&self) -> U256 { - self.vesting_wallet.releasable_eth() - } - - #[selector(name = "releasable")] - pub fn releasable_erc20(&mut self, token: Address) -> U256 { - self.vesting_wallet.releasable_erc20(token) - } - - #[selector(name = "vestedAmount")] - pub fn vested_amount_eth(&self, timestamp: u64) -> U256 { - self.vesting_wallet.vested_amount_eth(timestamp) - } - - #[selector(name = "vestedAmount")] - pub fn vested_amount_erc20( - &mut self, - token: Address, - timestamp: u64, - ) -> U256 { - self.vesting_wallet.vested_amount_erc20(token, timestamp) - } - - #[selector(name = "release")] - pub fn release_eth(&mut self) -> Result<(), Error> { - let owner = self.ownable.owner(); - self.vesting_wallet._release_eth(owner) - } - - #[selector(name = "release")] - pub fn release_erc20(&mut self, token: Address) -> Result<(), Error> { - let owner = self.ownable.owner(); - self.vesting_wallet._release_erc20(owner, token) - } -} +impl VestingWalletExample {} diff --git a/scripts/e2e-tests.sh b/scripts/e2e-tests.sh index ec92a3c88..f7b9cdeb1 100755 --- a/scripts/e2e-tests.sh +++ b/scripts/e2e-tests.sh @@ -11,4 +11,4 @@ cargo +"$NIGHTLY_TOOLCHAIN" build --release --target wasm32-unknown-unknown -Z b export RPC_URL=http://localhost:8547 # We should use stable here once nitro-testnode is updated and the contracts fit # the size limit. Work tracked [here](https://github.com/OpenZeppelin/rust-contracts-stylus/issues/87) -cargo +"$NIGHTLY_TOOLCHAIN" test --features std,e2e --test "vesting_wallet" +cargo +"$NIGHTLY_TOOLCHAIN" test --features std,e2e --test "*" From 37a11ac379e62ddd79a0edae67d040e2927b6696 Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 11 Oct 2024 11:03:34 +0200 Subject: [PATCH 024/138] Disinherit Ownable in VestingWallet --- contracts/src/finance/vesting_wallet.rs | 2 -- examples/vesting-wallet/src/constructor.sol | 18 +----------------- examples/vesting-wallet/src/lib.rs | 8 ++------ 3 files changed, 3 insertions(+), 25 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 21ead6995..5ca25f7be 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -89,7 +89,6 @@ sol_storage! { /// Vesting duration. uint64 _duration; /// Ownable contract - #[borrow] Ownable ownable; } } @@ -100,7 +99,6 @@ sol_storage! { unsafe impl TopLevelStorage for VestingWallet {} #[public] -#[inherit(Ownable)] impl VestingWallet { /// The contract should be able to receive Eth. #[payable] diff --git a/examples/vesting-wallet/src/constructor.sol b/examples/vesting-wallet/src/constructor.sol index b8b28df05..b208ccf48 100644 --- a/examples/vesting-wallet/src/constructor.sol +++ b/examples/vesting-wallet/src/constructor.sol @@ -9,25 +9,9 @@ contract VestingWalletExample { address private _owner; - error OwnableInvalidOwner(address owner); - event OwnershipTransferred( - address indexed previousOwner, - address indexed newOwner - ); - constructor(address beneficiary, uint64 startTimestamp, uint64 durationSeconds) payable { + _owner = beneficiary; _start = startTimestamp; _duration = durationSeconds; - - if (beneficiary == address(0)) { - revert OwnableInvalidOwner(address(0)); - } - _transferOwnership(beneficiary); - } - - function _transferOwnership(address newOwner) internal virtual { - address oldOwner = _owner; - _owner = newOwner; - emit OwnershipTransferred(oldOwner, newOwner); } } diff --git a/examples/vesting-wallet/src/lib.rs b/examples/vesting-wallet/src/lib.rs index 6396629a5..20a5bcfd1 100644 --- a/examples/vesting-wallet/src/lib.rs +++ b/examples/vesting-wallet/src/lib.rs @@ -1,21 +1,17 @@ #![cfg_attr(not(test), no_main, no_std)] extern crate alloc; -use openzeppelin_stylus::{ - access::ownable::Ownable, finance::vesting_wallet::VestingWallet, -}; +use openzeppelin_stylus::finance::vesting_wallet::VestingWallet; use stylus_sdk::prelude::{entrypoint, public, sol_storage}; sol_storage! { #[entrypoint] struct VestingWalletExample { - #[borrow] - Ownable ownable; #[borrow] VestingWallet vesting_wallet; } } #[public] -#[inherit(VestingWallet, Ownable)] +#[inherit(VestingWallet)] impl VestingWalletExample {} From efd4b3449ba9f3278579592b696e41df6ae89c71 Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 11 Oct 2024 11:56:43 +0200 Subject: [PATCH 025/138] Add Ownable errors + override owner method --- contracts/src/access/ownable.rs | 7 +++++++ contracts/src/finance/vesting_wallet.rs | 13 ++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/contracts/src/access/ownable.rs b/contracts/src/access/ownable.rs index 157457fd1..da5a9d4e8 100644 --- a/contracts/src/access/ownable.rs +++ b/contracts/src/access/ownable.rs @@ -11,6 +11,7 @@ use alloy_primitives::Address; use alloy_sol_types::sol; use stylus_sdk::{ + call::MethodError, evm, msg, stylus_proc::{public, sol_storage, SolidityError}, }; @@ -45,6 +46,12 @@ pub enum Error { InvalidOwner(OwnableInvalidOwner), } +impl MethodError for Error { + fn encode(self) -> alloc::vec::Vec { + self.into() + } +} + sol_storage! { /// State of an `Ownable` contract. pub struct Ownable { diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 5ca25f7be..3cfab13a0 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -31,7 +31,7 @@ use stylus_sdk::{ stylus_proc::{public, sol_interface, sol_storage}, }; -use crate::access::ownable::Ownable; +use crate::access::{ownable, ownable::Ownable}; sol! { /// Emitted when `amount` of ether has been released. @@ -70,6 +70,8 @@ sol_interface! { /// An error that occurred in the [`VestingWallet`] contract. #[derive(SolidityError, Debug)] pub enum Error { + /// Error type from [`Ownable`] contract [`ownable::Error`]. + Ownable(ownable::Error), /// Error type from [`stylus_sdk::call::Call`] contract /// [`stylus_sdk::call::Error`]. StylusError(stylus_sdk::call::Error), @@ -80,6 +82,8 @@ pub enum Error { sol_storage! { /// State of a VestingWallet Contract. pub struct VestingWallet { + /// Ownable contract + Ownable ownable; /// Amount of eth already released. uint256 _released; /// Amount of ERC20 tokens already released. @@ -88,8 +92,6 @@ sol_storage! { uint64 _start; /// Vesting duration. uint64 _duration; - /// Ownable contract - Ownable ownable; } } @@ -104,6 +106,11 @@ impl VestingWallet { #[payable] pub fn receive_ether(&self) {} + /// Returns the address of the current owner. + pub fn owner(&self) -> Address { + self.ownable.owner() + } + /// Getter for the start timestamp. pub fn start(&self) -> U256 { U256::from(self._start.get()) From 21f542580841db799c4bb2a619b675171bd3badb Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 11 Oct 2024 13:21:06 +0200 Subject: [PATCH 026/138] reorder fields to match actual contract's --- examples/vesting-wallet/src/constructor.sol | 6 +++--- examples/vesting-wallet/tests/abi/mod.rs | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/vesting-wallet/src/constructor.sol b/examples/vesting-wallet/src/constructor.sol index b208ccf48..cf6c84a4f 100644 --- a/examples/vesting-wallet/src/constructor.sol +++ b/examples/vesting-wallet/src/constructor.sol @@ -2,14 +2,14 @@ pragma solidity ^0.8.21; contract VestingWalletExample { + address private _owner; + uint256 private _released; mapping(address token => uint256) private _erc20Released; uint64 private immutable _start; uint64 private immutable _duration; - - address private _owner; - constructor(address beneficiary, uint64 startTimestamp, uint64 durationSeconds) payable { + constructor(address beneficiary, uint64 startTimestamp, uint64 durationSeconds) { _owner = beneficiary; _start = startTimestamp; _duration = durationSeconds; diff --git a/examples/vesting-wallet/tests/abi/mod.rs b/examples/vesting-wallet/tests/abi/mod.rs index 4b890e576..2a4e94e95 100644 --- a/examples/vesting-wallet/tests/abi/mod.rs +++ b/examples/vesting-wallet/tests/abi/mod.rs @@ -4,6 +4,7 @@ use alloy::sol; sol!( #[sol(rpc)] contract VestingWallet { + function owner() public view virtual returns (address owner); function start() external view returns (uint256 start); function duration() external view returns (uint256 duration); function end() external view returns (uint256 end); From 0604a0feda22ca8beb0eb73e047ffe3fbca5f2e6 Mon Sep 17 00:00:00 2001 From: Nenad Date: Sat, 12 Oct 2024 11:08:37 +0200 Subject: [PATCH 027/138] Remove immutable from constructor.sol --- examples/vesting-wallet/src/constructor.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/vesting-wallet/src/constructor.sol b/examples/vesting-wallet/src/constructor.sol index cf6c84a4f..543c091f5 100644 --- a/examples/vesting-wallet/src/constructor.sol +++ b/examples/vesting-wallet/src/constructor.sol @@ -6,8 +6,8 @@ contract VestingWalletExample { uint256 private _released; mapping(address token => uint256) private _erc20Released; - uint64 private immutable _start; - uint64 private immutable _duration; + uint64 private _start; + uint64 private _duration; constructor(address beneficiary, uint64 startTimestamp, uint64 durationSeconds) { _owner = beneficiary; From 39dd74b4f81f481ba47caa2cc039740383019f4a Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 14 Oct 2024 07:36:25 +0200 Subject: [PATCH 028/138] Rename vesting_wallet->vesting-wallet --- .../tests/{vesting_wallet.rs => vesting-wallet.rs} | 0 scripts/e2e-tests.sh | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename examples/vesting-wallet/tests/{vesting_wallet.rs => vesting-wallet.rs} (100%) diff --git a/examples/vesting-wallet/tests/vesting_wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs similarity index 100% rename from examples/vesting-wallet/tests/vesting_wallet.rs rename to examples/vesting-wallet/tests/vesting-wallet.rs diff --git a/scripts/e2e-tests.sh b/scripts/e2e-tests.sh index f7b9cdeb1..953b986fb 100755 --- a/scripts/e2e-tests.sh +++ b/scripts/e2e-tests.sh @@ -11,4 +11,4 @@ cargo +"$NIGHTLY_TOOLCHAIN" build --release --target wasm32-unknown-unknown -Z b export RPC_URL=http://localhost:8547 # We should use stable here once nitro-testnode is updated and the contracts fit # the size limit. Work tracked [here](https://github.com/OpenZeppelin/rust-contracts-stylus/issues/87) -cargo +"$NIGHTLY_TOOLCHAIN" test --features std,e2e --test "*" +cargo +"$NIGHTLY_TOOLCHAIN" test --features e2e --test "vesting-wallet" From 44b9990eda2d81db3e520c81fbcd560f53546d1e Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 14 Oct 2024 07:41:55 +0200 Subject: [PATCH 029/138] use Ownable constructor logic in constructor.sol --- examples/vesting-wallet/src/constructor.sol | 20 ++++++++++++++++++-- examples/vesting-wallet/tests/abi/mod.rs | 5 +++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/examples/vesting-wallet/src/constructor.sol b/examples/vesting-wallet/src/constructor.sol index 543c091f5..97ff8b3c9 100644 --- a/examples/vesting-wallet/src/constructor.sol +++ b/examples/vesting-wallet/src/constructor.sol @@ -9,9 +9,25 @@ contract VestingWalletExample { uint64 private _start; uint64 private _duration; - constructor(address beneficiary, uint64 startTimestamp, uint64 durationSeconds) { - _owner = beneficiary; + error OwnableInvalidOwner(address owner); + event OwnershipTransferred( + address indexed previousOwner, + address indexed newOwner + ); + + constructor(address beneficiary, uint64 startTimestamp, uint64 durationSeconds) payable { + if (beneficiary == address(0)) { + revert OwnableInvalidOwner(address(0)); + } + _transferOwnership(beneficiary); + _start = startTimestamp; _duration = durationSeconds; } + + function _transferOwnership(address newOwner) internal virtual { + address oldOwner = _owner; + _owner = newOwner; + emit OwnershipTransferred(oldOwner, newOwner); + } } diff --git a/examples/vesting-wallet/tests/abi/mod.rs b/examples/vesting-wallet/tests/abi/mod.rs index 2a4e94e95..17bc53e55 100644 --- a/examples/vesting-wallet/tests/abi/mod.rs +++ b/examples/vesting-wallet/tests/abi/mod.rs @@ -17,8 +17,13 @@ sol!( function vestedAmount(uint64 timestamp) external view returns (uint256 vestedAmount); function vestedAmount(address token, uint64 timestamp) external view returns (uint256 vestedAmount); + error OwnableUnauthorizedAccount(address account); + error OwnableInvalidOwner(address owner); + error ReleaseTokenFailed(address token); + #[derive(Debug, PartialEq)] + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); #[derive(Debug, PartialEq)] event EtherReleased(uint256 amount); #[derive(Debug, PartialEq)] From 3cd13a8a7cb55b113c365f19f2ee7638a6902835 Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 14 Oct 2024 08:02:10 +0200 Subject: [PATCH 030/138] Set const BLOCK_TIMESTAMP in tests --- examples/vesting-wallet/tests/vesting-wallet.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/vesting-wallet/tests/vesting-wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs index 33d33a67c..081b04919 100644 --- a/examples/vesting-wallet/tests/vesting-wallet.rs +++ b/examples/vesting-wallet/tests/vesting-wallet.rs @@ -10,7 +10,9 @@ mod abi; sol!("src/constructor.sol"); -const HOUR: u64 = 3600; +// Epoch timestamp: 1st January 2025 00::00::00 +const BLOCK_TIMESTAMP: u64 = 1_735_689_600; +const START: u64 = BLOCK_TIMESTAMP + 3600; // 1 hour const YEAR: u64 = 365 * 86400; fn ctr( @@ -27,24 +29,22 @@ fn ctr( #[e2e::test] async fn constructs(alice: Account) -> eyre::Result<()> { - let start = HOUR; let contract_addr = alice .as_deployer() - .with_constructor(ctr(alice.address(), start, YEAR)) + .with_constructor(ctr(alice.address(), START, YEAR)) .deploy() .await? .address()?; let contract = VestingWallet::new(contract_addr, &alice.wallet); - let VestingWallet::startReturn { start: _start } = - contract.start().call().await?; + let VestingWallet::startReturn { start } = contract.start().call().await?; let VestingWallet::durationReturn { duration } = contract.duration().call().await?; let VestingWallet::endReturn { end } = contract.end().call().await?; - assert_eq!(U256::from(start), _start); + assert_eq!(U256::from(START), start); assert_eq!(U256::from(YEAR), duration); - assert_eq!(end, U256::from(start + YEAR)); + assert_eq!(end, U256::from(START + YEAR)); Ok(()) } From 584a3e5facd7eaee7a8990b221693321305b5758 Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 14 Oct 2024 08:23:51 +0200 Subject: [PATCH 031/138] add mock erc20 --- examples/vesting-wallet/tests/mock/erc20.rs | 48 +++++++++++++++++++++ examples/vesting-wallet/tests/mock/mod.rs | 1 + 2 files changed, 49 insertions(+) create mode 100644 examples/vesting-wallet/tests/mock/erc20.rs create mode 100644 examples/vesting-wallet/tests/mock/mod.rs diff --git a/examples/vesting-wallet/tests/mock/erc20.rs b/examples/vesting-wallet/tests/mock/erc20.rs new file mode 100644 index 000000000..68514daea --- /dev/null +++ b/examples/vesting-wallet/tests/mock/erc20.rs @@ -0,0 +1,48 @@ +#![allow(dead_code)] +#![cfg(feature = "e2e")] +use alloy::{primitives::Address, sol}; +use e2e::Wallet; + +sol! { + #[allow(missing_docs)] + // Built with Remix IDE; solc v0.8.21+commit.d9974bed + #[sol(rpc, bytecode="608060405234801562000010575f80fd5b506040518060400160405280600981526020017f45524332304d6f636b00000000000000000000000000000000000000000000008152506040518060400160405280600381526020017f4d544b000000000000000000000000000000000000000000000000000000000081525081600390816200008e91906200030d565b508060049081620000a091906200030d565b505050620003f1565b5f81519050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b5f60028204905060018216806200012557607f821691505b6020821081036200013b576200013a620000e0565b5b50919050565b5f819050815f5260205f209050919050565b5f6020601f8301049050919050565b5f82821b905092915050565b5f600883026200019f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8262000162565b620001ab868362000162565b95508019841693508086168417925050509392505050565b5f819050919050565b5f819050919050565b5f620001f5620001ef620001e984620001c3565b620001cc565b620001c3565b9050919050565b5f819050919050565b6200021083620001d5565b620002286200021f82620001fc565b8484546200016e565b825550505050565b5f90565b6200023e62000230565b6200024b81848462000205565b505050565b5b818110156200027257620002665f8262000234565b60018101905062000251565b5050565b601f821115620002c1576200028b8162000141565b620002968462000153565b81016020851015620002a6578190505b620002be620002b58562000153565b83018262000250565b50505b505050565b5f82821c905092915050565b5f620002e35f1984600802620002c6565b1980831691505092915050565b5f620002fd8383620002d2565b9150826002028217905092915050565b6200031882620000a9565b67ffffffffffffffff811115620003345762000333620000b3565b5b6200034082546200010d565b6200034d82828562000276565b5f60209050601f83116001811462000383575f84156200036e578287015190505b6200037a8582620002f0565b865550620003e9565b601f198416620003938662000141565b5f5b82811015620003bc5784890151825560018201915060208501945060208101905062000395565b86831015620003dc5784890151620003d8601f891682620002d2565b8355505b6001600288020188555050505b505050505050565b610f4580620003ff5f395ff3fe608060405234801561000f575f80fd5b50600436106100a7575f3560e01c806340c10f191161006f57806340c10f191461016557806370a08231146101815780638483acfe146101b157806395d89b41146101cd578063a9059cbb146101eb578063dd62ed3e1461021b576100a7565b806306fdde03146100ab578063095ea7b3146100c957806318160ddd146100f957806323b872dd14610117578063313ce56714610147575b5f80fd5b6100b361024b565b6040516100c09190610bbe565b60405180910390f35b6100e360048036038101906100de9190610c6f565b6102db565b6040516100f09190610cc7565b60405180910390f35b6101016102ee565b60405161010e9190610cef565b60405180910390f35b610131600480360381019061012c9190610d08565b6102f7565b60405161013e9190610cc7565b60405180910390f35b61014f61030c565b60405161015c9190610d73565b60405180910390f35b61017f600480360381019061017a9190610c6f565b610314565b005b61019b60048036038101906101969190610d8c565b610322565b6040516101a89190610cef565b60405180910390f35b6101cb60048036038101906101c69190610d08565b610333565b005b6101d5610343565b6040516101e29190610bbe565b60405180910390f35b61020560048036038101906102009190610c6f565b6103d3565b6040516102129190610cc7565b60405180910390f35b61023560048036038101906102309190610db7565b6103e6565b6040516102429190610cef565b60405180910390f35b60606003805461025a90610e22565b80601f016020809104026020016040519081016040528092919081815260200182805461028690610e22565b80156102d15780601f106102a8576101008083540402835291602001916102d1565b820191905f5260205f20905b8154815290600101906020018083116102b457829003601f168201915b5050505050905090565b5f6102e683836103f9565b905092915050565b5f600254905090565b5f61030384848461041b565b90509392505050565b5f6012905090565b61031e8282610449565b5050565b5f61032c826104c8565b9050919050565b61033e83838361050d565b505050565b60606004805461035290610e22565b80601f016020809104026020016040519081016040528092919081815260200182805461037e90610e22565b80156103c95780601f106103a0576101008083540402835291602001916103c9565b820191905f5260205f20905b8154815290600101906020018083116103ac57829003601f168201915b5050505050905090565b5f6103de838361051f565b905092915050565b5f6103f18383610541565b905092915050565b5f806104036105c3565b905061041081858561050d565b600191505092915050565b5f806104256105c3565b90506104328582856105ca565b61043d85858561065c565b60019150509392505050565b5f73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036104b9575f6040517fec442f050000000000000000000000000000000000000000000000000000000081526004016104b09190610e61565b60405180910390fd5b6104c45f838361074c565b5050565b5f805f8373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f20549050919050565b61051a8383836001610965565b505050565b5f806105296105c3565b905061053681858561065c565b600191505092915050565b5f60015f8473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f2054905092915050565b5f33905090565b5f6105d584846103e6565b90507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81146106565781811015610647578281836040517ffb8f41b200000000000000000000000000000000000000000000000000000000815260040161063e93929190610e7a565b60405180910390fd5b61065584848484035f610965565b5b50505050565b5f73ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16036106cc575f6040517f96c6fd1e0000000000000000000000000000000000000000000000000000000081526004016106c39190610e61565b60405180910390fd5b5f73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff160361073c575f6040517fec442f050000000000000000000000000000000000000000000000000000000081526004016107339190610e61565b60405180910390fd5b61074783838361074c565b505050565b5f73ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff160361079c578060025f8282546107909190610edc565b9250508190555061086a565b5f805f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f2054905081811015610825578381836040517fe450d38c00000000000000000000000000000000000000000000000000000000815260040161081c93929190610e7a565b60405180910390fd5b8181035f808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f2081905550505b5f73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036108b1578060025f82825403925050819055506108fb565b805f808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f82825401925050819055505b8173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040516109589190610cef565b60405180910390a3505050565b5f73ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff16036109d5575f6040517fe602df050000000000000000000000000000000000000000000000000000000081526004016109cc9190610e61565b60405180910390fd5b5f73ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1603610a45575f6040517f94280d62000000000000000000000000000000000000000000000000000000008152600401610a3c9190610e61565b60405180910390fd5b8160015f8673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f20819055508015610b2e578273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92584604051610b259190610cef565b60405180910390a35b50505050565b5f81519050919050565b5f82825260208201905092915050565b5f5b83811015610b6b578082015181840152602081019050610b50565b5f8484015250505050565b5f601f19601f8301169050919050565b5f610b9082610b34565b610b9a8185610b3e565b9350610baa818560208601610b4e565b610bb381610b76565b840191505092915050565b5f6020820190508181035f830152610bd68184610b86565b905092915050565b5f80fd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f610c0b82610be2565b9050919050565b610c1b81610c01565b8114610c25575f80fd5b50565b5f81359050610c3681610c12565b92915050565b5f819050919050565b610c4e81610c3c565b8114610c58575f80fd5b50565b5f81359050610c6981610c45565b92915050565b5f8060408385031215610c8557610c84610bde565b5b5f610c9285828601610c28565b9250506020610ca385828601610c5b565b9150509250929050565b5f8115159050919050565b610cc181610cad565b82525050565b5f602082019050610cda5f830184610cb8565b92915050565b610ce981610c3c565b82525050565b5f602082019050610d025f830184610ce0565b92915050565b5f805f60608486031215610d1f57610d1e610bde565b5b5f610d2c86828701610c28565b9350506020610d3d86828701610c28565b9250506040610d4e86828701610c5b565b9150509250925092565b5f60ff82169050919050565b610d6d81610d58565b82525050565b5f602082019050610d865f830184610d64565b92915050565b5f60208284031215610da157610da0610bde565b5b5f610dae84828501610c28565b91505092915050565b5f8060408385031215610dcd57610dcc610bde565b5b5f610dda85828601610c28565b9250506020610deb85828601610c28565b9150509250929050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b5f6002820490506001821680610e3957607f821691505b602082108103610e4c57610e4b610df5565b5b50919050565b610e5b81610c01565b82525050565b5f602082019050610e745f830184610e52565b92915050565b5f606082019050610e8d5f830186610e52565b610e9a6020830185610ce0565b610ea76040830184610ce0565b949350505050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f610ee682610c3c565b9150610ef183610c3c565b9250828201905080821115610f0957610f08610eaf565b5b9291505056fea2646970667358221220383e898342e74543d1bfb6186eff00b4ae7a39d4ecde6190742c5e9f2a7a2e9364736f6c63430008150033")] + // SPDX-License-Identifier: MIT + contract ERC20Mock is ERC20 { + constructor() ERC20("ERC20Mock", "MTK") {} + + function approve(address spender, uint256 value) public override returns (bool) { + return super.approve(spender, value); + } + + function regular_approve(address owner, address spender, uint256 amount) public { + super._approve(owner, spender, amount); + } + + function balanceOf(address account) public override view returns (uint256) { + return super.balanceOf(account); + } + + function mint(address account, uint256 value) public { + super._mint(account, value); + } + + function transfer(address to, uint256 amount) public override returns (bool) { + return super.transfer(to, amount); + } + + function transferFrom(address from, address to, uint256 value) public override returns (bool) { + return super.transferFrom(from, to, value); + } + + function allowance(address owner, address spender) public view override returns (uint256) { + return super.allowance(owner, spender); + } + } +} + +pub async fn deploy(wallet: &Wallet) -> eyre::Result
{ + // Deploy the contract. + let contract = ERC20Mock::deploy(wallet).await?; + Ok(*contract.address()) +} diff --git a/examples/vesting-wallet/tests/mock/mod.rs b/examples/vesting-wallet/tests/mock/mod.rs new file mode 100644 index 000000000..8f3777f6b --- /dev/null +++ b/examples/vesting-wallet/tests/mock/mod.rs @@ -0,0 +1 @@ +pub mod erc20; From 57227cee52a8ced35f2a13ed32dcb391c6a961e7 Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 14 Oct 2024 09:16:11 +0200 Subject: [PATCH 032/138] Add working erc20 vesting test --- .../vesting-wallet/tests/vesting-wallet.rs | 48 ++++++++++++++++--- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/examples/vesting-wallet/tests/vesting-wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs index 081b04919..d5a8cbd25 100644 --- a/examples/vesting-wallet/tests/vesting-wallet.rs +++ b/examples/vesting-wallet/tests/vesting-wallet.rs @@ -2,18 +2,23 @@ use alloy::sol; use alloy_primitives::{Address, U256}; -use e2e::{Account, ReceiptExt}; +use e2e::{watch, Account, ReceiptExt}; -use crate::{abi::VestingWallet, VestingWalletExample::constructorCall}; +use crate::{ + abi::VestingWallet, + mock::{erc20, erc20::ERC20Mock}, + VestingWalletExample::constructorCall, +}; mod abi; +mod mock; sol!("src/constructor.sol"); // Epoch timestamp: 1st January 2025 00::00::00 const BLOCK_TIMESTAMP: u64 = 1_735_689_600; const START: u64 = BLOCK_TIMESTAMP + 3600; // 1 hour -const YEAR: u64 = 365 * 86400; +const DURATION: u64 = 365 * 86400; // 1 year fn ctr( beneficiary: Address, @@ -31,7 +36,7 @@ fn ctr( async fn constructs(alice: Account) -> eyre::Result<()> { let contract_addr = alice .as_deployer() - .with_constructor(ctr(alice.address(), START, YEAR)) + .with_constructor(ctr(alice.address(), START, DURATION)) .deploy() .await? .address()?; @@ -43,8 +48,39 @@ async fn constructs(alice: Account) -> eyre::Result<()> { let VestingWallet::endReturn { end } = contract.end().call().await?; assert_eq!(U256::from(START), start); - assert_eq!(U256::from(YEAR), duration); - assert_eq!(end, U256::from(START + YEAR)); + assert_eq!(U256::from(DURATION), duration); + assert_eq!(end, U256::from(START + DURATION)); + + Ok(()) +} + +#[e2e::test] +async fn erc20_vesting(alice: Account) -> eyre::Result<()> { + let start = START - 3600; + let balance = 1000_u64; + let i = 0_u64; + let timestamp = i * DURATION / 60 + start; + let expected_amount = U256::from(balance * (timestamp - start) / DURATION); + + let contract_addr = alice + .as_deployer() + .with_constructor(ctr(alice.address(), START, DURATION)) + .deploy() + .await? + .address()?; + let contract = VestingWallet::new(contract_addr, &alice.wallet); + + let erc20_address = erc20::deploy(&alice.wallet).await?; + let erc20_alice = ERC20Mock::new(erc20_address, &alice.wallet); + let _ = watch!(erc20_alice.mint(contract_addr, U256::from(balance))); + + let VestingWallet::vestedAmount_1Return { vestedAmount } = + contract.vestedAmount_1(erc20_address, timestamp).call().await?; + assert_eq!(expected_amount, vestedAmount); + + let VestingWallet::releasable_1Return { releasable } = + contract.releasable_1(erc20_address).call().await?; + assert_eq!(expected_amount, releasable); Ok(()) } From 48aa9acaf21aa886489120eda0a105751d12c3b0 Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 14 Oct 2024 09:33:49 +0200 Subject: [PATCH 033/138] add receive to abi/mod.rs --- examples/vesting-wallet/tests/abi/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/vesting-wallet/tests/abi/mod.rs b/examples/vesting-wallet/tests/abi/mod.rs index 17bc53e55..2cffa6bfa 100644 --- a/examples/vesting-wallet/tests/abi/mod.rs +++ b/examples/vesting-wallet/tests/abi/mod.rs @@ -5,6 +5,7 @@ sol!( #[sol(rpc)] contract VestingWallet { function owner() public view virtual returns (address owner); + receive() external payable virtual; function start() external view returns (uint256 start); function duration() external view returns (uint256 duration); function end() external view returns (uint256 end); From b675d79e04e89a8f62bb74a02b3fa26b95a5e312 Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 14 Oct 2024 10:11:51 +0200 Subject: [PATCH 034/138] add rejects_zero_address_for_beneficiary test --- .../vesting-wallet/tests/vesting-wallet.rs | 65 ++++++++++++------- 1 file changed, 43 insertions(+), 22 deletions(-) diff --git a/examples/vesting-wallet/tests/vesting-wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs index d5a8cbd25..5bbac2e77 100644 --- a/examples/vesting-wallet/tests/vesting-wallet.rs +++ b/examples/vesting-wallet/tests/vesting-wallet.rs @@ -2,7 +2,7 @@ use alloy::sol; use alloy_primitives::{Address, U256}; -use e2e::{watch, Account, ReceiptExt}; +use e2e::{watch, Account, ReceiptExt, Revert}; use crate::{ abi::VestingWallet, @@ -55,32 +55,53 @@ async fn constructs(alice: Account) -> eyre::Result<()> { } #[e2e::test] -async fn erc20_vesting(alice: Account) -> eyre::Result<()> { - let start = START - 3600; - let balance = 1000_u64; - let i = 0_u64; - let timestamp = i * DURATION / 60 + start; - let expected_amount = U256::from(balance * (timestamp - start) / DURATION); - - let contract_addr = alice +async fn rejects_zero_address_for_beneficiary( + alice: Account, +) -> eyre::Result<()> { + let err = alice .as_deployer() - .with_constructor(ctr(alice.address(), START, DURATION)) + .with_constructor(ctr(Address::ZERO, START, DURATION)) .deploy() - .await? - .address()?; - let contract = VestingWallet::new(contract_addr, &alice.wallet); + .await + .expect_err("should not deploy due to `OwnableInvalidOwner`"); - let erc20_address = erc20::deploy(&alice.wallet).await?; - let erc20_alice = ERC20Mock::new(erc20_address, &alice.wallet); - let _ = watch!(erc20_alice.mint(contract_addr, U256::from(balance))); + assert!(err.reverted_with(VestingWallet::OwnableInvalidOwner { + owner: Address::ZERO + })); - let VestingWallet::vestedAmount_1Return { vestedAmount } = - contract.vestedAmount_1(erc20_address, timestamp).call().await?; - assert_eq!(expected_amount, vestedAmount); + Ok(()) +} - let VestingWallet::releasable_1Return { releasable } = - contract.releasable_1(erc20_address).call().await?; - assert_eq!(expected_amount, releasable); +#[e2e::test] +async fn erc20_vesting(alice: Account) -> eyre::Result<()> { + let balance = 1000_u64; + + for i in 0..1 { + let start = START - 3600; + let timestamp = i * DURATION / 60 + start; + let expected_amount = + U256::from(balance * (timestamp - start) / DURATION); + + let contract_addr = alice + .as_deployer() + .with_constructor(ctr(alice.address(), start, DURATION)) + .deploy() + .await? + .address()?; + let contract = VestingWallet::new(contract_addr, &alice.wallet); + + let erc20_address = erc20::deploy(&alice.wallet).await?; + let erc20_alice = ERC20Mock::new(erc20_address, &alice.wallet); + let _ = watch!(erc20_alice.mint(contract_addr, U256::from(balance))); + + let VestingWallet::vestedAmount_1Return { vestedAmount } = + contract.vestedAmount_1(erc20_address, timestamp).call().await?; + assert_eq!(expected_amount, vestedAmount); + + let VestingWallet::releasable_1Return { releasable } = + contract.releasable_1(erc20_address).call().await?; + assert_eq!(expected_amount, releasable); + } Ok(()) } From 679742b5b79544382cfd9df7c276851f20b54067 Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 14 Oct 2024 11:29:24 +0200 Subject: [PATCH 035/138] Add check vesting schedule test for erc20 --- .../vesting-wallet/tests/vesting-wallet.rs | 54 ++++++++++++------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/examples/vesting-wallet/tests/vesting-wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs index 5bbac2e77..aa188c40a 100644 --- a/examples/vesting-wallet/tests/vesting-wallet.rs +++ b/examples/vesting-wallet/tests/vesting-wallet.rs @@ -72,19 +72,17 @@ async fn rejects_zero_address_for_beneficiary( Ok(()) } -#[e2e::test] -async fn erc20_vesting(alice: Account) -> eyre::Result<()> { - let balance = 1000_u64; - - for i in 0..1 { - let start = START - 3600; - let timestamp = i * DURATION / 60 + start; - let expected_amount = - U256::from(balance * (timestamp - start) / DURATION); - +mod erc20_vesting { + use super::*; + + #[e2e::test] + async fn check_vesting_schedule(alice: Account) -> eyre::Result<()> { + let balance = 1000_u64; + let start = START; + let duration = DURATION; let contract_addr = alice .as_deployer() - .with_constructor(ctr(alice.address(), start, DURATION)) + .with_constructor(ctr(alice.address(), start, duration)) .deploy() .await? .address()?; @@ -94,14 +92,30 @@ async fn erc20_vesting(alice: Account) -> eyre::Result<()> { let erc20_alice = ERC20Mock::new(erc20_address, &alice.wallet); let _ = watch!(erc20_alice.mint(contract_addr, U256::from(balance))); - let VestingWallet::vestedAmount_1Return { vestedAmount } = - contract.vestedAmount_1(erc20_address, timestamp).call().await?; - assert_eq!(expected_amount, vestedAmount); - - let VestingWallet::releasable_1Return { releasable } = - contract.releasable_1(erc20_address).call().await?; - assert_eq!(expected_amount, releasable); + for i in 0..64 { + let timestamp = i * duration / 60 + start; + let expected_amount = U256::from(std::cmp::min( + balance, + balance * (timestamp - start) / duration, + )); + + // TODO: update timestamp + + let VestingWallet::vestedAmount_1Return { vestedAmount } = contract + .vestedAmount_1(erc20_address, timestamp) + .call() + .await?; + assert_eq!( + expected_amount, vestedAmount, + "\n---\ni: {i}\nstart: {start}\ntimestamp: {timestamp}\n---\n" + ); + + // TODO: can't assert until block::timestamp can be manipulated + // let VestingWallet::releasable_1Return { releasable } = + // contract.releasable_1(erc20_address).call().await?; + // assert_eq!(expected_amount, releasable); + } + + Ok(()) } - - Ok(()) } From 445c0c35973a9d995f4301805d20027ccf2a39d3 Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 14 Oct 2024 12:28:48 +0200 Subject: [PATCH 036/138] implement check_vesting_schedule for ether + minor refactor --- examples/vesting-wallet/tests/abi/mod.rs | 2 +- .../vesting-wallet/tests/vesting-wallet.rs | 51 +++++++++++++++++-- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/examples/vesting-wallet/tests/abi/mod.rs b/examples/vesting-wallet/tests/abi/mod.rs index 2cffa6bfa..822cfdc1a 100644 --- a/examples/vesting-wallet/tests/abi/mod.rs +++ b/examples/vesting-wallet/tests/abi/mod.rs @@ -5,7 +5,7 @@ sol!( #[sol(rpc)] contract VestingWallet { function owner() public view virtual returns (address owner); - receive() external payable virtual; + function receiveEther() external payable virtual; function start() external view returns (uint256 start); function duration() external view returns (uint256 duration); function end() external view returns (uint256 end); diff --git a/examples/vesting-wallet/tests/vesting-wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs index aa188c40a..69f14e4c8 100644 --- a/examples/vesting-wallet/tests/vesting-wallet.rs +++ b/examples/vesting-wallet/tests/vesting-wallet.rs @@ -72,6 +72,50 @@ async fn rejects_zero_address_for_beneficiary( Ok(()) } +mod ether_vesting { + use super::*; + + #[e2e::test] + async fn check_vesting_schedule(alice: Account) -> eyre::Result<()> { + let balance = 1000_u64; + let start = START; + let duration = DURATION; + let contract_addr = alice + .as_deployer() + .with_constructor(ctr(alice.address(), start, duration)) + .deploy() + .await? + .address()?; + let contract = VestingWallet::new(contract_addr, &alice.wallet); + + let _ = watch!(contract.receiveEther().value(U256::from(balance)))?; + + for i in 0..64 { + let timestamp = i * duration / 60 + start; + let expected_amount = U256::from(std::cmp::min( + balance, + balance * (timestamp - start) / duration, + )); + + // TODO: update timestamp + + let vested_amount = + contract.vestedAmount_0(timestamp).call().await?.vestedAmount; + assert_eq!( + expected_amount, vested_amount, + "\n---\ni: {i}\nstart: {start}\ntimestamp: {timestamp}\n---\n" + ); + + // TODO: can't assert until block::timestamp can be manipulated + // let VestingWallet::releasable_1Return { releasable } = + // contract.releasable_1(erc20_address).call().await?; + // assert_eq!(expected_amount, releasable); + } + + Ok(()) + } +} + mod erc20_vesting { use super::*; @@ -101,12 +145,13 @@ mod erc20_vesting { // TODO: update timestamp - let VestingWallet::vestedAmount_1Return { vestedAmount } = contract + let vested_amount = contract .vestedAmount_1(erc20_address, timestamp) .call() - .await?; + .await? + .vestedAmount; assert_eq!( - expected_amount, vestedAmount, + expected_amount, vested_amount, "\n---\ni: {i}\nstart: {start}\ntimestamp: {timestamp}\n---\n" ); From e23ddf990da41ed88fa325679e7668dc38bbca5f Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 14 Oct 2024 13:39:49 +0200 Subject: [PATCH 037/138] refactor --- examples/vesting-wallet/tests/vesting-wallet.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/examples/vesting-wallet/tests/vesting-wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs index 69f14e4c8..806d16d14 100644 --- a/examples/vesting-wallet/tests/vesting-wallet.rs +++ b/examples/vesting-wallet/tests/vesting-wallet.rs @@ -42,14 +42,13 @@ async fn constructs(alice: Account) -> eyre::Result<()> { .address()?; let contract = VestingWallet::new(contract_addr, &alice.wallet); - let VestingWallet::startReturn { start } = contract.start().call().await?; - let VestingWallet::durationReturn { duration } = - contract.duration().call().await?; - let VestingWallet::endReturn { end } = contract.end().call().await?; + let start = contract.start().call().await?.start; + let duration = contract.duration().call().await?.duration; + let end = contract.end().call().await?.end; assert_eq!(U256::from(START), start); assert_eq!(U256::from(DURATION), duration); - assert_eq!(end, U256::from(START + DURATION)); + assert_eq!(U256::from(START + DURATION), end); Ok(()) } From fb7d9baed1be504f05c8aaa20e3bee310494d4b9 Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 14 Oct 2024 13:40:42 +0200 Subject: [PATCH 038/138] check owner in 'constructs' test --- examples/vesting-wallet/tests/vesting-wallet.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/vesting-wallet/tests/vesting-wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs index 806d16d14..babc7db0c 100644 --- a/examples/vesting-wallet/tests/vesting-wallet.rs +++ b/examples/vesting-wallet/tests/vesting-wallet.rs @@ -42,10 +42,12 @@ async fn constructs(alice: Account) -> eyre::Result<()> { .address()?; let contract = VestingWallet::new(contract_addr, &alice.wallet); + let owner = contract.owner().call().await?.owner; let start = contract.start().call().await?.start; let duration = contract.duration().call().await?.duration; let end = contract.end().call().await?.end; + assert_eq!(alice.address(), owner); assert_eq!(U256::from(START), start); assert_eq!(U256::from(DURATION), duration); assert_eq!(U256::from(START + DURATION), end); From bb9371188bb937130de9b7b1ea45bc9f220a58e1 Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 14 Oct 2024 13:41:36 +0200 Subject: [PATCH 039/138] revert e2e-tests.sh --- scripts/e2e-tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/e2e-tests.sh b/scripts/e2e-tests.sh index 953b986fb..f7b9cdeb1 100755 --- a/scripts/e2e-tests.sh +++ b/scripts/e2e-tests.sh @@ -11,4 +11,4 @@ cargo +"$NIGHTLY_TOOLCHAIN" build --release --target wasm32-unknown-unknown -Z b export RPC_URL=http://localhost:8547 # We should use stable here once nitro-testnode is updated and the contracts fit # the size limit. Work tracked [here](https://github.com/OpenZeppelin/rust-contracts-stylus/issues/87) -cargo +"$NIGHTLY_TOOLCHAIN" test --features e2e --test "vesting-wallet" +cargo +"$NIGHTLY_TOOLCHAIN" test --features std,e2e --test "*" From e033bd0cca01a2d10a26c5e9603b9901e7c2d400 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 15 Oct 2024 08:16:28 +0200 Subject: [PATCH 040/138] fetch timestamp dynamically --- contracts/src/finance/vesting_wallet.rs | 10 ++++++ examples/vesting-wallet/tests/abi/mod.rs | 2 ++ .../vesting-wallet/tests/vesting-wallet.rs | 34 +++++++++++++------ scripts/e2e-tests.sh | 2 +- 4 files changed, 37 insertions(+), 11 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 3cfab13a0..3f404fa2f 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -111,6 +111,16 @@ impl VestingWallet { self.ownable.owner() } + /// Getter for the start timestamp. + pub fn timestamp(&self) -> u64 { + block::timestamp() + } + + /// Getter for the start timestamp. + pub fn block_number(&self) -> u64 { + block::number() + } + /// Getter for the start timestamp. pub fn start(&self) -> U256 { U256::from(self._start.get()) diff --git a/examples/vesting-wallet/tests/abi/mod.rs b/examples/vesting-wallet/tests/abi/mod.rs index 822cfdc1a..94955ba8f 100644 --- a/examples/vesting-wallet/tests/abi/mod.rs +++ b/examples/vesting-wallet/tests/abi/mod.rs @@ -6,6 +6,8 @@ sol!( contract VestingWallet { function owner() public view virtual returns (address owner); function receiveEther() external payable virtual; + function timestamp() external view returns (uint64 timestamp); + function blockNumber() external view returns (uint64 blockNumber); function start() external view returns (uint256 start); function duration() external view returns (uint256 duration); function end() external view returns (uint256 end); diff --git a/examples/vesting-wallet/tests/vesting-wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs index babc7db0c..993653e89 100644 --- a/examples/vesting-wallet/tests/vesting-wallet.rs +++ b/examples/vesting-wallet/tests/vesting-wallet.rs @@ -1,6 +1,6 @@ #![cfg(feature = "e2e")] -use alloy::sol; +use alloy::{eips::BlockId, providers::Provider, sol}; use alloy_primitives::{Address, U256}; use e2e::{watch, Account, ReceiptExt, Revert}; @@ -15,9 +15,6 @@ mod mock; sol!("src/constructor.sol"); -// Epoch timestamp: 1st January 2025 00::00::00 -const BLOCK_TIMESTAMP: u64 = 1_735_689_600; -const START: u64 = BLOCK_TIMESTAMP + 3600; // 1 hour const DURATION: u64 = 365 * 86400; // 1 year fn ctr( @@ -32,11 +29,27 @@ fn ctr( } } +async fn block_timestamp(account: &Account) -> eyre::Result { + let timestamp = account + .wallet + .get_block( + BlockId::latest(), + alloy::rpc::types::BlockTransactionsKind::Full, + ) + .await? + .expect("latest block should exist") + .header + .timestamp; + + Ok(timestamp) +} + #[e2e::test] async fn constructs(alice: Account) -> eyre::Result<()> { + let start_timestamp = block_timestamp(&alice).await?; let contract_addr = alice .as_deployer() - .with_constructor(ctr(alice.address(), START, DURATION)) + .with_constructor(ctr(alice.address(), start_timestamp, DURATION)) .deploy() .await? .address()?; @@ -48,9 +61,9 @@ async fn constructs(alice: Account) -> eyre::Result<()> { let end = contract.end().call().await?.end; assert_eq!(alice.address(), owner); - assert_eq!(U256::from(START), start); + assert_eq!(U256::from(start_timestamp), start); assert_eq!(U256::from(DURATION), duration); - assert_eq!(U256::from(START + DURATION), end); + assert_eq!(U256::from(start_timestamp + DURATION), end); Ok(()) } @@ -59,9 +72,10 @@ async fn constructs(alice: Account) -> eyre::Result<()> { async fn rejects_zero_address_for_beneficiary( alice: Account, ) -> eyre::Result<()> { + let start = block_timestamp(&alice).await?; let err = alice .as_deployer() - .with_constructor(ctr(Address::ZERO, START, DURATION)) + .with_constructor(ctr(Address::ZERO, start, DURATION)) .deploy() .await .expect_err("should not deploy due to `OwnableInvalidOwner`"); @@ -79,7 +93,7 @@ mod ether_vesting { #[e2e::test] async fn check_vesting_schedule(alice: Account) -> eyre::Result<()> { let balance = 1000_u64; - let start = START; + let start = block_timestamp(&alice).await?; let duration = DURATION; let contract_addr = alice .as_deployer() @@ -123,7 +137,7 @@ mod erc20_vesting { #[e2e::test] async fn check_vesting_schedule(alice: Account) -> eyre::Result<()> { let balance = 1000_u64; - let start = START; + let start = block_timestamp(&alice).await?; let duration = DURATION; let contract_addr = alice .as_deployer() diff --git a/scripts/e2e-tests.sh b/scripts/e2e-tests.sh index f7b9cdeb1..953b986fb 100755 --- a/scripts/e2e-tests.sh +++ b/scripts/e2e-tests.sh @@ -11,4 +11,4 @@ cargo +"$NIGHTLY_TOOLCHAIN" build --release --target wasm32-unknown-unknown -Z b export RPC_URL=http://localhost:8547 # We should use stable here once nitro-testnode is updated and the contracts fit # the size limit. Work tracked [here](https://github.com/OpenZeppelin/rust-contracts-stylus/issues/87) -cargo +"$NIGHTLY_TOOLCHAIN" test --features std,e2e --test "*" +cargo +"$NIGHTLY_TOOLCHAIN" test --features e2e --test "vesting-wallet" From 96d582facd9575620e8dbcae9abac489385d8124 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 15 Oct 2024 09:33:33 +0200 Subject: [PATCH 041/138] remove helper timestamp and blocknumber methods --- contracts/src/finance/vesting_wallet.rs | 10 ---------- examples/vesting-wallet/tests/abi/mod.rs | 2 -- 2 files changed, 12 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 3f404fa2f..3cfab13a0 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -111,16 +111,6 @@ impl VestingWallet { self.ownable.owner() } - /// Getter for the start timestamp. - pub fn timestamp(&self) -> u64 { - block::timestamp() - } - - /// Getter for the start timestamp. - pub fn block_number(&self) -> u64 { - block::number() - } - /// Getter for the start timestamp. pub fn start(&self) -> U256 { U256::from(self._start.get()) diff --git a/examples/vesting-wallet/tests/abi/mod.rs b/examples/vesting-wallet/tests/abi/mod.rs index 94955ba8f..822cfdc1a 100644 --- a/examples/vesting-wallet/tests/abi/mod.rs +++ b/examples/vesting-wallet/tests/abi/mod.rs @@ -6,8 +6,6 @@ sol!( contract VestingWallet { function owner() public view virtual returns (address owner); function receiveEther() external payable virtual; - function timestamp() external view returns (uint64 timestamp); - function blockNumber() external view returns (uint64 blockNumber); function start() external view returns (uint256 start); function duration() external view returns (uint256 duration); function end() external view returns (uint256 end); From 0a0b58ea5fdcf0fbb36ef26a82d8e68630b537cc Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 15 Oct 2024 09:35:14 +0200 Subject: [PATCH 042/138] use const DURATION --- .../vesting-wallet/tests/vesting-wallet.rs | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/examples/vesting-wallet/tests/vesting-wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs index 993653e89..cff53141e 100644 --- a/examples/vesting-wallet/tests/vesting-wallet.rs +++ b/examples/vesting-wallet/tests/vesting-wallet.rs @@ -94,10 +94,9 @@ mod ether_vesting { async fn check_vesting_schedule(alice: Account) -> eyre::Result<()> { let balance = 1000_u64; let start = block_timestamp(&alice).await?; - let duration = DURATION; let contract_addr = alice .as_deployer() - .with_constructor(ctr(alice.address(), start, duration)) + .with_constructor(ctr(alice.address(), start, DURATION)) .deploy() .await? .address()?; @@ -106,14 +105,12 @@ mod ether_vesting { let _ = watch!(contract.receiveEther().value(U256::from(balance)))?; for i in 0..64 { - let timestamp = i * duration / 60 + start; + let timestamp = i * DURATION / 60 + start; let expected_amount = U256::from(std::cmp::min( balance, - balance * (timestamp - start) / duration, + balance * (timestamp - start) / DURATION, )); - // TODO: update timestamp - let vested_amount = contract.vestedAmount_0(timestamp).call().await?.vestedAmount; assert_eq!( @@ -121,10 +118,12 @@ mod ether_vesting { "\n---\ni: {i}\nstart: {start}\ntimestamp: {timestamp}\n---\n" ); - // TODO: can't assert until block::timestamp can be manipulated - // let VestingWallet::releasable_1Return { releasable } = - // contract.releasable_1(erc20_address).call().await?; - // assert_eq!(expected_amount, releasable); + // let releasable = + // contract.releasable_0().call().await?.releasable; + // assert_eq!( + // expected_amount, releasable, + // "\n---\ni: {i}\nstart: {start}\ntimestamp: + // {timestamp}\n---\n" ); } Ok(()) @@ -138,10 +137,9 @@ mod erc20_vesting { async fn check_vesting_schedule(alice: Account) -> eyre::Result<()> { let balance = 1000_u64; let start = block_timestamp(&alice).await?; - let duration = DURATION; let contract_addr = alice .as_deployer() - .with_constructor(ctr(alice.address(), start, duration)) + .with_constructor(ctr(alice.address(), start, DURATION)) .deploy() .await? .address()?; @@ -149,17 +147,15 @@ mod erc20_vesting { let erc20_address = erc20::deploy(&alice.wallet).await?; let erc20_alice = ERC20Mock::new(erc20_address, &alice.wallet); - let _ = watch!(erc20_alice.mint(contract_addr, U256::from(balance))); + let _ = watch!(erc20_alice.mint(contract_addr, U256::from(balance)))?; for i in 0..64 { - let timestamp = i * duration / 60 + start; + let timestamp = i * DURATION / 60 + start; let expected_amount = U256::from(std::cmp::min( balance, - balance * (timestamp - start) / duration, + balance * (timestamp - start) / DURATION, )); - // TODO: update timestamp - let vested_amount = contract .vestedAmount_1(erc20_address, timestamp) .call() From 4de613cdbc01641361ffc6301220bf790021eb0d Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 15 Oct 2024 09:44:27 +0200 Subject: [PATCH 043/138] Add check_releasable_X_percentage for ether --- .../vesting-wallet/tests/vesting-wallet.rs | 123 +++++++++++++++++- 1 file changed, 116 insertions(+), 7 deletions(-) diff --git a/examples/vesting-wallet/tests/vesting-wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs index cff53141e..4f9fb7818 100644 --- a/examples/vesting-wallet/tests/vesting-wallet.rs +++ b/examples/vesting-wallet/tests/vesting-wallet.rs @@ -117,17 +117,126 @@ mod ether_vesting { expected_amount, vested_amount, "\n---\ni: {i}\nstart: {start}\ntimestamp: {timestamp}\n---\n" ); - - // let releasable = - // contract.releasable_0().call().await?.releasable; - // assert_eq!( - // expected_amount, releasable, - // "\n---\ni: {i}\nstart: {start}\ntimestamp: - // {timestamp}\n---\n" ); } Ok(()) } + + #[e2e::test] + async fn check_releasable_0_percent(alice: Account) -> eyre::Result<()> { + let balance = 1000_u64; + let expected_amount = U256::ZERO; + let start = block_timestamp(&alice).await?; + + let contract_addr = alice + .as_deployer() + .with_constructor(ctr(alice.address(), start, DURATION)) + .deploy() + .await? + .address()?; + let contract = VestingWallet::new(contract_addr, &alice.wallet); + + let _ = watch!(contract.receiveEther().value(U256::from(balance)))?; + + let releasable = contract.releasable_0().call().await?.releasable; + assert_eq!(expected_amount, releasable); + + Ok(()) + } + + #[e2e::test] + async fn check_releasable_25_percent(alice: Account) -> eyre::Result<()> { + let balance = 1000_u64; + let expected_amount = U256::from(balance / 4); + let timestamp = block_timestamp(&alice).await?; + let start = timestamp - DURATION / 4; + + let contract_addr = alice + .as_deployer() + .with_constructor(ctr(alice.address(), start, DURATION)) + .deploy() + .await? + .address()?; + let contract = VestingWallet::new(contract_addr, &alice.wallet); + + let _ = watch!(contract.receiveEther().value(U256::from(balance)))?; + + let releasable = contract.releasable_0().call().await?.releasable; + assert_eq!(expected_amount, releasable); + + Ok(()) + } + + #[e2e::test] + async fn check_releasable_50_percent(alice: Account) -> eyre::Result<()> { + let balance = 1000_u64; + let expected_amount = U256::from(balance / 2); + let timestamp = block_timestamp(&alice).await?; + let start = timestamp - DURATION / 2; + + let contract_addr = alice + .as_deployer() + .with_constructor(ctr(alice.address(), start, DURATION)) + .deploy() + .await? + .address()?; + let contract = VestingWallet::new(contract_addr, &alice.wallet); + + let _ = watch!(contract.receiveEther().value(U256::from(balance)))?; + + let releasable = contract.releasable_0().call().await?.releasable; + assert_eq!(expected_amount, releasable); + + Ok(()) + } + + #[e2e::test] + async fn check_releasable_100_percent(alice: Account) -> eyre::Result<()> { + let balance = 1000_u64; + let expected_amount = U256::from(balance); + let timestamp = block_timestamp(&alice).await?; + let start = timestamp - DURATION; + + let contract_addr = alice + .as_deployer() + .with_constructor(ctr(alice.address(), start, DURATION)) + .deploy() + .await? + .address()?; + let contract = VestingWallet::new(contract_addr, &alice.wallet); + + let _ = watch!(contract.receiveEther().value(U256::from(balance)))?; + + let releasable = contract.releasable_0().call().await?.releasable; + assert_eq!(expected_amount, releasable); + + Ok(()) + } + + #[e2e::test] + async fn check_releasable_100_percent_vesting_in_past( + alice: Account, + ) -> eyre::Result<()> { + let balance = 1000_u64; + let expected_amount = U256::from(balance); + let timestamp = block_timestamp(&alice).await?; + let start = timestamp - DURATION * 4 / 3; + + let contract_addr = alice + .as_deployer() + .with_constructor(ctr(alice.address(), start, DURATION)) + .deploy() + .await? + .address()?; + let contract = VestingWallet::new(contract_addr, &alice.wallet); + + let _ = watch!(contract.receiveEther().value(U256::from(balance)))?; + + let releasable = contract.releasable_0().call().await?.releasable; + assert_eq!(expected_amount, releasable); + + Ok(()) + } } mod erc20_vesting { From 4e11eba8cf4b7d19454ba82dfdfde1546ce9b594 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 15 Oct 2024 10:09:34 +0200 Subject: [PATCH 044/138] test 'release' for ether inside existing tests --- .../vesting-wallet/tests/vesting-wallet.rs | 160 ++++++++++++++++-- 1 file changed, 144 insertions(+), 16 deletions(-) diff --git a/examples/vesting-wallet/tests/vesting-wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs index 4f9fb7818..8c0ee1143 100644 --- a/examples/vesting-wallet/tests/vesting-wallet.rs +++ b/examples/vesting-wallet/tests/vesting-wallet.rs @@ -1,14 +1,12 @@ #![cfg(feature = "e2e")] +use abi::VestingWallet; use alloy::{eips::BlockId, providers::Provider, sol}; use alloy_primitives::{Address, U256}; -use e2e::{watch, Account, ReceiptExt, Revert}; +use e2e::{receipt, watch, Account, EventExt, ReceiptExt, Revert}; +use mock::{erc20, erc20::ERC20Mock}; -use crate::{ - abi::VestingWallet, - mock::{erc20, erc20::ERC20Mock}, - VestingWalletExample::constructorCall, -}; +use crate::VestingWalletExample::constructorCall; mod abi; mod mock; @@ -125,7 +123,7 @@ mod ether_vesting { #[e2e::test] async fn check_releasable_0_percent(alice: Account) -> eyre::Result<()> { let balance = 1000_u64; - let expected_amount = U256::ZERO; + let expected_releasable = U256::ZERO; let start = block_timestamp(&alice).await?; let contract_addr = alice @@ -138,8 +136,34 @@ mod ether_vesting { let _ = watch!(contract.receiveEther().value(U256::from(balance)))?; + let old_alice_balance = + alice.wallet.get_balance(alice.address()).await?; + let old_contract_balance = + alice.wallet.get_balance(contract_addr).await?; + + let released = contract.released_0().call().await?.released; let releasable = contract.releasable_0().call().await?.releasable; - assert_eq!(expected_amount, releasable); + assert_eq!(U256::ZERO, released); + assert_eq!(expected_releasable, releasable); + + let receipt = receipt!(contract.release_0())?; + + let alice_balance = alice.wallet.get_balance(alice.address()).await?; + let contract_balance = alice.wallet.get_balance(contract_addr).await?; + let released = contract.released_0().call().await?.released; + let releasable = contract.releasable_0().call().await?.releasable; + assert_eq!(expected_releasable, released); + assert_eq!(U256::ZERO, releasable); + assert_eq!( + old_alice_balance + - U256::from(receipt.gas_used * receipt.effective_gas_price), + alice_balance + ); + assert_eq!(old_contract_balance, contract_balance); + + assert!( + receipt.emits(VestingWallet::EtherReleased { amount: released }) + ); Ok(()) } @@ -147,7 +171,7 @@ mod ether_vesting { #[e2e::test] async fn check_releasable_25_percent(alice: Account) -> eyre::Result<()> { let balance = 1000_u64; - let expected_amount = U256::from(balance / 4); + let expected_releasable = U256::from(balance / 4); let timestamp = block_timestamp(&alice).await?; let start = timestamp - DURATION / 4; @@ -161,8 +185,34 @@ mod ether_vesting { let _ = watch!(contract.receiveEther().value(U256::from(balance)))?; + let old_alice_balance = + alice.wallet.get_balance(alice.address()).await?; + let old_contract_balance = + alice.wallet.get_balance(contract_addr).await?; + + let released = contract.released_0().call().await?.released; let releasable = contract.releasable_0().call().await?.releasable; - assert_eq!(expected_amount, releasable); + assert_eq!(U256::ZERO, released); + assert_eq!(expected_releasable, releasable); + + let receipt = receipt!(contract.release_0())?; + + let alice_balance = alice.wallet.get_balance(alice.address()).await?; + let contract_balance = alice.wallet.get_balance(contract_addr).await?; + let released = contract.released_0().call().await?.released; + let releasable = contract.releasable_0().call().await?.releasable; + assert_eq!(expected_releasable, released); + assert_eq!(U256::ZERO, releasable); + assert_eq!( + old_alice_balance + released + - U256::from(receipt.gas_used * receipt.effective_gas_price), + alice_balance + ); + assert_eq!(old_contract_balance - released, contract_balance); + + assert!( + receipt.emits(VestingWallet::EtherReleased { amount: released }) + ); Ok(()) } @@ -170,7 +220,7 @@ mod ether_vesting { #[e2e::test] async fn check_releasable_50_percent(alice: Account) -> eyre::Result<()> { let balance = 1000_u64; - let expected_amount = U256::from(balance / 2); + let expected_releasable = U256::from(balance / 2); let timestamp = block_timestamp(&alice).await?; let start = timestamp - DURATION / 2; @@ -184,8 +234,34 @@ mod ether_vesting { let _ = watch!(contract.receiveEther().value(U256::from(balance)))?; + let old_alice_balance = + alice.wallet.get_balance(alice.address()).await?; + let old_contract_balance = + alice.wallet.get_balance(contract_addr).await?; + + let released = contract.released_0().call().await?.released; + let releasable = contract.releasable_0().call().await?.releasable; + assert_eq!(U256::ZERO, released); + assert_eq!(expected_releasable, releasable); + + let receipt = receipt!(contract.release_0())?; + + let alice_balance = alice.wallet.get_balance(alice.address()).await?; + let contract_balance = alice.wallet.get_balance(contract_addr).await?; + let released = contract.released_0().call().await?.released; let releasable = contract.releasable_0().call().await?.releasable; - assert_eq!(expected_amount, releasable); + assert_eq!(expected_releasable, released); + assert_eq!(U256::ZERO, releasable); + assert_eq!( + old_alice_balance + released + - U256::from(receipt.gas_used * receipt.effective_gas_price), + alice_balance + ); + assert_eq!(old_contract_balance - released, contract_balance); + + assert!( + receipt.emits(VestingWallet::EtherReleased { amount: released }) + ); Ok(()) } @@ -193,7 +269,7 @@ mod ether_vesting { #[e2e::test] async fn check_releasable_100_percent(alice: Account) -> eyre::Result<()> { let balance = 1000_u64; - let expected_amount = U256::from(balance); + let expected_releasable = U256::from(balance); let timestamp = block_timestamp(&alice).await?; let start = timestamp - DURATION; @@ -207,8 +283,34 @@ mod ether_vesting { let _ = watch!(contract.receiveEther().value(U256::from(balance)))?; + let old_alice_balance = + alice.wallet.get_balance(alice.address()).await?; + let old_contract_balance = + alice.wallet.get_balance(contract_addr).await?; + + let released = contract.released_0().call().await?.released; + let releasable = contract.releasable_0().call().await?.releasable; + assert_eq!(U256::ZERO, released); + assert_eq!(expected_releasable, releasable); + + let receipt = receipt!(contract.release_0())?; + + let alice_balance = alice.wallet.get_balance(alice.address()).await?; + let contract_balance = alice.wallet.get_balance(contract_addr).await?; + let released = contract.released_0().call().await?.released; let releasable = contract.releasable_0().call().await?.releasable; - assert_eq!(expected_amount, releasable); + assert_eq!(expected_releasable, released); + assert_eq!(U256::ZERO, releasable); + assert_eq!( + old_alice_balance + released + - U256::from(receipt.gas_used * receipt.effective_gas_price), + alice_balance + ); + assert_eq!(old_contract_balance - released, contract_balance); + + assert!( + receipt.emits(VestingWallet::EtherReleased { amount: released }) + ); Ok(()) } @@ -218,7 +320,7 @@ mod ether_vesting { alice: Account, ) -> eyre::Result<()> { let balance = 1000_u64; - let expected_amount = U256::from(balance); + let expected_releasable = U256::from(balance); let timestamp = block_timestamp(&alice).await?; let start = timestamp - DURATION * 4 / 3; @@ -232,8 +334,34 @@ mod ether_vesting { let _ = watch!(contract.receiveEther().value(U256::from(balance)))?; + let old_alice_balance = + alice.wallet.get_balance(alice.address()).await?; + let old_contract_balance = + alice.wallet.get_balance(contract_addr).await?; + + let released = contract.released_0().call().await?.released; + let releasable = contract.releasable_0().call().await?.releasable; + assert_eq!(U256::ZERO, released); + assert_eq!(expected_releasable, releasable); + + let receipt = receipt!(contract.release_0())?; + + let alice_balance = alice.wallet.get_balance(alice.address()).await?; + let contract_balance = alice.wallet.get_balance(contract_addr).await?; + let released = contract.released_0().call().await?.released; let releasable = contract.releasable_0().call().await?.releasable; - assert_eq!(expected_amount, releasable); + assert_eq!(expected_releasable, released); + assert_eq!(U256::ZERO, releasable); + assert_eq!( + old_alice_balance + released + - U256::from(receipt.gas_used * receipt.effective_gas_price), + alice_balance + ); + assert_eq!(old_contract_balance - released, contract_balance); + + assert!( + receipt.emits(VestingWallet::EtherReleased { amount: released }) + ); Ok(()) } From 4f3f6b9773950702dc57e4b07776770d2c555808 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 15 Oct 2024 10:10:01 +0200 Subject: [PATCH 045/138] rename check_releasable_0_percent->check_release_0_percent --- examples/vesting-wallet/tests/vesting-wallet.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/vesting-wallet/tests/vesting-wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs index 8c0ee1143..c5496b6bf 100644 --- a/examples/vesting-wallet/tests/vesting-wallet.rs +++ b/examples/vesting-wallet/tests/vesting-wallet.rs @@ -121,7 +121,7 @@ mod ether_vesting { } #[e2e::test] - async fn check_releasable_0_percent(alice: Account) -> eyre::Result<()> { + async fn check_release_0_percent(alice: Account) -> eyre::Result<()> { let balance = 1000_u64; let expected_releasable = U256::ZERO; let start = block_timestamp(&alice).await?; @@ -169,7 +169,7 @@ mod ether_vesting { } #[e2e::test] - async fn check_releasable_25_percent(alice: Account) -> eyre::Result<()> { + async fn check_release_25_percent(alice: Account) -> eyre::Result<()> { let balance = 1000_u64; let expected_releasable = U256::from(balance / 4); let timestamp = block_timestamp(&alice).await?; @@ -218,7 +218,7 @@ mod ether_vesting { } #[e2e::test] - async fn check_releasable_50_percent(alice: Account) -> eyre::Result<()> { + async fn check_release_50_percent(alice: Account) -> eyre::Result<()> { let balance = 1000_u64; let expected_releasable = U256::from(balance / 2); let timestamp = block_timestamp(&alice).await?; @@ -267,7 +267,7 @@ mod ether_vesting { } #[e2e::test] - async fn check_releasable_100_percent(alice: Account) -> eyre::Result<()> { + async fn check_release_100_percent(alice: Account) -> eyre::Result<()> { let balance = 1000_u64; let expected_releasable = U256::from(balance); let timestamp = block_timestamp(&alice).await?; @@ -316,7 +316,7 @@ mod ether_vesting { } #[e2e::test] - async fn check_releasable_100_percent_vesting_in_past( + async fn check_release_100_percent_vesting_in_past( alice: Account, ) -> eyre::Result<()> { let balance = 1000_u64; From 458b65b70634be7315412ee9a57e86d270e24f0e Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 15 Oct 2024 10:14:35 +0200 Subject: [PATCH 046/138] extract balance --- .../vesting-wallet/tests/vesting-wallet.rs | 38 ++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/examples/vesting-wallet/tests/vesting-wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs index c5496b6bf..1f5644ebb 100644 --- a/examples/vesting-wallet/tests/vesting-wallet.rs +++ b/examples/vesting-wallet/tests/vesting-wallet.rs @@ -13,6 +13,7 @@ mod mock; sol!("src/constructor.sol"); +const BALANCE: u256 = 1000; const DURATION: u64 = 365 * 86400; // 1 year fn ctr( @@ -90,7 +91,6 @@ mod ether_vesting { #[e2e::test] async fn check_vesting_schedule(alice: Account) -> eyre::Result<()> { - let balance = 1000_u64; let start = block_timestamp(&alice).await?; let contract_addr = alice .as_deployer() @@ -100,13 +100,13 @@ mod ether_vesting { .address()?; let contract = VestingWallet::new(contract_addr, &alice.wallet); - let _ = watch!(contract.receiveEther().value(U256::from(balance)))?; + let _ = watch!(contract.receiveEther().value(U256::from(BALANCE)))?; for i in 0..64 { let timestamp = i * DURATION / 60 + start; let expected_amount = U256::from(std::cmp::min( - balance, - balance * (timestamp - start) / DURATION, + BALANCE, + BALANCE * (timestamp - start) / DURATION, )); let vested_amount = @@ -122,7 +122,6 @@ mod ether_vesting { #[e2e::test] async fn check_release_0_percent(alice: Account) -> eyre::Result<()> { - let balance = 1000_u64; let expected_releasable = U256::ZERO; let start = block_timestamp(&alice).await?; @@ -134,7 +133,7 @@ mod ether_vesting { .address()?; let contract = VestingWallet::new(contract_addr, &alice.wallet); - let _ = watch!(contract.receiveEther().value(U256::from(balance)))?; + let _ = watch!(contract.receiveEther().value(U256::from(BALANCE)))?; let old_alice_balance = alice.wallet.get_balance(alice.address()).await?; @@ -170,8 +169,7 @@ mod ether_vesting { #[e2e::test] async fn check_release_25_percent(alice: Account) -> eyre::Result<()> { - let balance = 1000_u64; - let expected_releasable = U256::from(balance / 4); + let expected_releasable = U256::from(BALANCE / 4); let timestamp = block_timestamp(&alice).await?; let start = timestamp - DURATION / 4; @@ -183,7 +181,7 @@ mod ether_vesting { .address()?; let contract = VestingWallet::new(contract_addr, &alice.wallet); - let _ = watch!(contract.receiveEther().value(U256::from(balance)))?; + let _ = watch!(contract.receiveEther().value(U256::from(BALANCE)))?; let old_alice_balance = alice.wallet.get_balance(alice.address()).await?; @@ -219,8 +217,7 @@ mod ether_vesting { #[e2e::test] async fn check_release_50_percent(alice: Account) -> eyre::Result<()> { - let balance = 1000_u64; - let expected_releasable = U256::from(balance / 2); + let expected_releasable = U256::from(BALANCE / 2); let timestamp = block_timestamp(&alice).await?; let start = timestamp - DURATION / 2; @@ -232,7 +229,7 @@ mod ether_vesting { .address()?; let contract = VestingWallet::new(contract_addr, &alice.wallet); - let _ = watch!(contract.receiveEther().value(U256::from(balance)))?; + let _ = watch!(contract.receiveEther().value(U256::from(BALANCE)))?; let old_alice_balance = alice.wallet.get_balance(alice.address()).await?; @@ -268,8 +265,7 @@ mod ether_vesting { #[e2e::test] async fn check_release_100_percent(alice: Account) -> eyre::Result<()> { - let balance = 1000_u64; - let expected_releasable = U256::from(balance); + let expected_releasable = U256::from(BALANCE); let timestamp = block_timestamp(&alice).await?; let start = timestamp - DURATION; @@ -281,7 +277,7 @@ mod ether_vesting { .address()?; let contract = VestingWallet::new(contract_addr, &alice.wallet); - let _ = watch!(contract.receiveEther().value(U256::from(balance)))?; + let _ = watch!(contract.receiveEther().value(U256::from(BALANCE)))?; let old_alice_balance = alice.wallet.get_balance(alice.address()).await?; @@ -319,8 +315,7 @@ mod ether_vesting { async fn check_release_100_percent_vesting_in_past( alice: Account, ) -> eyre::Result<()> { - let balance = 1000_u64; - let expected_releasable = U256::from(balance); + let expected_releasable = U256::from(BALANCE); let timestamp = block_timestamp(&alice).await?; let start = timestamp - DURATION * 4 / 3; @@ -332,7 +327,7 @@ mod ether_vesting { .address()?; let contract = VestingWallet::new(contract_addr, &alice.wallet); - let _ = watch!(contract.receiveEther().value(U256::from(balance)))?; + let _ = watch!(contract.receiveEther().value(U256::from(BALANCE)))?; let old_alice_balance = alice.wallet.get_balance(alice.address()).await?; @@ -372,7 +367,6 @@ mod erc20_vesting { #[e2e::test] async fn check_vesting_schedule(alice: Account) -> eyre::Result<()> { - let balance = 1000_u64; let start = block_timestamp(&alice).await?; let contract_addr = alice .as_deployer() @@ -384,13 +378,13 @@ mod erc20_vesting { let erc20_address = erc20::deploy(&alice.wallet).await?; let erc20_alice = ERC20Mock::new(erc20_address, &alice.wallet); - let _ = watch!(erc20_alice.mint(contract_addr, U256::from(balance)))?; + let _ = watch!(erc20_alice.mint(contract_addr, U256::from(BALANCE)))?; for i in 0..64 { let timestamp = i * DURATION / 60 + start; let expected_amount = U256::from(std::cmp::min( - balance, - balance * (timestamp - start) / DURATION, + BALANCE, + BALANCE * (timestamp - start) / DURATION, )); let vested_amount = contract From 35e1de49cc7a7df5a55483a30745b879049e51db Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 15 Oct 2024 10:20:28 +0200 Subject: [PATCH 047/138] Add assert_in_delta function --- .../vesting-wallet/tests/vesting-wallet.rs | 73 ++++++++++--------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/examples/vesting-wallet/tests/vesting-wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs index 1f5644ebb..69e828439 100644 --- a/examples/vesting-wallet/tests/vesting-wallet.rs +++ b/examples/vesting-wallet/tests/vesting-wallet.rs @@ -13,7 +13,7 @@ mod mock; sol!("src/constructor.sol"); -const BALANCE: u256 = 1000; +const BALANCE: u64 = 1000; const DURATION: u64 = 365 * 86400; // 1 year fn ctr( @@ -43,6 +43,12 @@ async fn block_timestamp(account: &Account) -> eyre::Result { Ok(timestamp) } +fn assert_in_delta(expected: U256, actual: U256) { + let diff = expected.abs_diff(actual); + let delta = U256::from(1); + assert!(diff <= delta,"Your result of {actual} should be within {delta} of the expected result {expected}"); +} + #[e2e::test] async fn constructs(alice: Account) -> eyre::Result<()> { let start_timestamp = block_timestamp(&alice).await?; @@ -143,7 +149,7 @@ mod ether_vesting { let released = contract.released_0().call().await?.released; let releasable = contract.releasable_0().call().await?.releasable; assert_eq!(U256::ZERO, released); - assert_eq!(expected_releasable, releasable); + assert_in_delta(expected_releasable, releasable); let receipt = receipt!(contract.release_0())?; @@ -151,14 +157,14 @@ mod ether_vesting { let contract_balance = alice.wallet.get_balance(contract_addr).await?; let released = contract.released_0().call().await?.released; let releasable = contract.releasable_0().call().await?.releasable; - assert_eq!(expected_releasable, released); - assert_eq!(U256::ZERO, releasable); - assert_eq!( + assert_in_delta(expected_releasable, released); + assert_in_delta(U256::ZERO, releasable); + assert_in_delta( old_alice_balance - U256::from(receipt.gas_used * receipt.effective_gas_price), - alice_balance + alice_balance, ); - assert_eq!(old_contract_balance, contract_balance); + assert_in_delta(old_contract_balance, contract_balance); assert!( receipt.emits(VestingWallet::EtherReleased { amount: released }) @@ -191,7 +197,7 @@ mod ether_vesting { let released = contract.released_0().call().await?.released; let releasable = contract.releasable_0().call().await?.releasable; assert_eq!(U256::ZERO, released); - assert_eq!(expected_releasable, releasable); + assert_in_delta(expected_releasable, releasable); let receipt = receipt!(contract.release_0())?; @@ -199,14 +205,14 @@ mod ether_vesting { let contract_balance = alice.wallet.get_balance(contract_addr).await?; let released = contract.released_0().call().await?.released; let releasable = contract.releasable_0().call().await?.releasable; - assert_eq!(expected_releasable, released); - assert_eq!(U256::ZERO, releasable); - assert_eq!( + assert_in_delta(expected_releasable, released); + assert_in_delta(U256::ZERO, releasable); + assert_in_delta( old_alice_balance + released - U256::from(receipt.gas_used * receipt.effective_gas_price), - alice_balance + alice_balance, ); - assert_eq!(old_contract_balance - released, contract_balance); + assert_in_delta(old_contract_balance - released, contract_balance); assert!( receipt.emits(VestingWallet::EtherReleased { amount: released }) @@ -239,7 +245,7 @@ mod ether_vesting { let released = contract.released_0().call().await?.released; let releasable = contract.releasable_0().call().await?.releasable; assert_eq!(U256::ZERO, released); - assert_eq!(expected_releasable, releasable); + assert_in_delta(expected_releasable, releasable); let receipt = receipt!(contract.release_0())?; @@ -247,14 +253,14 @@ mod ether_vesting { let contract_balance = alice.wallet.get_balance(contract_addr).await?; let released = contract.released_0().call().await?.released; let releasable = contract.releasable_0().call().await?.releasable; - assert_eq!(expected_releasable, released); - assert_eq!(U256::ZERO, releasable); - assert_eq!( + assert_in_delta(expected_releasable, released); + assert_in_delta(U256::ZERO, releasable); + assert_in_delta( old_alice_balance + released - U256::from(receipt.gas_used * receipt.effective_gas_price), - alice_balance + alice_balance, ); - assert_eq!(old_contract_balance - released, contract_balance); + assert_in_delta(old_contract_balance - released, contract_balance); assert!( receipt.emits(VestingWallet::EtherReleased { amount: released }) @@ -287,7 +293,7 @@ mod ether_vesting { let released = contract.released_0().call().await?.released; let releasable = contract.releasable_0().call().await?.releasable; assert_eq!(U256::ZERO, released); - assert_eq!(expected_releasable, releasable); + assert_in_delta(expected_releasable, releasable); let receipt = receipt!(contract.release_0())?; @@ -295,14 +301,14 @@ mod ether_vesting { let contract_balance = alice.wallet.get_balance(contract_addr).await?; let released = contract.released_0().call().await?.released; let releasable = contract.releasable_0().call().await?.releasable; - assert_eq!(expected_releasable, released); - assert_eq!(U256::ZERO, releasable); - assert_eq!( + assert_in_delta(expected_releasable, released); + assert_in_delta(U256::ZERO, releasable); + assert_in_delta( old_alice_balance + released - U256::from(receipt.gas_used * receipt.effective_gas_price), - alice_balance + alice_balance, ); - assert_eq!(old_contract_balance - released, contract_balance); + assert_in_delta(old_contract_balance - released, contract_balance); assert!( receipt.emits(VestingWallet::EtherReleased { amount: released }) @@ -337,7 +343,7 @@ mod ether_vesting { let released = contract.released_0().call().await?.released; let releasable = contract.releasable_0().call().await?.releasable; assert_eq!(U256::ZERO, released); - assert_eq!(expected_releasable, releasable); + assert_in_delta(expected_releasable, releasable); let receipt = receipt!(contract.release_0())?; @@ -345,14 +351,14 @@ mod ether_vesting { let contract_balance = alice.wallet.get_balance(contract_addr).await?; let released = contract.released_0().call().await?.released; let releasable = contract.releasable_0().call().await?.releasable; - assert_eq!(expected_releasable, released); - assert_eq!(U256::ZERO, releasable); - assert_eq!( + assert_in_delta(expected_releasable, released); + assert_in_delta(U256::ZERO, releasable); + assert_in_delta( old_alice_balance + released - U256::from(receipt.gas_used * receipt.effective_gas_price), - alice_balance + alice_balance, ); - assert_eq!(old_contract_balance - released, contract_balance); + assert_in_delta(old_contract_balance - released, contract_balance); assert!( receipt.emits(VestingWallet::EtherReleased { amount: released }) @@ -396,11 +402,6 @@ mod erc20_vesting { expected_amount, vested_amount, "\n---\ni: {i}\nstart: {start}\ntimestamp: {timestamp}\n---\n" ); - - // TODO: can't assert until block::timestamp can be manipulated - // let VestingWallet::releasable_1Return { releasable } = - // contract.releasable_1(erc20_address).call().await?; - // assert_eq!(expected_amount, releasable); } Ok(()) From c96bc23b3592183d882c031558fd2bc6e4ed8c20 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 15 Oct 2024 13:54:36 +0200 Subject: [PATCH 048/138] Add release tests for erc20 --- examples/vesting-wallet/tests/mock/erc20.rs | 25 +- .../vesting-wallet/tests/vesting-wallet.rs | 265 +++++++++++++++++- 2 files changed, 265 insertions(+), 25 deletions(-) diff --git a/examples/vesting-wallet/tests/mock/erc20.rs b/examples/vesting-wallet/tests/mock/erc20.rs index 68514daea..df5e55177 100644 --- a/examples/vesting-wallet/tests/mock/erc20.rs +++ b/examples/vesting-wallet/tests/mock/erc20.rs @@ -6,38 +6,17 @@ use e2e::Wallet; sol! { #[allow(missing_docs)] // Built with Remix IDE; solc v0.8.21+commit.d9974bed - #[sol(rpc, bytecode="608060405234801562000010575f80fd5b506040518060400160405280600981526020017f45524332304d6f636b00000000000000000000000000000000000000000000008152506040518060400160405280600381526020017f4d544b000000000000000000000000000000000000000000000000000000000081525081600390816200008e91906200030d565b508060049081620000a091906200030d565b505050620003f1565b5f81519050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b5f60028204905060018216806200012557607f821691505b6020821081036200013b576200013a620000e0565b5b50919050565b5f819050815f5260205f209050919050565b5f6020601f8301049050919050565b5f82821b905092915050565b5f600883026200019f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8262000162565b620001ab868362000162565b95508019841693508086168417925050509392505050565b5f819050919050565b5f819050919050565b5f620001f5620001ef620001e984620001c3565b620001cc565b620001c3565b9050919050565b5f819050919050565b6200021083620001d5565b620002286200021f82620001fc565b8484546200016e565b825550505050565b5f90565b6200023e62000230565b6200024b81848462000205565b505050565b5b818110156200027257620002665f8262000234565b60018101905062000251565b5050565b601f821115620002c1576200028b8162000141565b620002968462000153565b81016020851015620002a6578190505b620002be620002b58562000153565b83018262000250565b50505b505050565b5f82821c905092915050565b5f620002e35f1984600802620002c6565b1980831691505092915050565b5f620002fd8383620002d2565b9150826002028217905092915050565b6200031882620000a9565b67ffffffffffffffff811115620003345762000333620000b3565b5b6200034082546200010d565b6200034d82828562000276565b5f60209050601f83116001811462000383575f84156200036e578287015190505b6200037a8582620002f0565b865550620003e9565b601f198416620003938662000141565b5f5b82811015620003bc5784890151825560018201915060208501945060208101905062000395565b86831015620003dc5784890151620003d8601f891682620002d2565b8355505b6001600288020188555050505b505050505050565b610f4580620003ff5f395ff3fe608060405234801561000f575f80fd5b50600436106100a7575f3560e01c806340c10f191161006f57806340c10f191461016557806370a08231146101815780638483acfe146101b157806395d89b41146101cd578063a9059cbb146101eb578063dd62ed3e1461021b576100a7565b806306fdde03146100ab578063095ea7b3146100c957806318160ddd146100f957806323b872dd14610117578063313ce56714610147575b5f80fd5b6100b361024b565b6040516100c09190610bbe565b60405180910390f35b6100e360048036038101906100de9190610c6f565b6102db565b6040516100f09190610cc7565b60405180910390f35b6101016102ee565b60405161010e9190610cef565b60405180910390f35b610131600480360381019061012c9190610d08565b6102f7565b60405161013e9190610cc7565b60405180910390f35b61014f61030c565b60405161015c9190610d73565b60405180910390f35b61017f600480360381019061017a9190610c6f565b610314565b005b61019b60048036038101906101969190610d8c565b610322565b6040516101a89190610cef565b60405180910390f35b6101cb60048036038101906101c69190610d08565b610333565b005b6101d5610343565b6040516101e29190610bbe565b60405180910390f35b61020560048036038101906102009190610c6f565b6103d3565b6040516102129190610cc7565b60405180910390f35b61023560048036038101906102309190610db7565b6103e6565b6040516102429190610cef565b60405180910390f35b60606003805461025a90610e22565b80601f016020809104026020016040519081016040528092919081815260200182805461028690610e22565b80156102d15780601f106102a8576101008083540402835291602001916102d1565b820191905f5260205f20905b8154815290600101906020018083116102b457829003601f168201915b5050505050905090565b5f6102e683836103f9565b905092915050565b5f600254905090565b5f61030384848461041b565b90509392505050565b5f6012905090565b61031e8282610449565b5050565b5f61032c826104c8565b9050919050565b61033e83838361050d565b505050565b60606004805461035290610e22565b80601f016020809104026020016040519081016040528092919081815260200182805461037e90610e22565b80156103c95780601f106103a0576101008083540402835291602001916103c9565b820191905f5260205f20905b8154815290600101906020018083116103ac57829003601f168201915b5050505050905090565b5f6103de838361051f565b905092915050565b5f6103f18383610541565b905092915050565b5f806104036105c3565b905061041081858561050d565b600191505092915050565b5f806104256105c3565b90506104328582856105ca565b61043d85858561065c565b60019150509392505050565b5f73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036104b9575f6040517fec442f050000000000000000000000000000000000000000000000000000000081526004016104b09190610e61565b60405180910390fd5b6104c45f838361074c565b5050565b5f805f8373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f20549050919050565b61051a8383836001610965565b505050565b5f806105296105c3565b905061053681858561065c565b600191505092915050565b5f60015f8473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f2054905092915050565b5f33905090565b5f6105d584846103e6565b90507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81146106565781811015610647578281836040517ffb8f41b200000000000000000000000000000000000000000000000000000000815260040161063e93929190610e7a565b60405180910390fd5b61065584848484035f610965565b5b50505050565b5f73ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16036106cc575f6040517f96c6fd1e0000000000000000000000000000000000000000000000000000000081526004016106c39190610e61565b60405180910390fd5b5f73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff160361073c575f6040517fec442f050000000000000000000000000000000000000000000000000000000081526004016107339190610e61565b60405180910390fd5b61074783838361074c565b505050565b5f73ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff160361079c578060025f8282546107909190610edc565b9250508190555061086a565b5f805f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f2054905081811015610825578381836040517fe450d38c00000000000000000000000000000000000000000000000000000000815260040161081c93929190610e7a565b60405180910390fd5b8181035f808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f2081905550505b5f73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036108b1578060025f82825403925050819055506108fb565b805f808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f82825401925050819055505b8173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040516109589190610cef565b60405180910390a3505050565b5f73ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff16036109d5575f6040517fe602df050000000000000000000000000000000000000000000000000000000081526004016109cc9190610e61565b60405180910390fd5b5f73ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1603610a45575f6040517f94280d62000000000000000000000000000000000000000000000000000000008152600401610a3c9190610e61565b60405180910390fd5b8160015f8673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f20819055508015610b2e578273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92584604051610b259190610cef565b60405180910390a35b50505050565b5f81519050919050565b5f82825260208201905092915050565b5f5b83811015610b6b578082015181840152602081019050610b50565b5f8484015250505050565b5f601f19601f8301169050919050565b5f610b9082610b34565b610b9a8185610b3e565b9350610baa818560208601610b4e565b610bb381610b76565b840191505092915050565b5f6020820190508181035f830152610bd68184610b86565b905092915050565b5f80fd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f610c0b82610be2565b9050919050565b610c1b81610c01565b8114610c25575f80fd5b50565b5f81359050610c3681610c12565b92915050565b5f819050919050565b610c4e81610c3c565b8114610c58575f80fd5b50565b5f81359050610c6981610c45565b92915050565b5f8060408385031215610c8557610c84610bde565b5b5f610c9285828601610c28565b9250506020610ca385828601610c5b565b9150509250929050565b5f8115159050919050565b610cc181610cad565b82525050565b5f602082019050610cda5f830184610cb8565b92915050565b610ce981610c3c565b82525050565b5f602082019050610d025f830184610ce0565b92915050565b5f805f60608486031215610d1f57610d1e610bde565b5b5f610d2c86828701610c28565b9350506020610d3d86828701610c28565b9250506040610d4e86828701610c5b565b9150509250925092565b5f60ff82169050919050565b610d6d81610d58565b82525050565b5f602082019050610d865f830184610d64565b92915050565b5f60208284031215610da157610da0610bde565b5b5f610dae84828501610c28565b91505092915050565b5f8060408385031215610dcd57610dcc610bde565b5b5f610dda85828601610c28565b9250506020610deb85828601610c28565b9150509250929050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b5f6002820490506001821680610e3957607f821691505b602082108103610e4c57610e4b610df5565b5b50919050565b610e5b81610c01565b82525050565b5f602082019050610e745f830184610e52565b92915050565b5f606082019050610e8d5f830186610e52565b610e9a6020830185610ce0565b610ea76040830184610ce0565b949350505050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f610ee682610c3c565b9150610ef183610c3c565b9250828201905080821115610f0957610f08610eaf565b5b9291505056fea2646970667358221220383e898342e74543d1bfb6186eff00b4ae7a39d4ecde6190742c5e9f2a7a2e9364736f6c63430008150033")] + #[sol(rpc, bytecode="608060405234801562000010575f80fd5b506040518060400160405280600981526020017f45524332304d6f636b00000000000000000000000000000000000000000000008152506040518060400160405280600381526020017f4d544b000000000000000000000000000000000000000000000000000000000081525081600390816200008e91906200030d565b508060049081620000a091906200030d565b505050620003f1565b5f81519050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b5f60028204905060018216806200012557607f821691505b6020821081036200013b576200013a620000e0565b5b50919050565b5f819050815f5260205f209050919050565b5f6020601f8301049050919050565b5f82821b905092915050565b5f600883026200019f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8262000162565b620001ab868362000162565b95508019841693508086168417925050509392505050565b5f819050919050565b5f819050919050565b5f620001f5620001ef620001e984620001c3565b620001cc565b620001c3565b9050919050565b5f819050919050565b6200021083620001d5565b620002286200021f82620001fc565b8484546200016e565b825550505050565b5f90565b6200023e62000230565b6200024b81848462000205565b505050565b5b818110156200027257620002665f8262000234565b60018101905062000251565b5050565b601f821115620002c1576200028b8162000141565b620002968462000153565b81016020851015620002a6578190505b620002be620002b58562000153565b83018262000250565b50505b505050565b5f82821c905092915050565b5f620002e35f1984600802620002c6565b1980831691505092915050565b5f620002fd8383620002d2565b9150826002028217905092915050565b6200031882620000a9565b67ffffffffffffffff811115620003345762000333620000b3565b5b6200034082546200010d565b6200034d82828562000276565b5f60209050601f83116001811462000383575f84156200036e578287015190505b6200037a8582620002f0565b865550620003e9565b601f198416620003938662000141565b5f5b82811015620003bc5784890151825560018201915060208501945060208101905062000395565b86831015620003dc5784890151620003d8601f891682620002d2565b8355505b6001600288020188555050505b505050505050565b610ec080620003ff5f395ff3fe608060405234801561000f575f80fd5b506004361061009c575f3560e01c806340c10f191161006457806340c10f191461015a57806370a082311461017657806395d89b41146101a6578063a9059cbb146101c4578063dd62ed3e146101f45761009c565b806306fdde03146100a0578063095ea7b3146100be57806318160ddd146100ee57806323b872dd1461010c578063313ce5671461013c575b5f80fd5b6100a8610224565b6040516100b59190610b39565b60405180910390f35b6100d860048036038101906100d39190610bea565b6102b4565b6040516100e59190610c42565b60405180910390f35b6100f66102d6565b6040516101039190610c6a565b60405180910390f35b61012660048036038101906101219190610c83565b6102df565b6040516101339190610c42565b60405180910390f35b61014461030d565b6040516101519190610cee565b60405180910390f35b610174600480360381019061016f9190610bea565b610315565b005b610190600480360381019061018b9190610d07565b610323565b60405161019d9190610c6a565b60405180910390f35b6101ae610334565b6040516101bb9190610b39565b60405180910390f35b6101de60048036038101906101d99190610bea565b6103c4565b6040516101eb9190610c42565b60405180910390f35b61020e60048036038101906102099190610d32565b6103e6565b60405161021b9190610c6a565b60405180910390f35b60606003805461023390610d9d565b80601f016020809104026020016040519081016040528092919081815260200182805461025f90610d9d565b80156102aa5780601f10610281576101008083540402835291602001916102aa565b820191905f5260205f20905b81548152906001019060200180831161028d57829003601f168201915b5050505050905090565b5f806102be610468565b90506102cb81858561046f565b600191505092915050565b5f600254905090565b5f806102e9610468565b90506102f6858285610481565b610301858585610513565b60019150509392505050565b5f6012905090565b61031f8282610603565b5050565b5f61032d82610682565b9050919050565b60606004805461034390610d9d565b80601f016020809104026020016040519081016040528092919081815260200182805461036f90610d9d565b80156103ba5780601f10610391576101008083540402835291602001916103ba565b820191905f5260205f20905b81548152906001019060200180831161039d57829003601f168201915b5050505050905090565b5f806103ce610468565b90506103db818585610513565b600191505092915050565b5f60015f8473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f2054905092915050565b5f33905090565b61047c83838360016106c7565b505050565b5f61048c84846103e6565b90507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff811461050d57818110156104fe578281836040517ffb8f41b20000000000000000000000000000000000000000000000000000000081526004016104f593929190610ddc565b60405180910390fd5b61050c84848484035f6106c7565b5b50505050565b5f73ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1603610583575f6040517f96c6fd1e00000000000000000000000000000000000000000000000000000000815260040161057a9190610e11565b60405180910390fd5b5f73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036105f3575f6040517fec442f050000000000000000000000000000000000000000000000000000000081526004016105ea9190610e11565b60405180910390fd5b6105fe838383610896565b505050565b5f73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603610673575f6040517fec442f0500000000000000000000000000000000000000000000000000000000815260040161066a9190610e11565b60405180910390fd5b61067e5f8383610896565b5050565b5f805f8373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f20549050919050565b5f73ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff1603610737575f6040517fe602df0500000000000000000000000000000000000000000000000000000000815260040161072e9190610e11565b60405180910390fd5b5f73ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16036107a7575f6040517f94280d6200000000000000000000000000000000000000000000000000000000815260040161079e9190610e11565b60405180910390fd5b8160015f8673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f20819055508015610890578273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040516108879190610c6a565b60405180910390a35b50505050565b5f73ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16036108e6578060025f8282546108da9190610e57565b925050819055506109b4565b5f805f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205490508181101561096f578381836040517fe450d38c00000000000000000000000000000000000000000000000000000000815260040161096693929190610ddc565b60405180910390fd5b8181035f808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f2081905550505b5f73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036109fb578060025f8282540392505081905550610a45565b805f808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f82825401925050819055505b8173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef83604051610aa29190610c6a565b60405180910390a3505050565b5f81519050919050565b5f82825260208201905092915050565b5f5b83811015610ae6578082015181840152602081019050610acb565b5f8484015250505050565b5f601f19601f8301169050919050565b5f610b0b82610aaf565b610b158185610ab9565b9350610b25818560208601610ac9565b610b2e81610af1565b840191505092915050565b5f6020820190508181035f830152610b518184610b01565b905092915050565b5f80fd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f610b8682610b5d565b9050919050565b610b9681610b7c565b8114610ba0575f80fd5b50565b5f81359050610bb181610b8d565b92915050565b5f819050919050565b610bc981610bb7565b8114610bd3575f80fd5b50565b5f81359050610be481610bc0565b92915050565b5f8060408385031215610c0057610bff610b59565b5b5f610c0d85828601610ba3565b9250506020610c1e85828601610bd6565b9150509250929050565b5f8115159050919050565b610c3c81610c28565b82525050565b5f602082019050610c555f830184610c33565b92915050565b610c6481610bb7565b82525050565b5f602082019050610c7d5f830184610c5b565b92915050565b5f805f60608486031215610c9a57610c99610b59565b5b5f610ca786828701610ba3565b9350506020610cb886828701610ba3565b9250506040610cc986828701610bd6565b9150509250925092565b5f60ff82169050919050565b610ce881610cd3565b82525050565b5f602082019050610d015f830184610cdf565b92915050565b5f60208284031215610d1c57610d1b610b59565b5b5f610d2984828501610ba3565b91505092915050565b5f8060408385031215610d4857610d47610b59565b5b5f610d5585828601610ba3565b9250506020610d6685828601610ba3565b9150509250929050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b5f6002820490506001821680610db457607f821691505b602082108103610dc757610dc6610d70565b5b50919050565b610dd681610b7c565b82525050565b5f606082019050610def5f830186610dcd565b610dfc6020830185610c5b565b610e096040830184610c5b565b949350505050565b5f602082019050610e245f830184610dcd565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f610e6182610bb7565b9150610e6c83610bb7565b9250828201905080821115610e8457610e83610e2a565b5b9291505056fea2646970667358221220aae0e1f0f9317957e6b898e81a54f655e91a33a9848dbdd292ef970a0904968264736f6c63430008150033")] // SPDX-License-Identifier: MIT contract ERC20Mock is ERC20 { constructor() ERC20("ERC20Mock", "MTK") {} - - function approve(address spender, uint256 value) public override returns (bool) { - return super.approve(spender, value); - } - - function regular_approve(address owner, address spender, uint256 amount) public { - super._approve(owner, spender, amount); - } - - function balanceOf(address account) public override view returns (uint256) { + function balanceOf(address account) public override view returns (uint256 balance) { return super.balanceOf(account); } function mint(address account, uint256 value) public { super._mint(account, value); } - - function transfer(address to, uint256 amount) public override returns (bool) { - return super.transfer(to, amount); - } - - function transferFrom(address from, address to, uint256 value) public override returns (bool) { - return super.transferFrom(from, to, value); - } - - function allowance(address owner, address spender) public view override returns (uint256) { - return super.allowance(owner, spender); - } } } diff --git a/examples/vesting-wallet/tests/vesting-wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs index 69e828439..bfdbca314 100644 --- a/examples/vesting-wallet/tests/vesting-wallet.rs +++ b/examples/vesting-wallet/tests/vesting-wallet.rs @@ -383,8 +383,8 @@ mod erc20_vesting { let contract = VestingWallet::new(contract_addr, &alice.wallet); let erc20_address = erc20::deploy(&alice.wallet).await?; - let erc20_alice = ERC20Mock::new(erc20_address, &alice.wallet); - let _ = watch!(erc20_alice.mint(contract_addr, U256::from(BALANCE)))?; + let erc20 = ERC20Mock::new(erc20_address, &alice.wallet); + let _ = watch!(erc20.mint(contract_addr, U256::from(BALANCE)))?; for i in 0..64 { let timestamp = i * DURATION / 60 + start; @@ -406,4 +406,265 @@ mod erc20_vesting { Ok(()) } + + #[e2e::test] + async fn check_release_0_percent(alice: Account) -> eyre::Result<()> { + let expected_releasable = U256::ZERO; + let start = block_timestamp(&alice).await?; + + let contract_addr = alice + .as_deployer() + .with_constructor(ctr(alice.address(), start, DURATION)) + .deploy() + .await? + .address()?; + let contract = VestingWallet::new(contract_addr, &alice.wallet); + + let erc20_address = erc20::deploy(&alice.wallet).await?; + let erc20 = ERC20Mock::new(erc20_address, &alice.wallet); + let _ = watch!(erc20.mint(contract_addr, U256::from(BALANCE)))?; + + let old_alice_balance = + erc20.balanceOf(alice.address()).call().await?.balance; + let old_contract_balance = + erc20.balanceOf(contract_addr).call().await?.balance; + + let released = + contract.released_1(erc20_address).call().await?.released; + let releasable = + contract.releasable_1(erc20_address).call().await?.releasable; + assert_eq!(U256::ZERO, released); + assert_in_delta(expected_releasable, releasable); + + let receipt = receipt!(contract.release_1(erc20_address))?; + + let alice_balance = + erc20.balanceOf(alice.address()).call().await?.balance; + let contract_balance = + erc20.balanceOf(contract_addr).call().await?.balance; + let released = + contract.released_1(erc20_address).call().await?.released; + let releasable = + contract.releasable_1(erc20_address).call().await?.releasable; + assert_in_delta(expected_releasable, released); + assert_in_delta(U256::ZERO, releasable); + assert_in_delta(old_alice_balance, alice_balance); + assert_in_delta(old_contract_balance, contract_balance); + + assert!( + receipt.emits(VestingWallet::EtherReleased { amount: released }) + ); + + Ok(()) + } + + #[e2e::test] + async fn check_release_25_percent(alice: Account) -> eyre::Result<()> { + let expected_releasable = U256::from(BALANCE / 4); + let timestamp = block_timestamp(&alice).await?; + let start = timestamp - DURATION / 4; + + let contract_addr = alice + .as_deployer() + .with_constructor(ctr(alice.address(), start, DURATION)) + .deploy() + .await? + .address()?; + let contract = VestingWallet::new(contract_addr, &alice.wallet); + + let erc20_address = erc20::deploy(&alice.wallet).await?; + let erc20 = ERC20Mock::new(erc20_address, &alice.wallet); + let _ = watch!(erc20.mint(contract_addr, U256::from(BALANCE)))?; + + let old_alice_balance = + erc20.balanceOf(alice.address()).call().await?.balance; + let old_contract_balance = + erc20.balanceOf(contract_addr).call().await?.balance; + + let released = + contract.released_1(erc20_address).call().await?.released; + let releasable = + contract.releasable_1(erc20_address).call().await?.releasable; + assert_eq!(U256::ZERO, released); + assert_in_delta(expected_releasable, releasable); + + let receipt = receipt!(contract.release_1(erc20_address))?; + + let alice_balance = + erc20.balanceOf(alice.address()).call().await?.balance; + let contract_balance = + erc20.balanceOf(contract_addr).call().await?.balance; + let released = + contract.released_1(erc20_address).call().await?.released; + let releasable = + contract.releasable_1(erc20_address).call().await?.releasable; + assert_in_delta(expected_releasable, released); + assert_in_delta(U256::ZERO, releasable); + assert_in_delta(old_alice_balance + released, alice_balance); + assert_in_delta(old_contract_balance - released, contract_balance); + + assert!( + receipt.emits(VestingWallet::EtherReleased { amount: released }) + ); + + Ok(()) + } + + #[e2e::test] + async fn check_release_50_percent(alice: Account) -> eyre::Result<()> { + let expected_releasable = U256::from(BALANCE / 2); + let timestamp = block_timestamp(&alice).await?; + let start = timestamp - DURATION / 2; + + let contract_addr = alice + .as_deployer() + .with_constructor(ctr(alice.address(), start, DURATION)) + .deploy() + .await? + .address()?; + let contract = VestingWallet::new(contract_addr, &alice.wallet); + + let erc20_address = erc20::deploy(&alice.wallet).await?; + let erc20 = ERC20Mock::new(erc20_address, &alice.wallet); + let _ = watch!(erc20.mint(contract_addr, U256::from(BALANCE)))?; + + let old_alice_balance = + erc20.balanceOf(alice.address()).call().await?.balance; + let old_contract_balance = + erc20.balanceOf(contract_addr).call().await?.balance; + + let released = + contract.released_1(erc20_address).call().await?.released; + let releasable = + contract.releasable_1(erc20_address).call().await?.releasable; + assert_eq!(U256::ZERO, released); + assert_in_delta(expected_releasable, releasable); + + let receipt = receipt!(contract.release_1(erc20_address))?; + + let alice_balance = + erc20.balanceOf(alice.address()).call().await?.balance; + let contract_balance = + erc20.balanceOf(contract_addr).call().await?.balance; + let released = + contract.released_1(erc20_address).call().await?.released; + let releasable = + contract.releasable_1(erc20_address).call().await?.releasable; + assert_in_delta(expected_releasable, released); + assert_in_delta(U256::ZERO, releasable); + assert_in_delta(old_alice_balance + released, alice_balance); + assert_in_delta(old_contract_balance - released, contract_balance); + + assert!( + receipt.emits(VestingWallet::EtherReleased { amount: released }) + ); + + Ok(()) + } + + #[e2e::test] + async fn check_release_100_percent(alice: Account) -> eyre::Result<()> { + let expected_releasable = U256::from(BALANCE); + let timestamp = block_timestamp(&alice).await?; + let start = timestamp - DURATION; + + let contract_addr = alice + .as_deployer() + .with_constructor(ctr(alice.address(), start, DURATION)) + .deploy() + .await? + .address()?; + let contract = VestingWallet::new(contract_addr, &alice.wallet); + + let erc20_address = erc20::deploy(&alice.wallet).await?; + let erc20 = ERC20Mock::new(erc20_address, &alice.wallet); + let _ = watch!(erc20.mint(contract_addr, U256::from(BALANCE)))?; + + let old_alice_balance = + erc20.balanceOf(alice.address()).call().await?.balance; + let old_contract_balance = + erc20.balanceOf(contract_addr).call().await?.balance; + + let released = + contract.released_1(erc20_address).call().await?.released; + let releasable = + contract.releasable_1(erc20_address).call().await?.releasable; + assert_eq!(U256::ZERO, released); + assert_in_delta(expected_releasable, releasable); + + let receipt = receipt!(contract.release_1(erc20_address))?; + + let alice_balance = + erc20.balanceOf(alice.address()).call().await?.balance; + let contract_balance = + erc20.balanceOf(contract_addr).call().await?.balance; + let released = + contract.released_1(erc20_address).call().await?.released; + let releasable = + contract.releasable_1(erc20_address).call().await?.releasable; + assert_in_delta(expected_releasable, released); + assert_in_delta(U256::ZERO, releasable); + assert_in_delta(old_alice_balance + released, alice_balance); + assert_in_delta(old_contract_balance - released, contract_balance); + + assert!( + receipt.emits(VestingWallet::EtherReleased { amount: released }) + ); + + Ok(()) + } + + #[e2e::test] + async fn check_release_100_percent_vesting_in_past( + alice: Account, + ) -> eyre::Result<()> { + let expected_releasable = U256::from(BALANCE); + let timestamp = block_timestamp(&alice).await?; + let start = timestamp - DURATION * 4 / 3; + + let contract_addr = alice + .as_deployer() + .with_constructor(ctr(alice.address(), start, DURATION)) + .deploy() + .await? + .address()?; + let contract = VestingWallet::new(contract_addr, &alice.wallet); + + let erc20_address = erc20::deploy(&alice.wallet).await?; + let erc20 = ERC20Mock::new(erc20_address, &alice.wallet); + let _ = watch!(erc20.mint(contract_addr, U256::from(BALANCE)))?; + + let old_alice_balance = + erc20.balanceOf(alice.address()).call().await?.balance; + let old_contract_balance = + erc20.balanceOf(contract_addr).call().await?.balance; + + let released = + contract.released_1(erc20_address).call().await?.released; + let releasable = + contract.releasable_1(erc20_address).call().await?.releasable; + assert_eq!(U256::ZERO, released); + assert_in_delta(expected_releasable, releasable); + + let receipt = receipt!(contract.release_1(erc20_address))?; + + let alice_balance = + erc20.balanceOf(alice.address()).call().await?.balance; + let contract_balance = + erc20.balanceOf(contract_addr).call().await?.balance; + let released = + contract.released_1(erc20_address).call().await?.released; + let releasable = + contract.releasable_1(erc20_address).call().await?.releasable; + assert_in_delta(expected_releasable, released); + assert_in_delta(U256::ZERO, releasable); + assert_in_delta(old_alice_balance + released, alice_balance); + assert_in_delta(old_contract_balance - released, contract_balance); + + assert!( + receipt.emits(VestingWallet::EtherReleased { amount: released }) + ); + + Ok(()) + } } From eba2886879018da00211959b3acec9f639af1a26 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 15 Oct 2024 13:55:06 +0200 Subject: [PATCH 049/138] vesting_in_past -> vesting_in_the_past --- examples/vesting-wallet/tests/vesting-wallet.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/vesting-wallet/tests/vesting-wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs index bfdbca314..0ebefa01e 100644 --- a/examples/vesting-wallet/tests/vesting-wallet.rs +++ b/examples/vesting-wallet/tests/vesting-wallet.rs @@ -318,7 +318,7 @@ mod ether_vesting { } #[e2e::test] - async fn check_release_100_percent_vesting_in_past( + async fn check_release_100_percent_vesting_in_the_past( alice: Account, ) -> eyre::Result<()> { let expected_releasable = U256::from(BALANCE); @@ -615,7 +615,7 @@ mod erc20_vesting { } #[e2e::test] - async fn check_release_100_percent_vesting_in_past( + async fn check_release_100_percent_vesting_in_the_past( alice: Account, ) -> eyre::Result<()> { let expected_releasable = U256::from(BALANCE); From 219f5dcb99be2c2d88311dae3382a5b8b95546b1 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 15 Oct 2024 14:25:00 +0200 Subject: [PATCH 050/138] Extract common check_release logic --- .../vesting-wallet/tests/vesting-wallet.rs | 482 +++--------------- 1 file changed, 69 insertions(+), 413 deletions(-) diff --git a/examples/vesting-wallet/tests/vesting-wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs index 0ebefa01e..92c239d32 100644 --- a/examples/vesting-wallet/tests/vesting-wallet.rs +++ b/examples/vesting-wallet/tests/vesting-wallet.rs @@ -95,235 +95,13 @@ async fn rejects_zero_address_for_beneficiary( mod ether_vesting { use super::*; - #[e2e::test] - async fn check_vesting_schedule(alice: Account) -> eyre::Result<()> { - let start = block_timestamp(&alice).await?; - let contract_addr = alice - .as_deployer() - .with_constructor(ctr(alice.address(), start, DURATION)) - .deploy() - .await? - .address()?; - let contract = VestingWallet::new(contract_addr, &alice.wallet); - - let _ = watch!(contract.receiveEther().value(U256::from(BALANCE)))?; - - for i in 0..64 { - let timestamp = i * DURATION / 60 + start; - let expected_amount = U256::from(std::cmp::min( - BALANCE, - BALANCE * (timestamp - start) / DURATION, - )); - - let vested_amount = - contract.vestedAmount_0(timestamp).call().await?.vestedAmount; - assert_eq!( - expected_amount, vested_amount, - "\n---\ni: {i}\nstart: {start}\ntimestamp: {timestamp}\n---\n" - ); - } - - Ok(()) - } - - #[e2e::test] - async fn check_release_0_percent(alice: Account) -> eyre::Result<()> { - let expected_releasable = U256::ZERO; - let start = block_timestamp(&alice).await?; - - let contract_addr = alice - .as_deployer() - .with_constructor(ctr(alice.address(), start, DURATION)) - .deploy() - .await? - .address()?; - let contract = VestingWallet::new(contract_addr, &alice.wallet); - - let _ = watch!(contract.receiveEther().value(U256::from(BALANCE)))?; - - let old_alice_balance = - alice.wallet.get_balance(alice.address()).await?; - let old_contract_balance = - alice.wallet.get_balance(contract_addr).await?; - - let released = contract.released_0().call().await?.released; - let releasable = contract.releasable_0().call().await?.releasable; - assert_eq!(U256::ZERO, released); - assert_in_delta(expected_releasable, releasable); - - let receipt = receipt!(contract.release_0())?; - - let alice_balance = alice.wallet.get_balance(alice.address()).await?; - let contract_balance = alice.wallet.get_balance(contract_addr).await?; - let released = contract.released_0().call().await?.released; - let releasable = contract.releasable_0().call().await?.releasable; - assert_in_delta(expected_releasable, released); - assert_in_delta(U256::ZERO, releasable); - assert_in_delta( - old_alice_balance - - U256::from(receipt.gas_used * receipt.effective_gas_price), - alice_balance, - ); - assert_in_delta(old_contract_balance, contract_balance); - - assert!( - receipt.emits(VestingWallet::EtherReleased { amount: released }) - ); - - Ok(()) - } - - #[e2e::test] - async fn check_release_25_percent(alice: Account) -> eyre::Result<()> { - let expected_releasable = U256::from(BALANCE / 4); - let timestamp = block_timestamp(&alice).await?; - let start = timestamp - DURATION / 4; - - let contract_addr = alice - .as_deployer() - .with_constructor(ctr(alice.address(), start, DURATION)) - .deploy() - .await? - .address()?; - let contract = VestingWallet::new(contract_addr, &alice.wallet); - - let _ = watch!(contract.receiveEther().value(U256::from(BALANCE)))?; - - let old_alice_balance = - alice.wallet.get_balance(alice.address()).await?; - let old_contract_balance = - alice.wallet.get_balance(contract_addr).await?; - - let released = contract.released_0().call().await?.released; - let releasable = contract.releasable_0().call().await?.releasable; - assert_eq!(U256::ZERO, released); - assert_in_delta(expected_releasable, releasable); - - let receipt = receipt!(contract.release_0())?; - - let alice_balance = alice.wallet.get_balance(alice.address()).await?; - let contract_balance = alice.wallet.get_balance(contract_addr).await?; - let released = contract.released_0().call().await?.released; - let releasable = contract.releasable_0().call().await?.releasable; - assert_in_delta(expected_releasable, released); - assert_in_delta(U256::ZERO, releasable); - assert_in_delta( - old_alice_balance + released - - U256::from(receipt.gas_used * receipt.effective_gas_price), - alice_balance, - ); - assert_in_delta(old_contract_balance - released, contract_balance); - - assert!( - receipt.emits(VestingWallet::EtherReleased { amount: released }) - ); - - Ok(()) - } - - #[e2e::test] - async fn check_release_50_percent(alice: Account) -> eyre::Result<()> { - let expected_releasable = U256::from(BALANCE / 2); - let timestamp = block_timestamp(&alice).await?; - let start = timestamp - DURATION / 2; - - let contract_addr = alice - .as_deployer() - .with_constructor(ctr(alice.address(), start, DURATION)) - .deploy() - .await? - .address()?; - let contract = VestingWallet::new(contract_addr, &alice.wallet); - - let _ = watch!(contract.receiveEther().value(U256::from(BALANCE)))?; - - let old_alice_balance = - alice.wallet.get_balance(alice.address()).await?; - let old_contract_balance = - alice.wallet.get_balance(contract_addr).await?; - - let released = contract.released_0().call().await?.released; - let releasable = contract.releasable_0().call().await?.releasable; - assert_eq!(U256::ZERO, released); - assert_in_delta(expected_releasable, releasable); - - let receipt = receipt!(contract.release_0())?; - - let alice_balance = alice.wallet.get_balance(alice.address()).await?; - let contract_balance = alice.wallet.get_balance(contract_addr).await?; - let released = contract.released_0().call().await?.released; - let releasable = contract.releasable_0().call().await?.releasable; - assert_in_delta(expected_releasable, released); - assert_in_delta(U256::ZERO, releasable); - assert_in_delta( - old_alice_balance + released - - U256::from(receipt.gas_used * receipt.effective_gas_price), - alice_balance, - ); - assert_in_delta(old_contract_balance - released, contract_balance); - - assert!( - receipt.emits(VestingWallet::EtherReleased { amount: released }) - ); - - Ok(()) - } - - #[e2e::test] - async fn check_release_100_percent(alice: Account) -> eyre::Result<()> { - let expected_releasable = U256::from(BALANCE); - let timestamp = block_timestamp(&alice).await?; - let start = timestamp - DURATION; - - let contract_addr = alice - .as_deployer() - .with_constructor(ctr(alice.address(), start, DURATION)) - .deploy() - .await? - .address()?; - let contract = VestingWallet::new(contract_addr, &alice.wallet); - - let _ = watch!(contract.receiveEther().value(U256::from(BALANCE)))?; - - let old_alice_balance = - alice.wallet.get_balance(alice.address()).await?; - let old_contract_balance = - alice.wallet.get_balance(contract_addr).await?; - - let released = contract.released_0().call().await?.released; - let releasable = contract.releasable_0().call().await?.releasable; - assert_eq!(U256::ZERO, released); - assert_in_delta(expected_releasable, releasable); - - let receipt = receipt!(contract.release_0())?; - - let alice_balance = alice.wallet.get_balance(alice.address()).await?; - let contract_balance = alice.wallet.get_balance(contract_addr).await?; - let released = contract.released_0().call().await?.released; - let releasable = contract.releasable_0().call().await?.releasable; - assert_in_delta(expected_releasable, released); - assert_in_delta(U256::ZERO, releasable); - assert_in_delta( - old_alice_balance + released - - U256::from(receipt.gas_used * receipt.effective_gas_price), - alice_balance, - ); - assert_in_delta(old_contract_balance - released, contract_balance); - - assert!( - receipt.emits(VestingWallet::EtherReleased { amount: released }) - ); - - Ok(()) - } - - #[e2e::test] - async fn check_release_100_percent_vesting_in_the_past( + async fn run_check_release( alice: Account, + expected_releasable: U256, + time_passed: u64, ) -> eyre::Result<()> { - let expected_releasable = U256::from(BALANCE); let timestamp = block_timestamp(&alice).await?; - let start = timestamp - DURATION * 4 / 3; + let start = timestamp - time_passed; let contract_addr = alice .as_deployer() @@ -366,10 +144,6 @@ mod ether_vesting { Ok(()) } -} - -mod erc20_vesting { - use super::*; #[e2e::test] async fn check_vesting_schedule(alice: Account) -> eyre::Result<()> { @@ -382,9 +156,7 @@ mod erc20_vesting { .address()?; let contract = VestingWallet::new(contract_addr, &alice.wallet); - let erc20_address = erc20::deploy(&alice.wallet).await?; - let erc20 = ERC20Mock::new(erc20_address, &alice.wallet); - let _ = watch!(erc20.mint(contract_addr, U256::from(BALANCE)))?; + let _ = watch!(contract.receiveEther().value(U256::from(BALANCE)))?; for i in 0..64 { let timestamp = i * DURATION / 60 + start; @@ -393,11 +165,8 @@ mod erc20_vesting { BALANCE * (timestamp - start) / DURATION, )); - let vested_amount = contract - .vestedAmount_1(erc20_address, timestamp) - .call() - .await? - .vestedAmount; + let vested_amount = + contract.vestedAmount_0(timestamp).call().await?.vestedAmount; assert_eq!( expected_amount, vested_amount, "\n---\ni: {i}\nstart: {start}\ntimestamp: {timestamp}\n---\n" @@ -409,60 +178,42 @@ mod erc20_vesting { #[e2e::test] async fn check_release_0_percent(alice: Account) -> eyre::Result<()> { - let expected_releasable = U256::ZERO; - let start = block_timestamp(&alice).await?; - - let contract_addr = alice - .as_deployer() - .with_constructor(ctr(alice.address(), start, DURATION)) - .deploy() - .await? - .address()?; - let contract = VestingWallet::new(contract_addr, &alice.wallet); - - let erc20_address = erc20::deploy(&alice.wallet).await?; - let erc20 = ERC20Mock::new(erc20_address, &alice.wallet); - let _ = watch!(erc20.mint(contract_addr, U256::from(BALANCE)))?; - - let old_alice_balance = - erc20.balanceOf(alice.address()).call().await?.balance; - let old_contract_balance = - erc20.balanceOf(contract_addr).call().await?.balance; - - let released = - contract.released_1(erc20_address).call().await?.released; - let releasable = - contract.releasable_1(erc20_address).call().await?.releasable; - assert_eq!(U256::ZERO, released); - assert_in_delta(expected_releasable, releasable); - - let receipt = receipt!(contract.release_1(erc20_address))?; + run_check_release(alice, U256::ZERO, 0).await + } - let alice_balance = - erc20.balanceOf(alice.address()).call().await?.balance; - let contract_balance = - erc20.balanceOf(contract_addr).call().await?.balance; - let released = - contract.released_1(erc20_address).call().await?.released; - let releasable = - contract.releasable_1(erc20_address).call().await?.releasable; - assert_in_delta(expected_releasable, released); - assert_in_delta(U256::ZERO, releasable); - assert_in_delta(old_alice_balance, alice_balance); - assert_in_delta(old_contract_balance, contract_balance); + #[e2e::test] + async fn check_release_25_percent(alice: Account) -> eyre::Result<()> { + run_check_release(alice, U256::from(BALANCE / 4), DURATION / 4).await + } - assert!( - receipt.emits(VestingWallet::EtherReleased { amount: released }) - ); + #[e2e::test] + async fn check_release_50_percent(alice: Account) -> eyre::Result<()> { + run_check_release(alice, U256::from(BALANCE / 2), DURATION / 2).await + } - Ok(()) + #[e2e::test] + async fn check_release_100_percent(alice: Account) -> eyre::Result<()> { + run_check_release(alice, U256::from(BALANCE), DURATION).await } #[e2e::test] - async fn check_release_25_percent(alice: Account) -> eyre::Result<()> { - let expected_releasable = U256::from(BALANCE / 4); + async fn check_release_100_percent_vesting_in_the_past( + alice: Account, + ) -> eyre::Result<()> { + run_check_release(alice, U256::from(BALANCE), DURATION * 4 / 3).await + } +} + +mod erc20_vesting { + use super::*; + + async fn run_check_release( + alice: Account, + expected_releasable: U256, + time_passed: u64, + ) -> eyre::Result<()> { let timestamp = block_timestamp(&alice).await?; - let start = timestamp - DURATION / 4; + let start = timestamp - time_passed; let contract_addr = alice .as_deployer() @@ -511,11 +262,8 @@ mod erc20_vesting { } #[e2e::test] - async fn check_release_50_percent(alice: Account) -> eyre::Result<()> { - let expected_releasable = U256::from(BALANCE / 2); - let timestamp = block_timestamp(&alice).await?; - let start = timestamp - DURATION / 2; - + async fn check_vesting_schedule(alice: Account) -> eyre::Result<()> { + let start = block_timestamp(&alice).await?; let contract_addr = alice .as_deployer() .with_constructor(ctr(alice.address(), start, DURATION)) @@ -528,143 +276,51 @@ mod erc20_vesting { let erc20 = ERC20Mock::new(erc20_address, &alice.wallet); let _ = watch!(erc20.mint(contract_addr, U256::from(BALANCE)))?; - let old_alice_balance = - erc20.balanceOf(alice.address()).call().await?.balance; - let old_contract_balance = - erc20.balanceOf(contract_addr).call().await?.balance; - - let released = - contract.released_1(erc20_address).call().await?.released; - let releasable = - contract.releasable_1(erc20_address).call().await?.releasable; - assert_eq!(U256::ZERO, released); - assert_in_delta(expected_releasable, releasable); - - let receipt = receipt!(contract.release_1(erc20_address))?; - - let alice_balance = - erc20.balanceOf(alice.address()).call().await?.balance; - let contract_balance = - erc20.balanceOf(contract_addr).call().await?.balance; - let released = - contract.released_1(erc20_address).call().await?.released; - let releasable = - contract.releasable_1(erc20_address).call().await?.releasable; - assert_in_delta(expected_releasable, released); - assert_in_delta(U256::ZERO, releasable); - assert_in_delta(old_alice_balance + released, alice_balance); - assert_in_delta(old_contract_balance - released, contract_balance); + for i in 0..64 { + let timestamp = i * DURATION / 60 + start; + let expected_amount = U256::from(std::cmp::min( + BALANCE, + BALANCE * (timestamp - start) / DURATION, + )); - assert!( - receipt.emits(VestingWallet::EtherReleased { amount: released }) - ); + let vested_amount = contract + .vestedAmount_1(erc20_address, timestamp) + .call() + .await? + .vestedAmount; + assert_eq!( + expected_amount, vested_amount, + "\n---\ni: {i}\nstart: {start}\ntimestamp: {timestamp}\n---\n" + ); + } Ok(()) } #[e2e::test] - async fn check_release_100_percent(alice: Account) -> eyre::Result<()> { - let expected_releasable = U256::from(BALANCE); - let timestamp = block_timestamp(&alice).await?; - let start = timestamp - DURATION; - - let contract_addr = alice - .as_deployer() - .with_constructor(ctr(alice.address(), start, DURATION)) - .deploy() - .await? - .address()?; - let contract = VestingWallet::new(contract_addr, &alice.wallet); - - let erc20_address = erc20::deploy(&alice.wallet).await?; - let erc20 = ERC20Mock::new(erc20_address, &alice.wallet); - let _ = watch!(erc20.mint(contract_addr, U256::from(BALANCE)))?; - - let old_alice_balance = - erc20.balanceOf(alice.address()).call().await?.balance; - let old_contract_balance = - erc20.balanceOf(contract_addr).call().await?.balance; - - let released = - contract.released_1(erc20_address).call().await?.released; - let releasable = - contract.releasable_1(erc20_address).call().await?.releasable; - assert_eq!(U256::ZERO, released); - assert_in_delta(expected_releasable, releasable); - - let receipt = receipt!(contract.release_1(erc20_address))?; + async fn check_release_0_percent(alice: Account) -> eyre::Result<()> { + run_check_release(alice, U256::ZERO, 0).await + } - let alice_balance = - erc20.balanceOf(alice.address()).call().await?.balance; - let contract_balance = - erc20.balanceOf(contract_addr).call().await?.balance; - let released = - contract.released_1(erc20_address).call().await?.released; - let releasable = - contract.releasable_1(erc20_address).call().await?.releasable; - assert_in_delta(expected_releasable, released); - assert_in_delta(U256::ZERO, releasable); - assert_in_delta(old_alice_balance + released, alice_balance); - assert_in_delta(old_contract_balance - released, contract_balance); + #[e2e::test] + async fn check_release_25_percent(alice: Account) -> eyre::Result<()> { + run_check_release(alice, U256::from(BALANCE / 4), DURATION / 4).await + } - assert!( - receipt.emits(VestingWallet::EtherReleased { amount: released }) - ); + #[e2e::test] + async fn check_release_50_percent(alice: Account) -> eyre::Result<()> { + run_check_release(alice, U256::from(BALANCE / 2), DURATION / 2).await + } - Ok(()) + #[e2e::test] + async fn check_release_100_percent(alice: Account) -> eyre::Result<()> { + run_check_release(alice, U256::from(BALANCE), DURATION).await } #[e2e::test] async fn check_release_100_percent_vesting_in_the_past( alice: Account, ) -> eyre::Result<()> { - let expected_releasable = U256::from(BALANCE); - let timestamp = block_timestamp(&alice).await?; - let start = timestamp - DURATION * 4 / 3; - - let contract_addr = alice - .as_deployer() - .with_constructor(ctr(alice.address(), start, DURATION)) - .deploy() - .await? - .address()?; - let contract = VestingWallet::new(contract_addr, &alice.wallet); - - let erc20_address = erc20::deploy(&alice.wallet).await?; - let erc20 = ERC20Mock::new(erc20_address, &alice.wallet); - let _ = watch!(erc20.mint(contract_addr, U256::from(BALANCE)))?; - - let old_alice_balance = - erc20.balanceOf(alice.address()).call().await?.balance; - let old_contract_balance = - erc20.balanceOf(contract_addr).call().await?.balance; - - let released = - contract.released_1(erc20_address).call().await?.released; - let releasable = - contract.releasable_1(erc20_address).call().await?.releasable; - assert_eq!(U256::ZERO, released); - assert_in_delta(expected_releasable, releasable); - - let receipt = receipt!(contract.release_1(erc20_address))?; - - let alice_balance = - erc20.balanceOf(alice.address()).call().await?.balance; - let contract_balance = - erc20.balanceOf(contract_addr).call().await?.balance; - let released = - contract.released_1(erc20_address).call().await?.released; - let releasable = - contract.releasable_1(erc20_address).call().await?.releasable; - assert_in_delta(expected_releasable, released); - assert_in_delta(U256::ZERO, releasable); - assert_in_delta(old_alice_balance + released, alice_balance); - assert_in_delta(old_contract_balance - released, contract_balance); - - assert!( - receipt.emits(VestingWallet::EtherReleased { amount: released }) - ); - - Ok(()) + run_check_release(alice, U256::from(BALANCE), DURATION * 4 / 3).await } } From 6e4b2c5387d7b3aabcbb50135b5c92c3b69522ea Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 15 Oct 2024 14:48:41 +0200 Subject: [PATCH 051/138] Revert e2e-tests.sh --- scripts/e2e-tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/e2e-tests.sh b/scripts/e2e-tests.sh index 953b986fb..f7b9cdeb1 100755 --- a/scripts/e2e-tests.sh +++ b/scripts/e2e-tests.sh @@ -11,4 +11,4 @@ cargo +"$NIGHTLY_TOOLCHAIN" build --release --target wasm32-unknown-unknown -Z b export RPC_URL=http://localhost:8547 # We should use stable here once nitro-testnode is updated and the contracts fit # the size limit. Work tracked [here](https://github.com/OpenZeppelin/rust-contracts-stylus/issues/87) -cargo +"$NIGHTLY_TOOLCHAIN" test --features e2e --test "vesting-wallet" +cargo +"$NIGHTLY_TOOLCHAIN" test --features std,e2e --test "*" From 64bb0341244b2550d809c1bca58896fbd2adb2f3 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 15 Oct 2024 14:50:31 +0200 Subject: [PATCH 052/138] Remove OwnableUnauthorizedAccount from mod.rs --- examples/vesting-wallet/tests/abi/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/vesting-wallet/tests/abi/mod.rs b/examples/vesting-wallet/tests/abi/mod.rs index 822cfdc1a..49f40b2a0 100644 --- a/examples/vesting-wallet/tests/abi/mod.rs +++ b/examples/vesting-wallet/tests/abi/mod.rs @@ -18,7 +18,6 @@ sol!( function vestedAmount(uint64 timestamp) external view returns (uint256 vestedAmount); function vestedAmount(address token, uint64 timestamp) external view returns (uint256 vestedAmount); - error OwnableUnauthorizedAccount(address account); error OwnableInvalidOwner(address owner); error ReleaseTokenFailed(address token); From 64d6db6a1b2db98ef5c1e76585ca6d8a07409144 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 15 Oct 2024 14:50:52 +0200 Subject: [PATCH 053/138] Missing new line in Erc20 mock --- examples/vesting-wallet/tests/mock/erc20.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/vesting-wallet/tests/mock/erc20.rs b/examples/vesting-wallet/tests/mock/erc20.rs index df5e55177..eaca2660a 100644 --- a/examples/vesting-wallet/tests/mock/erc20.rs +++ b/examples/vesting-wallet/tests/mock/erc20.rs @@ -10,6 +10,7 @@ sol! { // SPDX-License-Identifier: MIT contract ERC20Mock is ERC20 { constructor() ERC20("ERC20Mock", "MTK") {} + function balanceOf(address account) public override view returns (uint256 balance) { return super.balanceOf(account); } From 01c3f0ab534c388f7d5e21dad709478587835072 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 15 Oct 2024 15:01:56 +0200 Subject: [PATCH 054/138] Add comment to assert_in_delta --- examples/vesting-wallet/tests/vesting-wallet.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/examples/vesting-wallet/tests/vesting-wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs index 92c239d32..7be484e57 100644 --- a/examples/vesting-wallet/tests/vesting-wallet.rs +++ b/examples/vesting-wallet/tests/vesting-wallet.rs @@ -43,6 +43,11 @@ async fn block_timestamp(account: &Account) -> eyre::Result { Ok(timestamp) } +/// Since the block timestamp can theoretically change between the initial fetch (to calculate the `start` timestamp) +/// and the final release of vested funds in the test, it is best we assert that the released amount is within +/// some predefined range. +/// The reason why the timestamp can change is that we perform many mutations on-chain, from deploying and activating +/// contracts, sending initial ETH/ERC20 to the contract and then finally releasing the funds. fn assert_in_delta(expected: U256, actual: U256) { let diff = expected.abs_diff(actual); let delta = U256::from(1); From 710cf868465880930a35942ea45f4b9a1b6b6418 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 15 Oct 2024 16:04:50 +0200 Subject: [PATCH 055/138] Remove expected_releasable from run_check_release (calc. it from time_passed) --- .../vesting-wallet/tests/vesting-wallet.rs | 38 +++++++++++-------- scripts/e2e-tests.sh | 2 +- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/examples/vesting-wallet/tests/vesting-wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs index 7be484e57..e21aa5a1f 100644 --- a/examples/vesting-wallet/tests/vesting-wallet.rs +++ b/examples/vesting-wallet/tests/vesting-wallet.rs @@ -102,11 +102,14 @@ mod ether_vesting { async fn run_check_release( alice: Account, - expected_releasable: U256, time_passed: u64, ) -> eyre::Result<()> { let timestamp = block_timestamp(&alice).await?; let start = timestamp - time_passed; + let expected_amount = U256::from(std::cmp::min( + BALANCE, + BALANCE * time_passed / DURATION, + )); let contract_addr = alice .as_deployer() @@ -126,7 +129,7 @@ mod ether_vesting { let released = contract.released_0().call().await?.released; let releasable = contract.releasable_0().call().await?.releasable; assert_eq!(U256::ZERO, released); - assert_in_delta(expected_releasable, releasable); + assert_in_delta(expected_amount, releasable); let receipt = receipt!(contract.release_0())?; @@ -134,7 +137,7 @@ mod ether_vesting { let contract_balance = alice.wallet.get_balance(contract_addr).await?; let released = contract.released_0().call().await?.released; let releasable = contract.releasable_0().call().await?.releasable; - assert_in_delta(expected_releasable, released); + assert_in_delta(expected_amount, released); assert_in_delta(U256::ZERO, releasable); assert_in_delta( old_alice_balance + released @@ -183,29 +186,29 @@ mod ether_vesting { #[e2e::test] async fn check_release_0_percent(alice: Account) -> eyre::Result<()> { - run_check_release(alice, U256::ZERO, 0).await + run_check_release(alice, 0).await } #[e2e::test] async fn check_release_25_percent(alice: Account) -> eyre::Result<()> { - run_check_release(alice, U256::from(BALANCE / 4), DURATION / 4).await + run_check_release(alice, DURATION / 4).await } #[e2e::test] async fn check_release_50_percent(alice: Account) -> eyre::Result<()> { - run_check_release(alice, U256::from(BALANCE / 2), DURATION / 2).await + run_check_release(alice, DURATION / 2).await } #[e2e::test] async fn check_release_100_percent(alice: Account) -> eyre::Result<()> { - run_check_release(alice, U256::from(BALANCE), DURATION).await + run_check_release(alice, DURATION).await } #[e2e::test] async fn check_release_100_percent_vesting_in_the_past( alice: Account, ) -> eyre::Result<()> { - run_check_release(alice, U256::from(BALANCE), DURATION * 4 / 3).await + run_check_release(alice, DURATION * 4 / 3).await } } @@ -214,11 +217,14 @@ mod erc20_vesting { async fn run_check_release( alice: Account, - expected_releasable: U256, time_passed: u64, ) -> eyre::Result<()> { let timestamp = block_timestamp(&alice).await?; let start = timestamp - time_passed; + let expected_amount = U256::from(std::cmp::min( + BALANCE, + BALANCE * time_passed / DURATION, + )); let contract_addr = alice .as_deployer() @@ -242,7 +248,7 @@ mod erc20_vesting { let releasable = contract.releasable_1(erc20_address).call().await?.releasable; assert_eq!(U256::ZERO, released); - assert_in_delta(expected_releasable, releasable); + assert_in_delta(expected_amount, releasable); let receipt = receipt!(contract.release_1(erc20_address))?; @@ -254,7 +260,7 @@ mod erc20_vesting { contract.released_1(erc20_address).call().await?.released; let releasable = contract.releasable_1(erc20_address).call().await?.releasable; - assert_in_delta(expected_releasable, released); + assert_in_delta(expected_amount, released); assert_in_delta(U256::ZERO, releasable); assert_in_delta(old_alice_balance + released, alice_balance); assert_in_delta(old_contract_balance - released, contract_balance); @@ -304,28 +310,28 @@ mod erc20_vesting { #[e2e::test] async fn check_release_0_percent(alice: Account) -> eyre::Result<()> { - run_check_release(alice, U256::ZERO, 0).await + run_check_release(alice, 0).await } #[e2e::test] async fn check_release_25_percent(alice: Account) -> eyre::Result<()> { - run_check_release(alice, U256::from(BALANCE / 4), DURATION / 4).await + run_check_release(alice, DURATION / 4).await } #[e2e::test] async fn check_release_50_percent(alice: Account) -> eyre::Result<()> { - run_check_release(alice, U256::from(BALANCE / 2), DURATION / 2).await + run_check_release(alice, DURATION / 2).await } #[e2e::test] async fn check_release_100_percent(alice: Account) -> eyre::Result<()> { - run_check_release(alice, U256::from(BALANCE), DURATION).await + run_check_release(alice, DURATION).await } #[e2e::test] async fn check_release_100_percent_vesting_in_the_past( alice: Account, ) -> eyre::Result<()> { - run_check_release(alice, U256::from(BALANCE), DURATION * 4 / 3).await + run_check_release(alice, DURATION * 4 / 3).await } } diff --git a/scripts/e2e-tests.sh b/scripts/e2e-tests.sh index f7b9cdeb1..953b986fb 100755 --- a/scripts/e2e-tests.sh +++ b/scripts/e2e-tests.sh @@ -11,4 +11,4 @@ cargo +"$NIGHTLY_TOOLCHAIN" build --release --target wasm32-unknown-unknown -Z b export RPC_URL=http://localhost:8547 # We should use stable here once nitro-testnode is updated and the contracts fit # the size limit. Work tracked [here](https://github.com/OpenZeppelin/rust-contracts-stylus/issues/87) -cargo +"$NIGHTLY_TOOLCHAIN" test --features std,e2e --test "*" +cargo +"$NIGHTLY_TOOLCHAIN" test --features e2e --test "vesting-wallet" From 18682bf99efac7c3b35f0a462f872d061536f782 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 15 Oct 2024 16:06:45 +0200 Subject: [PATCH 056/138] Run cargo fmt --- examples/vesting-wallet/tests/vesting-wallet.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/vesting-wallet/tests/vesting-wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs index e21aa5a1f..8b64bffa2 100644 --- a/examples/vesting-wallet/tests/vesting-wallet.rs +++ b/examples/vesting-wallet/tests/vesting-wallet.rs @@ -43,11 +43,13 @@ async fn block_timestamp(account: &Account) -> eyre::Result { Ok(timestamp) } -/// Since the block timestamp can theoretically change between the initial fetch (to calculate the `start` timestamp) -/// and the final release of vested funds in the test, it is best we assert that the released amount is within +/// Since the block timestamp can theoretically change between the initial fetch +/// (to calculate the `start` timestamp) and the final release of vested funds +/// in the test, it is best we assert that the released amount is within /// some predefined range. -/// The reason why the timestamp can change is that we perform many mutations on-chain, from deploying and activating -/// contracts, sending initial ETH/ERC20 to the contract and then finally releasing the funds. +/// The reason why the timestamp can change is that we perform many mutations +/// on-chain, from deploying and activating contracts, sending initial ETH/ERC20 +/// to the contract and then finally releasing the funds. fn assert_in_delta(expected: U256, actual: U256) { let diff = expected.abs_diff(actual); let delta = U256::from(1); From 33f0be52334ea77fbcd8b94e8c402958a4177768 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 15 Oct 2024 16:15:11 +0200 Subject: [PATCH 057/138] revert e2e-tests.sh --- scripts/e2e-tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/e2e-tests.sh b/scripts/e2e-tests.sh index 953b986fb..f7b9cdeb1 100755 --- a/scripts/e2e-tests.sh +++ b/scripts/e2e-tests.sh @@ -11,4 +11,4 @@ cargo +"$NIGHTLY_TOOLCHAIN" build --release --target wasm32-unknown-unknown -Z b export RPC_URL=http://localhost:8547 # We should use stable here once nitro-testnode is updated and the contracts fit # the size limit. Work tracked [here](https://github.com/OpenZeppelin/rust-contracts-stylus/issues/87) -cargo +"$NIGHTLY_TOOLCHAIN" test --features e2e --test "vesting-wallet" +cargo +"$NIGHTLY_TOOLCHAIN" test --features std,e2e --test "*" From a1dead2a653a3c2de0dd8b5698a4f6c0eb0fda84 Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 16 Oct 2024 10:56:51 +0200 Subject: [PATCH 058/138] Reimplement transfer_ownership & renounce_ownership --- contracts/src/finance/vesting_wallet.rs | 33 +++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 3cfab13a0..981a8ba19 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -111,6 +111,39 @@ impl VestingWallet { self.ownable.owner() } + /// Transfers ownership of the contract to a new account (`new_owner`). Can + /// only be called by the current owner. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `new_owner` - The next owner of this contract. + /// + /// # Errors + /// + /// If `new_owner` is the zero address, then the error + /// [`OwnableInvalidOwner`] is returned. + pub fn transfer_ownership( + &mut self, + new_owner: Address, + ) -> Result<(), Error> { + Ok(self.ownable.transfer_ownership(new_owner)?) + } + + /// Leaves the contract without owner. It will not be possible to call + /// [`Self::only_owner`] functions. Can only be called by the current owner. + /// + /// NOTE: Renouncing ownership will leave the contract without an owner, + /// thereby disabling any functionality that is only available to the owner. + /// + /// # Errors + /// + /// If not called by the owner, then the error + /// [`Error::UnauthorizedAccount`] is returned. + pub fn renounce_ownership(&mut self) -> Result<(), Error> { + Ok(self.ownable.renounce_ownership()?) + } + /// Getter for the start timestamp. pub fn start(&self) -> U256 { U256::from(self._start.get()) From 1314a16ad50065a59c99b47c1a90e88db72d15b2 Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 16 Oct 2024 11:13:25 +0200 Subject: [PATCH 059/138] Use transfer_eth --- contracts/src/finance/vesting_wallet.rs | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 981a8ba19..9ef82a668 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -24,8 +24,8 @@ use alloy_primitives::{Address, U256, U64}; use alloy_sol_types::sol; use stylus_proc::SolidityError; use stylus_sdk::{ - block, - call::{call, Call}, + block, call, + call::{transfer_eth, Call}, contract, evm, function_selector, storage::TopLevelStorage, stylus_proc::{public, sol_interface, sol_storage}, @@ -44,6 +44,11 @@ sol! { } sol! { + /// Indicates an error related to the underlying Ether transfer. + #[derive(Debug)] + #[allow(missing_docs)] + error ReleaseEtherFailed(); + /// Indicates an error related to the underlying ERC20 transfer. /// /// * `token` - Address of the token being released. @@ -72,9 +77,10 @@ sol_interface! { pub enum Error { /// Error type from [`Ownable`] contract [`ownable::Error`]. Ownable(ownable::Error), - /// Error type from [`stylus_sdk::call::Call`] contract - /// [`stylus_sdk::call::Error`]. - StylusError(stylus_sdk::call::Error), + /// Error type from [`call::Call`] contract [`call::Error`]. + StylusError(call::Error), + /// Indicates an error related to the underlying Ether transfer. + ReleaseEtherFailed(ReleaseEtherFailed), /// Indicates an error related to the underlying ERC20 transfer. ReleaseTokenFailed(ReleaseTokenFailed), } @@ -199,10 +205,8 @@ impl VestingWallet { evm::log(EtherReleased { amount }); - let owner = self.ownable.owner(); - call(Call::new_in(self).value(amount), owner, &[])?; - - Ok(()) + transfer_eth(self.ownable.owner(), amount) + .map_err(|_| Error::ReleaseEtherFailed(ReleaseEtherFailed {})) } /// Release the tokens that have already vested. From 548c6c961df1c25c3f3ca3b77ffe890aecd4909a Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 11 Nov 2024 11:08:05 +0100 Subject: [PATCH 060/138] ref: align example's cargo.toml + import SolidityError from stylus_sdk --- Cargo.lock | 4 +--- contracts/src/finance/vesting_wallet.rs | 3 +-- examples/vesting-wallet/Cargo.toml | 8 +++----- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 84a9b2e50..161c2628d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4024,15 +4024,13 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "vesting-wallet-example" -version = "0.0.0" +version = "0.1.1" dependencies = [ "alloy", "alloy-primitives", "e2e", "eyre", - "mini-alloc", "openzeppelin-stylus", - "stylus-proc", "stylus-sdk", "tokio", ] diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 9ef82a668..ffa4c122c 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -22,13 +22,12 @@ //! intended. use alloy_primitives::{Address, U256, U64}; use alloy_sol_types::sol; -use stylus_proc::SolidityError; use stylus_sdk::{ block, call, call::{transfer_eth, Call}, contract, evm, function_selector, storage::TopLevelStorage, - stylus_proc::{public, sol_interface, sol_storage}, + stylus_proc::{SolidityError, public, sol_interface, sol_storage}, }; use crate::access::{ownable, ownable::Ownable}; diff --git a/examples/vesting-wallet/Cargo.toml b/examples/vesting-wallet/Cargo.toml index fb67048f4..7f6cbf464 100644 --- a/examples/vesting-wallet/Cargo.toml +++ b/examples/vesting-wallet/Cargo.toml @@ -4,20 +4,18 @@ edition.workspace = true license.workspace = true repository.workspace = true publish = false -version = "0.0.0" +version.workspace = true [dependencies] -openzeppelin-stylus = { path = "../../contracts" } +openzeppelin-stylus.workspace = true alloy-primitives.workspace = true stylus-sdk.workspace = true -stylus-proc.workspace = true -mini-alloc.workspace = true [dev-dependencies] alloy.workspace = true eyre.workspace = true tokio.workspace = true -e2e = { path = "../../lib/e2e" } +e2e.workspace = true [features] e2e = [] From 7060af105e768f318163fb8db21991d291278824 Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 11 Nov 2024 12:47:34 +0100 Subject: [PATCH 061/138] ref: format code --- contracts/src/finance/vesting_wallet.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index ffa4c122c..639f54466 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -27,7 +27,7 @@ use stylus_sdk::{ call::{transfer_eth, Call}, contract, evm, function_selector, storage::TopLevelStorage, - stylus_proc::{SolidityError, public, sol_interface, sol_storage}, + stylus_proc::{public, sol_interface, sol_storage, SolidityError}, }; use crate::access::{ownable, ownable::Ownable}; From e8e487171d1600ab3d97734395615cffb8799d4a Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 11 Nov 2024 12:57:36 +0100 Subject: [PATCH 062/138] ref: move ierc20 sol interface to local module (avoid missing docs warn) --- contracts/src/finance/vesting_wallet.rs | 32 +++++++++++-------- .../vesting-wallet/tests/vesting-wallet.rs | 8 +++-- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 639f54466..eee980d7d 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -27,7 +27,7 @@ use stylus_sdk::{ call::{transfer_eth, Call}, contract, evm, function_selector, storage::TopLevelStorage, - stylus_proc::{public, sol_interface, sol_storage, SolidityError}, + stylus_proc::{public, sol_storage, SolidityError}, }; use crate::access::{ownable, ownable::Ownable}; @@ -56,18 +56,24 @@ sol! { error ReleaseTokenFailed(address token); } -sol_interface! { - /// Interface of the [`crate::token::erc20::Erc20`] standard as defined in the ERC. - interface IERC20 { - /// Returns the value of tokens owned by `account`. - function balanceOf(address account) external view returns (uint256); - - /// Moves a `value` amount of tokens from the caller's account to `to`. - /// - /// Returns a boolean value indicating whether the operation succeeded. - /// - /// Emits a [`crate::token::erc20::Transfer`] event. - function transfer(address to, uint256 value) external returns (bool); +pub use erc20::IERC20; +#[allow(missing_docs)] +mod erc20 { + use stylus_sdk::stylus_proc::sol_interface; + + sol_interface! { + /// Interface of the [`crate::token::erc20::Erc20`] standard as defined in the ERC. + interface IERC20 { + /// Returns the value of tokens owned by `account`. + function balanceOf(address account) external view returns (uint256); + + /// Moves a `value` amount of tokens from the caller's account to `to`. + /// + /// Returns a boolean value indicating whether the operation succeeded. + /// + /// Emits a [`crate::token::erc20::Transfer`] event. + function transfer(address to, uint256 value) external returns (bool); + } } } diff --git a/examples/vesting-wallet/tests/vesting-wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs index 8b64bffa2..e41c03ba1 100644 --- a/examples/vesting-wallet/tests/vesting-wallet.rs +++ b/examples/vesting-wallet/tests/vesting-wallet.rs @@ -1,8 +1,12 @@ #![cfg(feature = "e2e")] use abi::VestingWallet; -use alloy::{eips::BlockId, providers::Provider, sol}; -use alloy_primitives::{Address, U256}; +use alloy::{ + eips::BlockId, + primitives::{Address, U256}, + providers::Provider, + sol, +}; use e2e::{receipt, watch, Account, EventExt, ReceiptExt, Revert}; use mock::{erc20, erc20::ERC20Mock}; From 8e7270b46f8ba0f639af3b7530872b58fc58a261 Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 11 Nov 2024 13:16:12 +0100 Subject: [PATCH 063/138] fix: remove no_std from vesting-wallet example --- examples/vesting-wallet/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/vesting-wallet/src/lib.rs b/examples/vesting-wallet/src/lib.rs index 20a5bcfd1..6e74ebe28 100644 --- a/examples/vesting-wallet/src/lib.rs +++ b/examples/vesting-wallet/src/lib.rs @@ -1,4 +1,4 @@ -#![cfg_attr(not(test), no_main, no_std)] +#![cfg_attr(not(test), no_main)] extern crate alloc; use openzeppelin_stylus::finance::vesting_wallet::VestingWallet; From 26d8f4be87abd96e1eca2c7920311db0f0fa78b7 Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 11 Nov 2024 13:24:39 +0100 Subject: [PATCH 064/138] docs: update CHANGELOG.md --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94cc5d580..116b02052 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,10 +12,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - ERC-1155 Multi Token Standard. #275 - `SafeErc20` Utility. #289 - Finite Fields arithmetics. #376 +- `VestingWallet` contract. #402 ### Changed -- +- Implemented `MethodError` for `ownable::Error`. #402 ### Fixed From b2bdc0b3b22613653b643404a53fa84a25d07929 Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 11 Nov 2024 13:27:02 +0100 Subject: [PATCH 065/138] ref: interface casing change IERC20->IErc20 --- contracts/src/finance/vesting_wallet.rs | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index eee980d7d..73cf26304 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -56,22 +56,13 @@ sol! { error ReleaseTokenFailed(address token); } -pub use erc20::IERC20; +pub use token::IErc20; #[allow(missing_docs)] -mod erc20 { - use stylus_sdk::stylus_proc::sol_interface; - - sol_interface! { - /// Interface of the [`crate::token::erc20::Erc20`] standard as defined in the ERC. - interface IERC20 { - /// Returns the value of tokens owned by `account`. +mod token { + stylus_sdk::stylus_proc::sol_interface! { + /// Interface of the ERC-20 token. + interface IErc20 { function balanceOf(address account) external view returns (uint256); - - /// Moves a `value` amount of tokens from the caller's account to `to`. - /// - /// Returns a boolean value indicating whether the operation succeeded. - /// - /// Emits a [`crate::token::erc20::Transfer`] event. function transfer(address to, uint256 value) external returns (bool); } } From fc076546af1455ac596bc9deb940dc9ee75bb553 Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 11 Nov 2024 13:30:21 +0100 Subject: [PATCH 066/138] fix: SafeErc20 comment article an->a --- contracts/src/token/erc20/utils/safe_erc20.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/token/erc20/utils/safe_erc20.rs b/contracts/src/token/erc20/utils/safe_erc20.rs index 1669a69cd..0d005f9ff 100644 --- a/contracts/src/token/erc20/utils/safe_erc20.rs +++ b/contracts/src/token/erc20/utils/safe_erc20.rs @@ -77,7 +77,7 @@ sol_storage! { /// BorrowMut)`. Should be fixed in the future by the Stylus team. unsafe impl TopLevelStorage for SafeErc20 {} -/// Required interface of an [`SafeErc20`] utility contract. +/// Required interface of a [`SafeErc20`] utility contract. pub trait ISafeErc20 { /// The error type associated to this trait implementation. type Error: Into>; From 32744159aee07f67e46bba2061691062361323f0 Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 11 Nov 2024 13:51:31 +0100 Subject: [PATCH 067/138] ref: add IVestingWallet trait --- contracts/src/finance/vesting_wallet.rs | 143 +++++++++++++++--------- 1 file changed, 92 insertions(+), 51 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 73cf26304..a164d88f7 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -22,6 +22,7 @@ //! intended. use alloy_primitives::{Address, U256, U64}; use alloy_sol_types::sol; +use openzeppelin_stylus_proc::interface_id; use stylus_sdk::{ block, call, call::{transfer_eth, Call}, @@ -77,7 +78,7 @@ pub enum Error { StylusError(call::Error), /// Indicates an error related to the underlying Ether transfer. ReleaseEtherFailed(ReleaseEtherFailed), - /// Indicates an error related to the underlying ERC20 transfer. + /// Indicates an error related to the underlying [`IErc20`] transfer. ReleaseTokenFailed(ReleaseTokenFailed), } @@ -102,16 +103,17 @@ sol_storage! { /// BorrowMut)`. Should be fixed in the future by the Stylus team. unsafe impl TopLevelStorage for VestingWallet {} -#[public] -impl VestingWallet { +/// Required interface of a [`VestingWallet`] compliant contract. +#[interface_id] +pub trait IVestingWallet { + /// The error type associated to this trait implementation. + type Error: Into>; + /// The contract should be able to receive Eth. - #[payable] - pub fn receive_ether(&self) {} + fn receive_ether(&self); /// Returns the address of the current owner. - pub fn owner(&self) -> Address { - self.ownable.owner() - } + fn owner(&self) -> Address; /// Transfers ownership of the contract to a new account (`new_owner`). Can /// only be called by the current owner. @@ -125,12 +127,7 @@ impl VestingWallet { /// /// If `new_owner` is the zero address, then the error /// [`OwnableInvalidOwner`] is returned. - pub fn transfer_ownership( - &mut self, - new_owner: Address, - ) -> Result<(), Error> { - Ok(self.ownable.transfer_ownership(new_owner)?) - } + fn transfer_ownership(&mut self, new_owner: Address) -> Result<(), Error>; /// Leaves the contract without owner. It will not be possible to call /// [`Self::only_owner`] functions. Can only be called by the current owner. @@ -142,56 +139,111 @@ impl VestingWallet { /// /// If not called by the owner, then the error /// [`Error::UnauthorizedAccount`] is returned. - pub fn renounce_ownership(&mut self) -> Result<(), Error> { + fn renounce_ownership(&mut self) -> Result<(), Error>; + + /// Getter for the start timestamp. + fn start(&self) -> U256; + + /// Getter for the vesting duration. + fn duration(&self) -> U256; + + /// Getter for the end timestamp. + fn end(&self) -> U256; + + /// Amount of eth already released + #[selector(name = "released")] + fn released_eth(&self) -> U256; + + /// Amount of token already released + #[selector(name = "released")] + fn released_erc20(&self, token: Address) -> U256; + + /// Getter for the amount of releasable eth. + #[selector(name = "releasable")] + fn releasable_eth(&self) -> U256; + + /// Getter for the amount of releasable `token` tokens. `token` should be + /// the address of an [`crate::token::erc20::Erc20`] contract. + #[selector(name = "releasable")] + fn releasable_erc20(&mut self, token: Address) -> U256; + + /// Release the native token (ether) that have already vested. + /// + /// Emits an [`EtherReleased`] event. + #[selector(name = "release")] + fn release_eth(&mut self) -> Result<(), Error>; + + /// Release the tokens that have already vested. + /// + /// Emits an [`ERC20Released`] event. + #[selector(name = "release")] + fn release_erc20(&mut self, token: Address) -> Result<(), Error>; + + /// Calculates the amount of ether that has already vested. Default + /// implementation is a linear vesting curve. + #[selector(name = "vestedAmount")] + fn vested_amount_eth(&self, timestamp: u64) -> U256; + + /// Calculates the amount of tokens that has already vested. Default + /// implementation is a linear vesting curve. + #[selector(name = "vestedAmount")] + fn vested_amount_erc20(&mut self, token: Address, timestamp: u64) -> U256; +} + +#[public] +impl IVestingWallet for VestingWallet { + type Error = Error; + + #[payable] + fn receive_ether(&self) {} + + fn owner(&self) -> Address { + self.ownable.owner() + } + + fn transfer_ownership(&mut self, new_owner: Address) -> Result<(), Error> { + Ok(self.ownable.transfer_ownership(new_owner)?) + } + + fn renounce_ownership(&mut self) -> Result<(), Error> { Ok(self.ownable.renounce_ownership()?) } - /// Getter for the start timestamp. - pub fn start(&self) -> U256 { + fn start(&self) -> U256 { U256::from(self._start.get()) } - /// Getter for the vesting duration. - pub fn duration(&self) -> U256 { + fn duration(&self) -> U256 { U256::from(self._duration.get()) } - /// Getter for the end timestamp. - pub fn end(&self) -> U256 { + fn end(&self) -> U256 { self.start() + self.duration() } - /// Amount of eth already released #[selector(name = "released")] - pub fn released_eth(&self) -> U256 { + fn released_eth(&self) -> U256 { self._released.get() } - /// Amount of token already released #[selector(name = "released")] - pub fn released_erc20(&self, token: Address) -> U256 { + fn released_erc20(&self, token: Address) -> U256 { self._erc20_released.get(token) } - /// Getter for the amount of releasable eth. #[selector(name = "releasable")] - pub fn releasable_eth(&self) -> U256 { + fn releasable_eth(&self) -> U256 { self.vested_amount_eth(block::timestamp()) - self.released_eth() } - /// Getter for the amount of releasable `token` tokens. `token` should be - /// the address of an [`crate::token::erc20::Erc20`] contract. #[selector(name = "releasable")] - pub fn releasable_erc20(&mut self, token: Address) -> U256 { + fn releasable_erc20(&mut self, token: Address) -> U256 { self.vested_amount_erc20(token, block::timestamp()) - self.released_erc20(token) } - /// Release the native token (ether) that have already vested. - /// - /// Emits an [`EtherReleased`] event. #[selector(name = "release")] - pub fn release_eth(&mut self) -> Result<(), Error> { + fn release_eth(&mut self) -> Result<(), Error> { let amount = self.releasable_eth(); let released = self .released_eth() @@ -205,11 +257,8 @@ impl VestingWallet { .map_err(|_| Error::ReleaseEtherFailed(ReleaseEtherFailed {})) } - /// Release the tokens that have already vested. - /// - /// Emits an [`ERC20Released`] event. #[selector(name = "release")] - pub fn release_erc20(&mut self, token: Address) -> Result<(), Error> { + fn release_erc20(&mut self, token: Address) -> Result<(), Error> { let amount = self.releasable_erc20(token); let released = self .released_erc20(token) @@ -219,7 +268,7 @@ impl VestingWallet { evm::log(ERC20Released { token, amount }); - let erc20 = IERC20::new(token); + let erc20 = IErc20::new(token); let owner = self.ownable.owner(); let call = Call::new_in(self); let succeeded = erc20.transfer(call, owner, amount)?; @@ -230,25 +279,17 @@ impl VestingWallet { Ok(()) } - /// Calculates the amount of ether that has already vested. Default - /// implementation is a linear vesting curve. #[selector(name = "vestedAmount")] - pub fn vested_amount_eth(&self, timestamp: u64) -> U256 { + fn vested_amount_eth(&self, timestamp: u64) -> U256 { self._vesting_schedule( contract::balance() + self.released_eth(), U64::from(timestamp), ) } - /// Calculates the amount of tokens that has already vested. Default - /// implementation is a linear vesting curve. #[selector(name = "vestedAmount")] - pub fn vested_amount_erc20( - &mut self, - token: Address, - timestamp: u64, - ) -> U256 { - let erc20 = IERC20::new(token); + fn vested_amount_erc20(&mut self, token: Address, timestamp: u64) -> U256 { + let erc20 = IErc20::new(token); let call = Call::new_in(self); let balance = erc20 .balance_of(call, contract::address()) @@ -265,7 +306,7 @@ impl VestingWallet { /// Virtual implementation of the vesting formula. This returns the amount /// vested, as a function of time, for an asset given its total /// historical allocation. - pub fn _vesting_schedule( + fn _vesting_schedule( &self, total_allocation: U256, timestamp: U64, From 225716c813081e5bda7229eb405f959bfff2ef3e Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 11 Nov 2024 13:52:18 +0100 Subject: [PATCH 068/138] ref: rename _vesting_schedule->vesting_schedule --- contracts/src/finance/vesting_wallet.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index a164d88f7..78b3ae969 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -281,7 +281,7 @@ impl IVestingWallet for VestingWallet { #[selector(name = "vestedAmount")] fn vested_amount_eth(&self, timestamp: u64) -> U256 { - self._vesting_schedule( + self.vesting_schedule( contract::balance() + self.released_eth(), U64::from(timestamp), ) @@ -295,7 +295,7 @@ impl IVestingWallet for VestingWallet { .balance_of(call, contract::address()) .expect("should return the balance"); - self._vesting_schedule( + self.vesting_schedule( balance + self.released_erc20(token), U64::from(timestamp), ) @@ -306,11 +306,7 @@ impl VestingWallet { /// Virtual implementation of the vesting formula. This returns the amount /// vested, as a function of time, for an asset given its total /// historical allocation. - fn _vesting_schedule( - &self, - total_allocation: U256, - timestamp: U64, - ) -> U256 { + fn vesting_schedule(&self, total_allocation: U256, timestamp: U64) -> U256 { if U256::from(timestamp) < self.start() { U256::ZERO } else if U256::from(timestamp) >= self.end() { @@ -384,12 +380,12 @@ mod tests { assert_eq!( U256::ZERO, - contract._vesting_schedule(two, start - U64::from(1)) + contract.vesting_schedule(two, start - U64::from(1)) ); assert_eq!( one, - contract._vesting_schedule(two, start + duration / U64::from(2)) + contract.vesting_schedule(two, start + duration / U64::from(2)) ); - assert_eq!(two, contract._vesting_schedule(two, start + duration)); + assert_eq!(two, contract.vesting_schedule(two, start + duration)); } } From 2e76cf6e54d90f77fd2dd9c220099b8cd00e9efa Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 11 Nov 2024 14:34:54 +0100 Subject: [PATCH 069/138] fix(tests): add missing IVestingWallet import --- contracts/src/finance/vesting_wallet.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 78b3ae969..dfe3875e8 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -323,7 +323,7 @@ mod tests { use alloy_primitives::{address, uint, Address, U256, U64}; use stylus_sdk::block; - use super::VestingWallet; + use super::{IVestingWallet, VestingWallet}; const TOKEN: Address = address!("A11CEacF9aa32246d767FCCD72e02d6bCbcC375d"); const DURATION: u64 = 4 * 365 * 86400; // 4 years From b71170c5b02044a74cc02a537fddd063fb322781 Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 11 Nov 2024 14:36:45 +0100 Subject: [PATCH 070/138] ref: update err msg in release_erc20 --- contracts/src/finance/vesting_wallet.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index dfe3875e8..3a5dcc3d9 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -263,7 +263,7 @@ impl IVestingWallet for VestingWallet { let released = self .released_erc20(token) .checked_add(amount) - .expect("should not exceed `U256::MAX` for `_erc20Released`"); + .expect("should not exceed `U256::MAX` for `_erc20_released`"); self._erc20_released.setter(token).set(released); evm::log(ERC20Released { token, amount }); From 166b7c9d9e6ea436e924ea734f747f9ff5e3d698 Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 11 Nov 2024 16:59:47 +0100 Subject: [PATCH 071/138] fix: docs --- contracts/src/finance/vesting_wallet.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 3a5dcc3d9..df5989dc8 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -1,6 +1,7 @@ //! A vesting wallet is an ownable contract that can receive native currency and -//! ERC20 tokens, and release these assets to the wallet owner, also referred to -//! as "beneficiary", according to a vesting schedule. +//! [`crate::token::erc20::Erc20`] tokens, and release these assets to the +//! wallet owner, also referred to as "beneficiary", according to a vesting +//! schedule. //! //! Any assets transferred to this contract will follow the vesting schedule as //! if they were locked from the beginning. Consequently, if the vesting has @@ -35,10 +36,15 @@ use crate::access::{ownable, ownable::Ownable}; sol! { /// Emitted when `amount` of ether has been released. + /// + /// * `amount` - Total ether released. #[allow(missing_docs)] event EtherReleased(uint256 amount); /// Emitted when `amount` of ERC20 `token` has been released. + /// + /// * `token` - Address of the token being released. + /// * `amount` - Number of tokens released. #[allow(missing_docs)] event ERC20Released(address indexed token, uint256 amount); } @@ -83,7 +89,7 @@ pub enum Error { } sol_storage! { - /// State of a VestingWallet Contract. + /// State of the [`VestingWallet`] Contract. pub struct VestingWallet { /// Ownable contract Ownable ownable; From a09a27396ed2095084d35f8011f946ce6861cc9a Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 11 Nov 2024 17:01:06 +0100 Subject: [PATCH 072/138] fix: return Self::Error in IVestingWallet trait fns --- contracts/src/finance/vesting_wallet.rs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index df5989dc8..8fbc0ce74 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -133,7 +133,10 @@ pub trait IVestingWallet { /// /// If `new_owner` is the zero address, then the error /// [`OwnableInvalidOwner`] is returned. - fn transfer_ownership(&mut self, new_owner: Address) -> Result<(), Error>; + fn transfer_ownership( + &mut self, + new_owner: Address, + ) -> Result<(), Self::Error>; /// Leaves the contract without owner. It will not be possible to call /// [`Self::only_owner`] functions. Can only be called by the current owner. @@ -145,7 +148,7 @@ pub trait IVestingWallet { /// /// If not called by the owner, then the error /// [`Error::UnauthorizedAccount`] is returned. - fn renounce_ownership(&mut self) -> Result<(), Error>; + fn renounce_ownership(&mut self) -> Result<(), Self::Error>; /// Getter for the start timestamp. fn start(&self) -> U256; @@ -177,13 +180,13 @@ pub trait IVestingWallet { /// /// Emits an [`EtherReleased`] event. #[selector(name = "release")] - fn release_eth(&mut self) -> Result<(), Error>; + fn release_eth(&mut self) -> Result<(), Self::Error>; /// Release the tokens that have already vested. /// /// Emits an [`ERC20Released`] event. #[selector(name = "release")] - fn release_erc20(&mut self, token: Address) -> Result<(), Error>; + fn release_erc20(&mut self, token: Address) -> Result<(), Self::Error>; /// Calculates the amount of ether that has already vested. Default /// implementation is a linear vesting curve. @@ -207,11 +210,14 @@ impl IVestingWallet for VestingWallet { self.ownable.owner() } - fn transfer_ownership(&mut self, new_owner: Address) -> Result<(), Error> { + fn transfer_ownership( + &mut self, + new_owner: Address, + ) -> Result<(), Self::Error> { Ok(self.ownable.transfer_ownership(new_owner)?) } - fn renounce_ownership(&mut self) -> Result<(), Error> { + fn renounce_ownership(&mut self) -> Result<(), Self::Error> { Ok(self.ownable.renounce_ownership()?) } @@ -249,7 +255,7 @@ impl IVestingWallet for VestingWallet { } #[selector(name = "release")] - fn release_eth(&mut self) -> Result<(), Error> { + fn release_eth(&mut self) -> Result<(), Self::Error> { let amount = self.releasable_eth(); let released = self .released_eth() @@ -264,7 +270,7 @@ impl IVestingWallet for VestingWallet { } #[selector(name = "release")] - fn release_erc20(&mut self, token: Address) -> Result<(), Error> { + fn release_erc20(&mut self, token: Address) -> Result<(), Self::Error> { let amount = self.releasable_erc20(token); let released = self .released_erc20(token) From 76c3edd56210a1a292103662521f7cdf7b365a28 Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 11 Nov 2024 17:20:31 +0100 Subject: [PATCH 073/138] fix: add missing comment sections (partial) --- contracts/src/finance/vesting_wallet.rs | 107 +++++++++++++++++++++++- 1 file changed, 103 insertions(+), 4 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 8fbc0ce74..55312ef2e 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -119,11 +119,19 @@ pub trait IVestingWallet { fn receive_ether(&self); /// Returns the address of the current owner. + /// + /// Re-export of [`Ownable::owner`]. + /// + /// # Arguments + /// + /// * `&self` - Read access to the contract's state. fn owner(&self) -> Address; /// Transfers ownership of the contract to a new account (`new_owner`). Can /// only be called by the current owner. /// + /// Re-export of [`Ownable::transfer_ownership`]. + /// /// # Arguments /// /// * `&mut self` - Write access to the contract's state. @@ -131,8 +139,14 @@ pub trait IVestingWallet { /// /// # Errors /// + /// If called by any account other than the owner, then the error + /// [`ownable::Error::UnauthorizedAccount`] is returned. /// If `new_owner` is the zero address, then the error - /// [`OwnableInvalidOwner`] is returned. + /// [`ownable::Error::InvalidOwner`] is returned. + /// + /// # Events + /// + /// Emits a [`ownable::OwnershipTransferred`] event. fn transfer_ownership( &mut self, new_owner: Address, @@ -141,60 +155,136 @@ pub trait IVestingWallet { /// Leaves the contract without owner. It will not be possible to call /// [`Self::only_owner`] functions. Can only be called by the current owner. /// + /// Re-export of [`Ownable::renounce_ownership`]. + /// /// NOTE: Renouncing ownership will leave the contract without an owner, /// thereby disabling any functionality that is only available to the owner. /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// /// # Errors /// /// If not called by the owner, then the error - /// [`Error::UnauthorizedAccount`] is returned. + /// [`ownable::Error::UnauthorizedAccount`] is returned. + /// + /// # Events + /// + /// Emits a [`ownable::OwnershipTransferred`] event. fn renounce_ownership(&mut self) -> Result<(), Self::Error>; /// Getter for the start timestamp. + /// + /// # Arguments + /// + /// * `&self` - Read access to the contract's state. fn start(&self) -> U256; /// Getter for the vesting duration. + /// + /// # Arguments + /// + /// * `&self` - Read access to the contract's state. fn duration(&self) -> U256; /// Getter for the end timestamp. + /// + /// # Arguments + /// + /// * `&self` - Read access to the contract's state. + /// + /// # Panics + /// + /// If end time exceeds `U256::MAX`. fn end(&self) -> U256; - /// Amount of eth already released + /// Amount of eth already released. + /// + /// # Arguments + /// + /// * `&self` - Read access to the contract's state. #[selector(name = "released")] fn released_eth(&self) -> U256; - /// Amount of token already released + /// Amount of token already released. + /// + /// # Arguments + /// + /// * `&self` - Read access to the contract's state. + /// * `token` - Address of the token being released. #[selector(name = "released")] fn released_erc20(&self, token: Address) -> U256; /// Getter for the amount of releasable eth. + /// + /// # Arguments + /// + /// * `&self` - Read access to the contract's state. #[selector(name = "releasable")] fn releasable_eth(&self) -> U256; /// Getter for the amount of releasable `token` tokens. `token` should be /// the address of an [`crate::token::erc20::Erc20`] contract. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `token` - Address of the releasable token. #[selector(name = "releasable")] fn releasable_erc20(&mut self, token: Address) -> U256; /// Release the native token (ether) that have already vested. /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// + /// # Events + /// /// Emits an [`EtherReleased`] event. #[selector(name = "release")] fn release_eth(&mut self) -> Result<(), Self::Error>; /// Release the tokens that have already vested. /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `token` - Address of the token being released. + /// + /// # Events + /// /// Emits an [`ERC20Released`] event. #[selector(name = "release")] fn release_erc20(&mut self, token: Address) -> Result<(), Self::Error>; /// Calculates the amount of ether that has already vested. Default /// implementation is a linear vesting curve. + /// + /// # Arguments + /// + /// * `&self` - Read access to the contract's state. + /// * `timestamp` - Point in time for which to check the vested amount. + /// + /// # Panics + /// + /// If total allocation exceeds `U256::MAX`. #[selector(name = "vestedAmount")] fn vested_amount_eth(&self, timestamp: u64) -> U256; /// Calculates the amount of tokens that has already vested. Default /// implementation is a linear vesting curve. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `token` - Address of the token being released. + /// * `timestamp` - Point in time for which to check the vested amount. + /// + /// # Panics + /// + /// If total allocation exceeds `U256::MAX`. #[selector(name = "vestedAmount")] fn vested_amount_erc20(&mut self, token: Address, timestamp: u64) -> U256; } @@ -229,6 +319,7 @@ impl IVestingWallet for VestingWallet { U256::from(self._duration.get()) } + // TODO: checked_add fn end(&self) -> U256 { self.start() + self.duration() } @@ -243,11 +334,13 @@ impl IVestingWallet for VestingWallet { self._erc20_released.get(token) } + // TODO: checked_sub #[selector(name = "releasable")] fn releasable_eth(&self) -> U256 { self.vested_amount_eth(block::timestamp()) - self.released_eth() } + // TODO: checked_sub #[selector(name = "releasable")] fn releasable_erc20(&mut self, token: Address) -> U256 { self.vested_amount_erc20(token, block::timestamp()) @@ -291,6 +384,7 @@ impl IVestingWallet for VestingWallet { Ok(()) } + // TODO: checked_add #[selector(name = "vestedAmount")] fn vested_amount_eth(&self, timestamp: u64) -> U256 { self.vesting_schedule( @@ -299,6 +393,8 @@ impl IVestingWallet for VestingWallet { ) } + // TODO: checked_add + // TODO: use RawCall to remove the need for &mut self #[selector(name = "vestedAmount")] fn vested_amount_erc20(&mut self, token: Address, timestamp: u64) -> U256 { let erc20 = IErc20::new(token); @@ -315,6 +411,9 @@ impl IVestingWallet for VestingWallet { } impl VestingWallet { + // TODO: checked_mul + // TODO: unchecked_sub + // TODO: unchecked_div? /// Virtual implementation of the vesting formula. This returns the amount /// vested, as a function of time, for an asset given its total /// historical allocation. From a23291c9728c1184e7812eed026f1fc48ab8d85d Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 12 Nov 2024 07:58:17 +0100 Subject: [PATCH 074/138] ref: remove redundant /// in nonces --- contracts/src/utils/nonces.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/utils/nonces.rs b/contracts/src/utils/nonces.rs index 752565174..13678fcc3 100644 --- a/contracts/src/utils/nonces.rs +++ b/contracts/src/utils/nonces.rs @@ -52,7 +52,7 @@ impl Nonces { /// * `&mut self` - Write access to the contract's state. /// * `owner` - The address for which to consume the nonce. /// - /// /// # Panics + /// # Panics /// /// This function will panic if the nonce for the given `owner` has reached /// the maximum value representable by `U256`, causing the `checked_add` From e2b5cd99ad0b4d4053d04fe28c7b504da21339a5 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 12 Nov 2024 08:00:36 +0100 Subject: [PATCH 075/138] ref: use unchecked checked_add in end() --- contracts/src/finance/vesting_wallet.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 55312ef2e..3e2dcfad6 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -146,7 +146,7 @@ pub trait IVestingWallet { /// /// # Events /// - /// Emits a [`ownable::OwnershipTransferred`] event. + /// Emits an [`ownable::OwnershipTransferred`] event. fn transfer_ownership( &mut self, new_owner: Address, @@ -171,7 +171,7 @@ pub trait IVestingWallet { /// /// # Events /// - /// Emits a [`ownable::OwnershipTransferred`] event. + /// Emits an [`ownable::OwnershipTransferred`] event. fn renounce_ownership(&mut self) -> Result<(), Self::Error>; /// Getter for the start timestamp. @@ -193,10 +193,6 @@ pub trait IVestingWallet { /// # Arguments /// /// * `&self` - Read access to the contract's state. - /// - /// # Panics - /// - /// If end time exceeds `U256::MAX`. fn end(&self) -> U256; /// Amount of eth already released. @@ -319,9 +315,9 @@ impl IVestingWallet for VestingWallet { U256::from(self._duration.get()) } - // TODO: checked_add fn end(&self) -> U256 { - self.start() + self.duration() + // SAFETY: both `start` and `duration` are stored as u64 + unsafe { self.start().checked_add(self.duration()).unwrap_unchecked() } } #[selector(name = "released")] From 40702469afdcc07905a19a02aef57c130e1d7b9a Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 12 Nov 2024 08:11:51 +0100 Subject: [PATCH 076/138] docs: add missing args section to vesting_schedule --- contracts/src/finance/vesting_wallet.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 3e2dcfad6..00eeadb6b 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -413,6 +413,12 @@ impl VestingWallet { /// Virtual implementation of the vesting formula. This returns the amount /// vested, as a function of time, for an asset given its total /// historical allocation. + /// + /// # Arguments + /// + /// * `&self` - Read access to the contract's state. + /// * `total_allocation` - Total vested amount. + /// * `timestamp` - Point in time for which to calculate the vested amount. fn vesting_schedule(&self, total_allocation: U256, timestamp: U64) -> U256 { if U256::from(timestamp) < self.start() { U256::ZERO From 192a5dd4f0ab529559dc7253a65d7fc42a45f8bb Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 12 Nov 2024 08:41:40 +0100 Subject: [PATCH 077/138] ref: vesting_schedule --- contracts/src/finance/vesting_wallet.rs | 32 +++++++++++++++++++------ 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 00eeadb6b..133c0d7f9 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -407,9 +407,6 @@ impl IVestingWallet for VestingWallet { } impl VestingWallet { - // TODO: checked_mul - // TODO: unchecked_sub - // TODO: unchecked_div? /// Virtual implementation of the vesting formula. This returns the amount /// vested, as a function of time, for an asset given its total /// historical allocation. @@ -419,14 +416,35 @@ impl VestingWallet { /// * `&self` - Read access to the contract's state. /// * `total_allocation` - Total vested amount. /// * `timestamp` - Point in time for which to calculate the vested amount. + /// + /// # Panics + /// + /// If `total_allocation * (timestamp - self.start())` exceeds `U256::MAX`. fn vesting_schedule(&self, total_allocation: U256, timestamp: U64) -> U256 { - if U256::from(timestamp) < self.start() { + let timestamp = U256::from(timestamp); + + if timestamp < self.start() { U256::ZERO - } else if U256::from(timestamp) >= self.end() { + } else if timestamp >= self.end() { total_allocation } else { - (total_allocation * (U256::from(timestamp) - self.start())) - / self.duration() + // SAFETY: `timestamp` is guaranteed to be greater than + // `self.start()`, as checked by earlier bounds. + // SAFETY: `self.duration()` is non-zero. If `self.duration()` were + // zero, then `end == start`, meaning that `timestamp >= + // self.end()` and the function would have returned + // earlier. + unsafe { + let elapsed = + timestamp.checked_sub(self.start()).unwrap_unchecked(); + let scaled_allocation = total_allocation + .checked_mul(elapsed) + .expect("allocation overflow: exceeds `U256::MAX`"); + + scaled_allocation + .checked_div(self.duration()) + .unwrap_unchecked() + } } } } From a6840ac429fcdf77aa90dd10702096a41686e99d Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 12 Nov 2024 08:52:43 +0100 Subject: [PATCH 078/138] ref: use checked_add for vested_amount + release fns --- contracts/src/finance/vesting_wallet.rs | 34 +++++++++++++++---------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 133c0d7f9..6849468f9 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -63,6 +63,7 @@ sol! { error ReleaseTokenFailed(address token); } +// TODO: use existing IErc20 pub use token::IErc20; #[allow(missing_docs)] mod token { @@ -361,10 +362,13 @@ impl IVestingWallet for VestingWallet { #[selector(name = "release")] fn release_erc20(&mut self, token: Address) -> Result<(), Self::Error> { let amount = self.releasable_erc20(token); - let released = self - .released_erc20(token) - .checked_add(amount) - .expect("should not exceed `U256::MAX` for `_erc20_released`"); + + // SAFETY: total supply of a [`crate::token::erc20::Erc20`] cannot + // exceed `U256::MAX`. + let released = unsafe { + self.released_erc20(token).checked_add(amount).unwrap_unchecked() + }; + self._erc20_released.setter(token).set(released); evm::log(ERC20Released { token, amount }); @@ -380,16 +384,15 @@ impl IVestingWallet for VestingWallet { Ok(()) } - // TODO: checked_add #[selector(name = "vestedAmount")] fn vested_amount_eth(&self, timestamp: u64) -> U256 { - self.vesting_schedule( - contract::balance() + self.released_eth(), - U64::from(timestamp), - ) + let total_allocation = contract::balance() + .checked_add(self.released_eth()) + .expect("total allocation should not exceed `U256::MAX`"); + + self.vesting_schedule(total_allocation, U64::from(timestamp)) } - // TODO: checked_add // TODO: use RawCall to remove the need for &mut self #[selector(name = "vestedAmount")] fn vested_amount_erc20(&mut self, token: Address, timestamp: u64) -> U256 { @@ -399,10 +402,13 @@ impl IVestingWallet for VestingWallet { .balance_of(call, contract::address()) .expect("should return the balance"); - self.vesting_schedule( - balance + self.released_erc20(token), - U64::from(timestamp), - ) + // SAFETY: total supply of a [`crate::token::erc20::Erc20`] cannot + // exceed `U256::MAX`. + let total_allocation = unsafe { + balance.checked_add(self.released_erc20(token)).unwrap_unchecked() + }; + + self.vesting_schedule(total_allocation, U64::from(timestamp)) } } From 37651f82725c80b7c86e9e6b6c8b72021fd49444 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 12 Nov 2024 09:00:02 +0100 Subject: [PATCH 079/138] ref: remove redundant unsafe sections --- contracts/src/finance/vesting_wallet.rs | 46 ++++++++++--------------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 6849468f9..5ef071a99 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -32,7 +32,10 @@ use stylus_sdk::{ stylus_proc::{public, sol_storage, SolidityError}, }; -use crate::access::{ownable, ownable::Ownable}; +use crate::{ + access::{ownable, ownable::Ownable}, + utils::math::storage::AddAssignUnchecked, +}; sol! { /// Emitted when `amount` of ether has been released. @@ -317,8 +320,9 @@ impl IVestingWallet for VestingWallet { } fn end(&self) -> U256 { - // SAFETY: both `start` and `duration` are stored as u64 - unsafe { self.start().checked_add(self.duration()).unwrap_unchecked() } + // SAFETY: both `start` and `duration` are stored as u64, so they cannot + // exceed `U256::MAX` + self.start() + self.duration() } #[selector(name = "released")] @@ -365,11 +369,7 @@ impl IVestingWallet for VestingWallet { // SAFETY: total supply of a [`crate::token::erc20::Erc20`] cannot // exceed `U256::MAX`. - let released = unsafe { - self.released_erc20(token).checked_add(amount).unwrap_unchecked() - }; - - self._erc20_released.setter(token).set(released); + self._erc20_released.setter(token).add_assign_unchecked(amount); evm::log(ERC20Released { token, amount }); @@ -404,11 +404,10 @@ impl IVestingWallet for VestingWallet { // SAFETY: total supply of a [`crate::token::erc20::Erc20`] cannot // exceed `U256::MAX`. - let total_allocation = unsafe { - balance.checked_add(self.released_erc20(token)).unwrap_unchecked() - }; - - self.vesting_schedule(total_allocation, U64::from(timestamp)) + self.vesting_schedule( + balance + self.released_erc20(token), + U64::from(timestamp), + ) } } @@ -436,21 +435,14 @@ impl VestingWallet { } else { // SAFETY: `timestamp` is guaranteed to be greater than // `self.start()`, as checked by earlier bounds. + let scaled_allocation = total_allocation + .checked_mul(timestamp - self.start()) + .expect("allocation overflow: exceeds `U256::MAX`"); + // SAFETY: `self.duration()` is non-zero. If `self.duration()` were - // zero, then `end == start`, meaning that `timestamp >= - // self.end()` and the function would have returned - // earlier. - unsafe { - let elapsed = - timestamp.checked_sub(self.start()).unwrap_unchecked(); - let scaled_allocation = total_allocation - .checked_mul(elapsed) - .expect("allocation overflow: exceeds `U256::MAX`"); - - scaled_allocation - .checked_div(self.duration()) - .unwrap_unchecked() - } + // zero, then `end == start`, meaning that `timestamp >= self.end()` + // and the function would have returned earlier. + scaled_allocation / self.duration() } } } From c7e09cb22c96855c42548ab562307ae77ef95917 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 12 Nov 2024 09:03:40 +0100 Subject: [PATCH 080/138] docs: add missing SAFETY comments to releasable --- contracts/src/finance/vesting_wallet.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 5ef071a99..ae58ce4ca 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -335,15 +335,17 @@ impl IVestingWallet for VestingWallet { self._erc20_released.get(token) } - // TODO: checked_sub #[selector(name = "releasable")] fn releasable_eth(&self) -> U256 { + // SAFETY: total vested amount is by definition greater than or equal to + // the vested and released amount. self.vested_amount_eth(block::timestamp()) - self.released_eth() } - // TODO: checked_sub #[selector(name = "releasable")] fn releasable_erc20(&mut self, token: Address) -> U256 { + // SAFETY: total vested amount is by definition greater than or equal to + // the vested and released amount. self.vested_amount_erc20(token, block::timestamp()) - self.released_erc20(token) } From be198438aa09a90644e10d51aca29231cdfb6f91 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 12 Nov 2024 10:16:49 +0100 Subject: [PATCH 081/138] ref: use SafeErc20 for transfer + RawCall for balanceOf --- contracts/src/finance/vesting_wallet.rs | 83 ++++++++++++++++--------- 1 file changed, 52 insertions(+), 31 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index ae58ce4ca..81ffb6a22 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -22,18 +22,20 @@ //! adjustment in the vesting schedule to ensure the vested amount is as //! intended. use alloy_primitives::{Address, U256, U64}; -use alloy_sol_types::sol; +use alloy_sol_types::{sol, SolCall}; use openzeppelin_stylus_proc::interface_id; use stylus_sdk::{ - block, call, - call::{transfer_eth, Call}, + block, + call::{self, transfer_eth, RawCall}, contract, evm, function_selector, storage::TopLevelStorage, stylus_proc::{public, sol_storage, SolidityError}, + types::AddressVM, }; use crate::{ - access::{ownable, ownable::Ownable}, + access::ownable::{self, Ownable}, + token::erc20::utils::safe_erc20::{ISafeErc20, SafeErc20}, utils::math::storage::AddAssignUnchecked, }; @@ -64,13 +66,20 @@ sol! { #[derive(Debug)] #[allow(missing_docs)] error ReleaseTokenFailed(address token); + + /// The token address is not valid. (eg. `Address::ZERO`) + /// + /// * `token` - Address of the token being released. + #[derive(Debug)] + #[allow(missing_docs)] + error InvalidToken(address token); } // TODO: use existing IErc20 pub use token::IErc20; #[allow(missing_docs)] mod token { - stylus_sdk::stylus_proc::sol_interface! { + alloy_sol_types::sol! { /// Interface of the ERC-20 token. interface IErc20 { function balanceOf(address account) external view returns (uint256); @@ -90,6 +99,8 @@ pub enum Error { ReleaseEtherFailed(ReleaseEtherFailed), /// Indicates an error related to the underlying [`IErc20`] transfer. ReleaseTokenFailed(ReleaseTokenFailed), + /// The token address is not valid. (eg. `Address::ZERO`) + InvalidToken(InvalidToken), } sol_storage! { @@ -97,6 +108,8 @@ sol_storage! { pub struct VestingWallet { /// Ownable contract Ownable ownable; + /// SafeErc20 contract + SafeErc20 safe_erc20; /// Amount of eth already released. uint256 _released; /// Amount of ERC20 tokens already released. @@ -229,10 +242,10 @@ pub trait IVestingWallet { /// /// # Arguments /// - /// * `&mut self` - Write access to the contract's state. + /// * `&self` - Read access to the contract's state. /// * `token` - Address of the releasable token. #[selector(name = "releasable")] - fn releasable_erc20(&mut self, token: Address) -> U256; + fn releasable_erc20(&self, token: Address) -> Result; /// Release the native token (ether) that have already vested. /// @@ -278,7 +291,7 @@ pub trait IVestingWallet { /// /// # Arguments /// - /// * `&mut self` - Write access to the contract's state. + /// * `&self` - Read access to the contract's state. /// * `token` - Address of the token being released. /// * `timestamp` - Point in time for which to check the vested amount. /// @@ -286,7 +299,11 @@ pub trait IVestingWallet { /// /// If total allocation exceeds `U256::MAX`. #[selector(name = "vestedAmount")] - fn vested_amount_erc20(&mut self, token: Address, timestamp: u64) -> U256; + fn vested_amount_erc20( + &self, + token: Address, + timestamp: u64, + ) -> Result; } #[public] @@ -343,11 +360,11 @@ impl IVestingWallet for VestingWallet { } #[selector(name = "releasable")] - fn releasable_erc20(&mut self, token: Address) -> U256 { + fn releasable_erc20(&self, token: Address) -> Result { + let vested = self.vested_amount_erc20(token, block::timestamp())?; // SAFETY: total vested amount is by definition greater than or equal to // the vested and released amount. - self.vested_amount_erc20(token, block::timestamp()) - - self.released_erc20(token) + Ok(vested - self.released_erc20(token)) } #[selector(name = "release")] @@ -367,7 +384,12 @@ impl IVestingWallet for VestingWallet { #[selector(name = "release")] fn release_erc20(&mut self, token: Address) -> Result<(), Self::Error> { - let amount = self.releasable_erc20(token); + let amount = self.releasable_erc20(token)?; + let owner = self.ownable.owner(); + + self.safe_erc20.safe_transfer(token, owner, amount).map_err(|_| { + Error::ReleaseTokenFailed(ReleaseTokenFailed { token }) + })?; // SAFETY: total supply of a [`crate::token::erc20::Erc20`] cannot // exceed `U256::MAX`. @@ -375,14 +397,6 @@ impl IVestingWallet for VestingWallet { evm::log(ERC20Released { token, amount }); - let erc20 = IErc20::new(token); - let owner = self.ownable.owner(); - let call = Call::new_in(self); - let succeeded = erc20.transfer(call, owner, amount)?; - if !succeeded { - return Err(ReleaseTokenFailed { token }.into()); - } - Ok(()) } @@ -395,21 +409,28 @@ impl IVestingWallet for VestingWallet { self.vesting_schedule(total_allocation, U64::from(timestamp)) } - // TODO: use RawCall to remove the need for &mut self #[selector(name = "vestedAmount")] - fn vested_amount_erc20(&mut self, token: Address, timestamp: u64) -> U256 { - let erc20 = IErc20::new(token); - let call = Call::new_in(self); - let balance = erc20 - .balance_of(call, contract::address()) + fn vested_amount_erc20( + &self, + token: Address, + timestamp: u64, + ) -> Result { + if !Address::has_code(&token) { + return Err(InvalidToken { token }.into()); + } + + let call = IErc20::balanceOfCall { account: contract::address() }; + let balance = RawCall::new() + .limit_return_data(0, 32) + .call(token, &call.abi_encode()) + .map(|b| U256::from_be_slice(&b)) .expect("should return the balance"); // SAFETY: total supply of a [`crate::token::erc20::Erc20`] cannot // exceed `U256::MAX`. - self.vesting_schedule( - balance + self.released_erc20(token), - U64::from(timestamp), - ) + let total_allocation = balance + self.released_erc20(token); + + Ok(self.vesting_schedule(total_allocation, U64::from(timestamp))) } } From c9762ce374c4f3becbde630e8ded113363a3556b Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 12 Nov 2024 11:49:33 +0100 Subject: [PATCH 082/138] fix: move SafeErc20 state to the bottom of the storage --- contracts/src/finance/vesting_wallet.rs | 4 ++-- scripts/e2e-tests.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 81ffb6a22..080f0bd60 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -108,8 +108,6 @@ sol_storage! { pub struct VestingWallet { /// Ownable contract Ownable ownable; - /// SafeErc20 contract - SafeErc20 safe_erc20; /// Amount of eth already released. uint256 _released; /// Amount of ERC20 tokens already released. @@ -118,6 +116,8 @@ sol_storage! { uint64 _start; /// Vesting duration. uint64 _duration; + /// SafeErc20 contract + SafeErc20 safe_erc20; } } diff --git a/scripts/e2e-tests.sh b/scripts/e2e-tests.sh index 87c4fe654..8aa413fea 100755 --- a/scripts/e2e-tests.sh +++ b/scripts/e2e-tests.sh @@ -9,4 +9,4 @@ cargo build --release --target wasm32-unknown-unknown -Z build-std=std,panic_abo export RPC_URL=http://localhost:8547 -cargo test --features std,e2e --test "*" +cargo test --features std,e2e --test "vesting-wallet" From 503d0f65e3931b2dee3ddb8d447c5b75ec6ba7f2 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 12 Nov 2024 12:04:34 +0100 Subject: [PATCH 083/138] docs: add missing comment sections --- contracts/src/finance/vesting_wallet.rs | 45 +++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 080f0bd60..d15064bb0 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -234,6 +234,11 @@ pub trait IVestingWallet { /// # Arguments /// /// * `&self` - Read access to the contract's state. + /// + /// # Panics + /// + /// If total allocation exceeds `U256::MAX`. + /// If `total_allocation * (timestamp - self.start())` exceeds `U256::MAX`. #[selector(name = "releasable")] fn releasable_eth(&self) -> U256; @@ -244,6 +249,15 @@ pub trait IVestingWallet { /// /// * `&self` - Read access to the contract's state. /// * `token` - Address of the releasable token. + /// + /// # Errors + /// + /// If the `token` address is not a contract, then the error + /// [`Error::InvalidToken`] is returned. + /// + /// # Panics + /// + /// If `total_allocation * (timestamp - self.start())` exceeds `U256::MAX`. #[selector(name = "releasable")] fn releasable_erc20(&self, token: Address) -> Result; @@ -253,9 +267,19 @@ pub trait IVestingWallet { /// /// * `&mut self` - Write access to the contract's state. /// + /// # Errors + /// + /// If ETH transfer fails, then the error [`Error::ReleaseEtherFailed`] is + /// returned. + /// /// # Events /// /// Emits an [`EtherReleased`] event. + /// + /// # Panics + /// + /// If total allocation exceeds `U256::MAX`. + /// If `total_allocation * (timestamp - self.start())` exceeds `U256::MAX`. #[selector(name = "release")] fn release_eth(&mut self) -> Result<(), Self::Error>; @@ -266,9 +290,20 @@ pub trait IVestingWallet { /// * `&mut self` - Write access to the contract's state. /// * `token` - Address of the token being released. /// + /// # Errors + /// + /// If the `token` address is not a contract, then the error + /// [`Error::InvalidToken`] is returned. + /// If the contract fails to execute the call, then the error + /// [`Error::ReleaseTokenFailed`] is returned. + /// /// # Events /// /// Emits an [`ERC20Released`] event. + /// + /// # Panics + /// + /// If `total_allocation * (timestamp - self.start())` exceeds `U256::MAX`. #[selector(name = "release")] fn release_erc20(&mut self, token: Address) -> Result<(), Self::Error>; @@ -283,6 +318,7 @@ pub trait IVestingWallet { /// # Panics /// /// If total allocation exceeds `U256::MAX`. + /// If `total_allocation * (timestamp - self.start())` exceeds `U256::MAX`. #[selector(name = "vestedAmount")] fn vested_amount_eth(&self, timestamp: u64) -> U256; @@ -295,9 +331,14 @@ pub trait IVestingWallet { /// * `token` - Address of the token being released. /// * `timestamp` - Point in time for which to check the vested amount. /// + /// # Errors + /// + /// If the `token` address is not a contract, then the error + /// [`Error::InvalidToken`] is returned. + /// /// # Panics /// - /// If total allocation exceeds `U256::MAX`. + /// If `total_allocation * (timestamp - self.start())` exceeds `U256::MAX`. #[selector(name = "vestedAmount")] fn vested_amount_erc20( &self, @@ -460,7 +501,7 @@ impl VestingWallet { // `self.start()`, as checked by earlier bounds. let scaled_allocation = total_allocation .checked_mul(timestamp - self.start()) - .expect("allocation overflow: exceeds `U256::MAX`"); + .expect("scaled allocation overflow: exceeds `U256::MAX`"); // SAFETY: `self.duration()` is non-zero. If `self.duration()` were // zero, then `end == start`, meaning that `timestamp >= self.end()` From 5de1394d4bb95e9b4ac8c124538ce0f8a2690026 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 12 Nov 2024 12:08:33 +0100 Subject: [PATCH 084/138] ref: rename reads_vesting_allocation->gets_vesting_schedule + extract elapsed --- contracts/src/finance/vesting_wallet.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index d15064bb0..aea5d1bf8 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -499,8 +499,10 @@ impl VestingWallet { } else { // SAFETY: `timestamp` is guaranteed to be greater than // `self.start()`, as checked by earlier bounds. + let elapsed = timestamp - self.start(); + let scaled_allocation = total_allocation - .checked_mul(timestamp - self.start()) + .checked_mul(elapsed) .expect("scaled allocation overflow: exceeds `U256::MAX`"); // SAFETY: `self.duration()` is non-zero. If `self.duration()` were @@ -562,7 +564,7 @@ mod tests { } #[motsu::test] - fn reads_vested_allocation(contract: VestingWallet) { + fn gets_vesting_schedule(contract: VestingWallet) { let start = start(); let duration = U64::from(DURATION); contract._start.set(start); From 9763ff426b6839a3308592f3502d6ceb4d3a5dc3 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 12 Nov 2024 12:15:09 +0100 Subject: [PATCH 085/138] ref: assert 0 vested amount when deployed --- examples/vesting-wallet/tests/vesting-wallet.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/examples/vesting-wallet/tests/vesting-wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs index e41c03ba1..4d84e5fb1 100644 --- a/examples/vesting-wallet/tests/vesting-wallet.rs +++ b/examples/vesting-wallet/tests/vesting-wallet.rs @@ -75,11 +75,17 @@ async fn constructs(alice: Account) -> eyre::Result<()> { let start = contract.start().call().await?.start; let duration = contract.duration().call().await?.duration; let end = contract.end().call().await?.end; + let vested_amount_eth = + contract.vestedAmount_0().call().await?.vestedAmount; + let vested_amount_erc20 = + contract.vestedAmount_1().call().await?.vestedAmount; assert_eq!(alice.address(), owner); assert_eq!(U256::from(start_timestamp), start); assert_eq!(U256::from(DURATION), duration); assert_eq!(U256::from(start_timestamp + DURATION), end); + assert_eq!(U256::ZERO, vested_amount_eth); + assert_eq!(U256::ZERO, vested_amount_erc20); Ok(()) } From da9fdf214bc8318d0de3bc81eed92a01b2bb2044 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 12 Nov 2024 12:15:27 +0100 Subject: [PATCH 086/138] Revert e2e-tests.sh --- scripts/e2e-tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/e2e-tests.sh b/scripts/e2e-tests.sh index 8aa413fea..87c4fe654 100755 --- a/scripts/e2e-tests.sh +++ b/scripts/e2e-tests.sh @@ -9,4 +9,4 @@ cargo build --release --target wasm32-unknown-unknown -Z build-std=std,panic_abo export RPC_URL=http://localhost:8547 -cargo test --features std,e2e --test "vesting-wallet" +cargo test --features std,e2e --test "*" From a7ac3ff0c4987e262816bd09f55b5c1859869688 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 12 Nov 2024 12:28:25 +0100 Subject: [PATCH 087/138] revert: assert 0 vested amount when deployed Refs 9763ff426b6839a3308592f3502d6ceb4d3a5dc3 --- examples/vesting-wallet/tests/vesting-wallet.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/examples/vesting-wallet/tests/vesting-wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs index 4d84e5fb1..e41c03ba1 100644 --- a/examples/vesting-wallet/tests/vesting-wallet.rs +++ b/examples/vesting-wallet/tests/vesting-wallet.rs @@ -75,17 +75,11 @@ async fn constructs(alice: Account) -> eyre::Result<()> { let start = contract.start().call().await?.start; let duration = contract.duration().call().await?.duration; let end = contract.end().call().await?.end; - let vested_amount_eth = - contract.vestedAmount_0().call().await?.vestedAmount; - let vested_amount_erc20 = - contract.vestedAmount_1().call().await?.vestedAmount; assert_eq!(alice.address(), owner); assert_eq!(U256::from(start_timestamp), start); assert_eq!(U256::from(DURATION), duration); assert_eq!(U256::from(start_timestamp + DURATION), end); - assert_eq!(U256::ZERO, vested_amount_eth); - assert_eq!(U256::ZERO, vested_amount_erc20); Ok(()) } From f5c1410c4ce280ecc0e629488c5acf4a09aabc0c Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 12 Nov 2024 12:40:56 +0100 Subject: [PATCH 088/138] fix: change Self::only_owner->Ownable::only_owner --- contracts/src/finance/vesting_wallet.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index aea5d1bf8..4fb4b92e8 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -170,7 +170,8 @@ pub trait IVestingWallet { ) -> Result<(), Self::Error>; /// Leaves the contract without owner. It will not be possible to call - /// [`Self::only_owner`] functions. Can only be called by the current owner. + /// [`Ownable::only_owner`] functions. Can only be called by the current + /// owner. /// /// Re-export of [`Ownable::renounce_ownership`]. /// From 5123c4d87bb5a3435a898c7e43c6970477667c2a Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 12 Nov 2024 20:46:01 +0100 Subject: [PATCH 089/138] feat: add additional unit tests for end() and vesting_schedule + update comments --- contracts/src/finance/vesting_wallet.rs | 36 +++++- .../vesting-wallet/tests/vesting-wallet.rs | 108 +++++++++--------- 2 files changed, 87 insertions(+), 57 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 4fb4b92e8..7730dc519 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -397,7 +397,7 @@ impl IVestingWallet for VestingWallet { #[selector(name = "releasable")] fn releasable_eth(&self) -> U256 { // SAFETY: total vested amount is by definition greater than or equal to - // the vested and released amount. + // the released amount. self.vested_amount_eth(block::timestamp()) - self.released_eth() } @@ -405,7 +405,7 @@ impl IVestingWallet for VestingWallet { fn releasable_erc20(&self, token: Address) -> Result { let vested = self.vested_amount_erc20(token, block::timestamp())?; // SAFETY: total vested amount is by definition greater than or equal to - // the vested and released amount. + // the released amount. Ok(vested - self.released_erc20(token)) } @@ -504,7 +504,7 @@ impl VestingWallet { let scaled_allocation = total_allocation .checked_mul(elapsed) - .expect("scaled allocation overflow: exceeds `U256::MAX`"); + .expect("scaled allocation exceeds `U256::MAX`"); // SAFETY: `self.duration()` is non-zero. If `self.duration()` were // zero, then `end == start`, meaning that `timestamp >= self.end()` @@ -550,6 +550,15 @@ mod tests { assert_eq!(U256::from(start + duration), contract.end()); } + #[motsu::test] + fn reads_max_end(contract: VestingWallet) { + let start = U64::MAX; + let duration = U64::MAX; + contract._start.set(start); + contract._duration.set(duration); + assert_eq!(U256::from(start + duration), contract.end()); + } + #[motsu::test] fn reads_released_eth(contract: VestingWallet) { let one = uint!(1_U256); @@ -583,5 +592,26 @@ mod tests { contract.vesting_schedule(two, start + duration / U64::from(2)) ); assert_eq!(two, contract.vesting_schedule(two, start + duration)); + assert_eq!( + two, + contract.vesting_schedule(two, start + duration + U64::from(1)) + ); + } + + #[motsu::test] + fn gets_vesting_schedule_zero_duration(contract: VestingWallet) { + let start = start(); + contract._start.set(start); + contract._duration.set(U64::ZERO); + + let one = uint!(1_U256); + let two = uint!(2_U256); + + assert_eq!( + U256::ZERO, + contract.vesting_schedule(two, start - U64::from(1)) + ); + assert_eq!(two, contract.vesting_schedule(two, start)); + assert_eq!(two, contract.vesting_schedule(two, start + U64::from(1))); } } diff --git a/examples/vesting-wallet/tests/vesting-wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs index e41c03ba1..d0c276573 100644 --- a/examples/vesting-wallet/tests/vesting-wallet.rs +++ b/examples/vesting-wallet/tests/vesting-wallet.rs @@ -159,6 +159,33 @@ mod ether_vesting { Ok(()) } + #[e2e::test] + async fn check_release_0_percent(alice: Account) -> eyre::Result<()> { + run_check_release(alice, 0).await + } + + #[e2e::test] + async fn check_release_25_percent(alice: Account) -> eyre::Result<()> { + run_check_release(alice, DURATION / 4).await + } + + #[e2e::test] + async fn check_release_50_percent(alice: Account) -> eyre::Result<()> { + run_check_release(alice, DURATION / 2).await + } + + #[e2e::test] + async fn check_release_100_percent(alice: Account) -> eyre::Result<()> { + run_check_release(alice, DURATION).await + } + + #[e2e::test] + async fn check_release_100_percent_vesting_in_the_past( + alice: Account, + ) -> eyre::Result<()> { + run_check_release(alice, DURATION * 4 / 3).await + } + #[e2e::test] async fn check_vesting_schedule(alice: Account) -> eyre::Result<()> { let start = block_timestamp(&alice).await?; @@ -189,33 +216,6 @@ mod ether_vesting { Ok(()) } - - #[e2e::test] - async fn check_release_0_percent(alice: Account) -> eyre::Result<()> { - run_check_release(alice, 0).await - } - - #[e2e::test] - async fn check_release_25_percent(alice: Account) -> eyre::Result<()> { - run_check_release(alice, DURATION / 4).await - } - - #[e2e::test] - async fn check_release_50_percent(alice: Account) -> eyre::Result<()> { - run_check_release(alice, DURATION / 2).await - } - - #[e2e::test] - async fn check_release_100_percent(alice: Account) -> eyre::Result<()> { - run_check_release(alice, DURATION).await - } - - #[e2e::test] - async fn check_release_100_percent_vesting_in_the_past( - alice: Account, - ) -> eyre::Result<()> { - run_check_release(alice, DURATION * 4 / 3).await - } } mod erc20_vesting { @@ -278,6 +278,33 @@ mod erc20_vesting { Ok(()) } + #[e2e::test] + async fn check_release_0_percent(alice: Account) -> eyre::Result<()> { + run_check_release(alice, 0).await + } + + #[e2e::test] + async fn check_release_25_percent(alice: Account) -> eyre::Result<()> { + run_check_release(alice, DURATION / 4).await + } + + #[e2e::test] + async fn check_release_50_percent(alice: Account) -> eyre::Result<()> { + run_check_release(alice, DURATION / 2).await + } + + #[e2e::test] + async fn check_release_100_percent(alice: Account) -> eyre::Result<()> { + run_check_release(alice, DURATION).await + } + + #[e2e::test] + async fn check_release_100_percent_vesting_in_the_past( + alice: Account, + ) -> eyre::Result<()> { + run_check_release(alice, DURATION * 4 / 3).await + } + #[e2e::test] async fn check_vesting_schedule(alice: Account) -> eyre::Result<()> { let start = block_timestamp(&alice).await?; @@ -313,31 +340,4 @@ mod erc20_vesting { Ok(()) } - - #[e2e::test] - async fn check_release_0_percent(alice: Account) -> eyre::Result<()> { - run_check_release(alice, 0).await - } - - #[e2e::test] - async fn check_release_25_percent(alice: Account) -> eyre::Result<()> { - run_check_release(alice, DURATION / 4).await - } - - #[e2e::test] - async fn check_release_50_percent(alice: Account) -> eyre::Result<()> { - run_check_release(alice, DURATION / 2).await - } - - #[e2e::test] - async fn check_release_100_percent(alice: Account) -> eyre::Result<()> { - run_check_release(alice, DURATION).await - } - - #[e2e::test] - async fn check_release_100_percent_vesting_in_the_past( - alice: Account, - ) -> eyre::Result<()> { - run_check_release(alice, DURATION * 4 / 3).await - } } From 8b6854ab20b56babfa5c1450b7a43142b841e4cc Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Tue, 12 Nov 2024 21:06:20 +0100 Subject: [PATCH 090/138] ref: extract common deployment logic for erc20 e2e tests --- .../vesting-wallet/tests/vesting-wallet.rs | 44 +++++++++++-------- scripts/e2e-tests.sh | 2 +- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/examples/vesting-wallet/tests/vesting-wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs index d0c276573..cab1cd6c1 100644 --- a/examples/vesting-wallet/tests/vesting-wallet.rs +++ b/examples/vesting-wallet/tests/vesting-wallet.rs @@ -221,6 +221,26 @@ mod ether_vesting { mod erc20_vesting { use super::*; + async fn deploy( + account: &Account, + start: u64, + duration: u64, + allocation: u64, + ) -> eyre::Result<(Address, Address)> { + let contract_addr = account + .as_deployer() + .with_constructor(ctr(account.address(), start, duration)) + .deploy() + .await? + .address()?; + + let erc20_address = erc20::deploy(&account.wallet).await?; + let erc20 = ERC20Mock::new(erc20_address, &account.wallet); + let _ = watch!(erc20.mint(contract_addr, U256::from(allocation)))?; + + Ok((contract_addr, erc20_address)) + } + async fn run_check_release( alice: Account, time_passed: u64, @@ -231,18 +251,11 @@ mod erc20_vesting { BALANCE, BALANCE * time_passed / DURATION, )); + let (contract_addr, erc20_address) = + deploy(&alice, start, DURATION, BALANCE).await?; - let contract_addr = alice - .as_deployer() - .with_constructor(ctr(alice.address(), start, DURATION)) - .deploy() - .await? - .address()?; let contract = VestingWallet::new(contract_addr, &alice.wallet); - - let erc20_address = erc20::deploy(&alice.wallet).await?; let erc20 = ERC20Mock::new(erc20_address, &alice.wallet); - let _ = watch!(erc20.mint(contract_addr, U256::from(BALANCE)))?; let old_alice_balance = erc20.balanceOf(alice.address()).call().await?.balance; @@ -308,17 +321,10 @@ mod erc20_vesting { #[e2e::test] async fn check_vesting_schedule(alice: Account) -> eyre::Result<()> { let start = block_timestamp(&alice).await?; - let contract_addr = alice - .as_deployer() - .with_constructor(ctr(alice.address(), start, DURATION)) - .deploy() - .await? - .address()?; - let contract = VestingWallet::new(contract_addr, &alice.wallet); + let (contract_addr, erc20_address) = + deploy(&alice, start, DURATION, BALANCE).await?; - let erc20_address = erc20::deploy(&alice.wallet).await?; - let erc20 = ERC20Mock::new(erc20_address, &alice.wallet); - let _ = watch!(erc20.mint(contract_addr, U256::from(BALANCE)))?; + let contract = VestingWallet::new(contract_addr, &alice.wallet); for i in 0..64 { let timestamp = i * DURATION / 60 + start; diff --git a/scripts/e2e-tests.sh b/scripts/e2e-tests.sh index 87c4fe654..8aa413fea 100755 --- a/scripts/e2e-tests.sh +++ b/scripts/e2e-tests.sh @@ -9,4 +9,4 @@ cargo build --release --target wasm32-unknown-unknown -Z build-std=std,panic_abo export RPC_URL=http://localhost:8547 -cargo test --features std,e2e --test "*" +cargo test --features std,e2e --test "vesting-wallet" From 5e3cddb0965859d44d920a4886edd0c5fd754c28 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Tue, 12 Nov 2024 13:05:55 +0100 Subject: [PATCH 091/138] build(deps): bump crate-ci/typos from 1.27.2 to 1.27.3 (#401) Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.27.2 to 1.27.3.
Release notes

Sourced from crate-ci/typos's releases.

v1.27.3

[1.27.3] - 2024-11-08

Fixes

  • Don't correct alloced
  • Don't correct registor, a more domain specific variant of register
Changelog

Sourced from crate-ci/typos's changelog.

[1.27.3] - 2024-11-08

Fixes

  • Don't correct alloced
  • Don't correct requestor, a more domain specific variant of requester
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=crate-ci/typos&package-manager=github_actions&previous-version=1.27.2&new-version=1.27.3)](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 0420c5ecc..8fe3e95f8 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -127,4 +127,4 @@ jobs: uses: actions/checkout@v4 - name: Check spelling of files in the workspace - uses: crate-ci/typos@v1.27.2 + uses: crate-ci/typos@v1.27.3 From 56653620e6c3d9995c9061a9e40c389c7f7fa519 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Wed, 13 Nov 2024 07:40:52 +0100 Subject: [PATCH 092/138] fix: cast to U256 earlier to avoid overflow in reads_max_end test --- contracts/src/finance/vesting_wallet.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 7730dc519..c100ad0f4 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -556,7 +556,7 @@ mod tests { let duration = U64::MAX; contract._start.set(start); contract._duration.set(duration); - assert_eq!(U256::from(start + duration), contract.end()); + assert_eq!(U256::from(start) + U256::from(duration), contract.end()); } #[motsu::test] From 2042642e22391118451f9cf6a5ba818a100d8430 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Wed, 13 Nov 2024 08:19:54 +0100 Subject: [PATCH 093/138] feat(test): add invalid token tests --- examples/vesting-wallet/tests/abi/mod.rs | 4 +- .../vesting-wallet/tests/vesting-wallet.rs | 59 ++++++++++++++++++- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/examples/vesting-wallet/tests/abi/mod.rs b/examples/vesting-wallet/tests/abi/mod.rs index 49f40b2a0..ed2244f4a 100644 --- a/examples/vesting-wallet/tests/abi/mod.rs +++ b/examples/vesting-wallet/tests/abi/mod.rs @@ -18,9 +18,11 @@ sol!( function vestedAmount(uint64 timestamp) external view returns (uint256 vestedAmount); function vestedAmount(address token, uint64 timestamp) external view returns (uint256 vestedAmount); + error OwnableUnauthorizedAccount(address account); error OwnableInvalidOwner(address owner); - + error ReleaseEtherFailed(); error ReleaseTokenFailed(address token); + error InvalidToken(address token); #[derive(Debug, PartialEq)] event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); diff --git a/examples/vesting-wallet/tests/vesting-wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs index cab1cd6c1..d0599e64e 100644 --- a/examples/vesting-wallet/tests/vesting-wallet.rs +++ b/examples/vesting-wallet/tests/vesting-wallet.rs @@ -7,7 +7,7 @@ use alloy::{ providers::Provider, sol, }; -use e2e::{receipt, watch, Account, EventExt, ReceiptExt, Revert}; +use e2e::{receipt, send, watch, Account, EventExt, ReceiptExt, Revert}; use mock::{erc20, erc20::ERC20Mock}; use crate::VestingWalletExample::constructorCall; @@ -346,4 +346,61 @@ mod erc20_vesting { Ok(()) } + + #[e2e::test] + async fn releasable_erc20_reverts_on_invalid_token( + alice: Account, + ) -> eyre::Result<()> { + let start = block_timestamp(&alice).await?; + let contract_addr = deploy(&alice, start, DURATION, BALANCE).await?.0; + + let contract = VestingWallet::new(contract_addr, &alice.wallet); + + let err = send!(contract.releasable_1(Address::ZERO)) + .expect_err("should not get releasable amount for invalid token"); + + assert!(err.reverted_with(VestingWallet::InvalidToken { + token: Address::ZERO + })); + + Ok(()) + } + + #[e2e::test] + async fn release_erc20_reverts_on_invalid_token( + alice: Account, + ) -> eyre::Result<()> { + let start = block_timestamp(&alice).await?; + let contract_addr = deploy(&alice, start, DURATION, BALANCE).await?.0; + + let contract = VestingWallet::new(contract_addr, &alice.wallet); + + let err = send!(contract.release_1(Address::ZERO)) + .expect_err("should not release for invalid token"); + + assert!(err.reverted_with(VestingWallet::InvalidToken { + token: Address::ZERO + })); + + Ok(()) + } + + #[e2e::test] + async fn vested_amount_erc20_reverts_on_invalid_token( + alice: Account, + ) -> eyre::Result<()> { + let start = block_timestamp(&alice).await?; + let contract_addr = deploy(&alice, start, DURATION, BALANCE).await?.0; + + let contract = VestingWallet::new(contract_addr, &alice.wallet); + + let err = send!(contract.vestedAmount_1(Address::ZERO, start)) + .expect_err("should not get vested amount for invalid token"); + + assert!(err.reverted_with(VestingWallet::InvalidToken { + token: Address::ZERO + })); + + Ok(()) + } } From 5e02d9f13bba52142848c0cc0504bf7c574d34e4 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Wed, 13 Nov 2024 08:27:47 +0100 Subject: [PATCH 094/138] feat(test): add release_erc20_reverts_on_failed_transfer --- .../tests/mock/erc20_return_false.rs | 44 +++++++++++++++++++ examples/vesting-wallet/tests/mock/mod.rs | 1 + .../vesting-wallet/tests/vesting-wallet.rs | 32 ++++++++++++++ 3 files changed, 77 insertions(+) create mode 100644 examples/vesting-wallet/tests/mock/erc20_return_false.rs diff --git a/examples/vesting-wallet/tests/mock/erc20_return_false.rs b/examples/vesting-wallet/tests/mock/erc20_return_false.rs new file mode 100644 index 000000000..809c80626 --- /dev/null +++ b/examples/vesting-wallet/tests/mock/erc20_return_false.rs @@ -0,0 +1,44 @@ +#![allow(dead_code)] +#![cfg(feature = "e2e")] +use alloy::{primitives::Address, sol}; +use e2e::Wallet; + +sol! { + #[allow(missing_docs)] + // Built with Remix IDE; solc v0.8.21+commit.d9974bed + #[sol(rpc, bytecode="608060405234801562000010575f80fd5b506040518060400160405280601481526020017f455243323052657475726e46616c73654d6f636b0000000000000000000000008152506040518060400160405280600381526020017f52464d000000000000000000000000000000000000000000000000000000000081525081600390816200008e91906200030d565b508060049081620000a091906200030d565b505050620003f1565b5f81519050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b5f60028204905060018216806200012557607f821691505b6020821081036200013b576200013a620000e0565b5b50919050565b5f819050815f5260205f209050919050565b5f6020601f8301049050919050565b5f82821b905092915050565b5f600883026200019f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8262000162565b620001ab868362000162565b95508019841693508086168417925050509392505050565b5f819050919050565b5f819050919050565b5f620001f5620001ef620001e984620001c3565b620001cc565b620001c3565b9050919050565b5f819050919050565b6200021083620001d5565b620002286200021f82620001fc565b8484546200016e565b825550505050565b5f90565b6200023e62000230565b6200024b81848462000205565b505050565b5b818110156200027257620002665f8262000234565b60018101905062000251565b5050565b601f821115620002c1576200028b8162000141565b620002968462000153565b81016020851015620002a6578190505b620002be620002b58562000153565b83018262000250565b50505b505050565b5f82821c905092915050565b5f620002e35f1984600802620002c6565b1980831691505092915050565b5f620002fd8383620002d2565b9150826002028217905092915050565b6200031882620000a9565b67ffffffffffffffff811115620003345762000333620000b3565b5b6200034082546200010d565b6200034d82828562000276565b5f60209050601f83116001811462000383575f84156200036e578287015190505b6200037a8582620002f0565b865550620003e9565b601f198416620003938662000141565b5f5b82811015620003bc5784890151825560018201915060208501945060208101905062000395565b86831015620003dc5784890151620003d8601f891682620002d2565b8355505b6001600288020188555050505b505050505050565b610b0d80620003ff5f395ff3fe608060405234801561000f575f80fd5b506004361061009c575f3560e01c806340c10f191161006457806340c10f191461015a57806370a082311461017657806395d89b41146101a6578063a9059cbb146101c4578063dd62ed3e146101f45761009c565b806306fdde03146100a0578063095ea7b3146100be57806318160ddd146100ee57806323b872dd1461010c578063313ce5671461013c575b5f80fd5b6100a8610224565b6040516100b59190610786565b60405180910390f35b6100d860048036038101906100d39190610837565b6102b4565b6040516100e5919061088f565b60405180910390f35b6100f66102bb565b60405161010391906108b7565b60405180910390f35b610126600480360381019061012191906108d0565b6102c4565b604051610133919061088f565b60405180910390f35b6101446102cc565b604051610151919061093b565b60405180910390f35b610174600480360381019061016f9190610837565b6102d4565b005b610190600480360381019061018b9190610954565b6102e2565b60405161019d91906108b7565b60405180910390f35b6101ae6102f3565b6040516101bb9190610786565b60405180910390f35b6101de60048036038101906101d99190610837565b610383565b6040516101eb919061088f565b60405180910390f35b61020e6004803603810190610209919061097f565b61038a565b60405161021b91906108b7565b60405180910390f35b606060038054610233906109ea565b80601f016020809104026020016040519081016040528092919081815260200182805461025f906109ea565b80156102aa5780601f10610281576101008083540402835291602001916102aa565b820191905f5260205f20905b81548152906001019060200180831161028d57829003601f168201915b5050505050905090565b5f92915050565b5f600254905090565b5f9392505050565b5f6012905090565b6102de828261039d565b5050565b5f6102ec8261041c565b9050919050565b606060048054610302906109ea565b80601f016020809104026020016040519081016040528092919081815260200182805461032e906109ea565b80156103795780601f1061035057610100808354040283529160200191610379565b820191905f5260205f20905b81548152906001019060200180831161035c57829003601f168201915b5050505050905090565b5f92915050565b5f6103958383610461565b905092915050565b5f73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff160361040d575f6040517fec442f050000000000000000000000000000000000000000000000000000000081526004016104049190610a29565b60405180910390fd5b6104185f83836104e3565b5050565b5f805f8373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f20549050919050565b5f60015f8473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f2054905092915050565b5f73ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1603610533578060025f8282546105279190610a6f565b92505081905550610601565b5f805f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f20549050818110156105bc578381836040517fe450d38c0000000000000000000000000000000000000000000000000000000081526004016105b393929190610aa2565b60405180910390fd5b8181035f808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f2081905550505b5f73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603610648578060025f8282540392505081905550610692565b805f808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f82825401925050819055505b8173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040516106ef91906108b7565b60405180910390a3505050565b5f81519050919050565b5f82825260208201905092915050565b5f5b83811015610733578082015181840152602081019050610718565b5f8484015250505050565b5f601f19601f8301169050919050565b5f610758826106fc565b6107628185610706565b9350610772818560208601610716565b61077b8161073e565b840191505092915050565b5f6020820190508181035f83015261079e818461074e565b905092915050565b5f80fd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6107d3826107aa565b9050919050565b6107e3816107c9565b81146107ed575f80fd5b50565b5f813590506107fe816107da565b92915050565b5f819050919050565b61081681610804565b8114610820575f80fd5b50565b5f813590506108318161080d565b92915050565b5f806040838503121561084d5761084c6107a6565b5b5f61085a858286016107f0565b925050602061086b85828601610823565b9150509250929050565b5f8115159050919050565b61088981610875565b82525050565b5f6020820190506108a25f830184610880565b92915050565b6108b181610804565b82525050565b5f6020820190506108ca5f8301846108a8565b92915050565b5f805f606084860312156108e7576108e66107a6565b5b5f6108f4868287016107f0565b9350506020610905868287016107f0565b925050604061091686828701610823565b9150509250925092565b5f60ff82169050919050565b61093581610920565b82525050565b5f60208201905061094e5f83018461092c565b92915050565b5f60208284031215610969576109686107a6565b5b5f610976848285016107f0565b91505092915050565b5f8060408385031215610995576109946107a6565b5b5f6109a2858286016107f0565b92505060206109b3858286016107f0565b9150509250929050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b5f6002820490506001821680610a0157607f821691505b602082108103610a1457610a136109bd565b5b50919050565b610a23816107c9565b82525050565b5f602082019050610a3c5f830184610a1a565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f610a7982610804565b9150610a8483610804565b9250828201905080821115610a9c57610a9b610a42565b5b92915050565b5f606082019050610ab55f830186610a1a565b610ac260208301856108a8565b610acf60408301846108a8565b94935050505056fea26469706673582212204aac6dd6254b82f37f30add0ed2937474eced0bafc505b611f66b99ebe39999e64736f6c63430008150033")] + // SPDX-License-Identifier: MIT + contract ERC20ReturnFalseMock is ERC20 { + constructor() ERC20("ERC20ReturnFalseMock", "RFM") {} + + function approve(address, uint256) public override returns (bool) { + return false; + } + + function transfer(address, uint256) public override returns (bool) { + return false; + } + + function transferFrom(address, address, uint256) public override returns (bool) { + return false; + } + + function balanceOf(address account) public override view returns (uint256) { + return super.balanceOf(account); + } + + function mint(address account, uint256 value) public { + super._mint(account, value); + } + + function allowance(address owner, address spender) public view override returns (uint256) { + return super.allowance(owner, spender); + } + } +} + +pub async fn deploy(wallet: &Wallet) -> eyre::Result
{ + // Deploy the contract. + let contract = ERC20ReturnFalseMock::deploy(wallet).await?; + Ok(*contract.address()) +} diff --git a/examples/vesting-wallet/tests/mock/mod.rs b/examples/vesting-wallet/tests/mock/mod.rs index 8f3777f6b..8d39a8309 100644 --- a/examples/vesting-wallet/tests/mock/mod.rs +++ b/examples/vesting-wallet/tests/mock/mod.rs @@ -1 +1,2 @@ pub mod erc20; +pub mod erc20_return_false; diff --git a/examples/vesting-wallet/tests/vesting-wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs index d0599e64e..9605fac1b 100644 --- a/examples/vesting-wallet/tests/vesting-wallet.rs +++ b/examples/vesting-wallet/tests/vesting-wallet.rs @@ -385,6 +385,38 @@ mod erc20_vesting { Ok(()) } + #[e2e::test] + async fn release_erc20_reverts_on_failed_transfer( + alice: Account, + ) -> eyre::Result<()> { + use mock::{ + erc20_return_false, erc20_return_false::ERC20ReturnFalseMock, + }; + + let start = block_timestamp(&alice).await?; + let contract_addr = alice + .as_deployer() + .with_constructor(ctr(alice.address(), start, DURATION)) + .deploy() + .await? + .address()?; + + let erc20_address = erc20_return_false::deploy(&alice.wallet).await?; + let erc20 = ERC20ReturnFalseMock::new(erc20_address, &alice.wallet); + let _ = watch!(erc20.mint(contract_addr, U256::from(BALANCE)))?; + + let contract = VestingWallet::new(contract_addr, &alice.wallet); + + let err = send!(contract.release_1(erc20_address)) + .expect_err("should not release when transfer fails"); + + assert!(err.reverted_with(VestingWallet::ReleaseTokenFailed { + token: erc20_address + })); + + Ok(()) + } + #[e2e::test] async fn vested_amount_erc20_reverts_on_invalid_token( alice: Account, From e54df17760b21a9091c13c4ee9e69c8a2afc6759 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Wed, 13 Nov 2024 08:32:08 +0100 Subject: [PATCH 095/138] fix: revert e2e-tests.sh --- scripts/e2e-tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/e2e-tests.sh b/scripts/e2e-tests.sh index 8aa413fea..87c4fe654 100755 --- a/scripts/e2e-tests.sh +++ b/scripts/e2e-tests.sh @@ -9,4 +9,4 @@ cargo build --release --target wasm32-unknown-unknown -Z build-std=std,panic_abo export RPC_URL=http://localhost:8547 -cargo test --features std,e2e --test "vesting-wallet" +cargo test --features std,e2e --test "*" From 254512ed04a011f2f10b81e5c78812a0db27134e Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 18 Nov 2024 07:23:10 +0100 Subject: [PATCH 096/138] docs: Apply comment updates from code review Co-authored-by: Daniel Bigos --- contracts/src/finance/mod.rs | 2 +- contracts/src/finance/vesting_wallet.rs | 18 +++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/contracts/src/finance/mod.rs b/contracts/src/finance/mod.rs index 0abc750be..f02a56459 100644 --- a/contracts/src/finance/mod.rs +++ b/contracts/src/finance/mod.rs @@ -1,2 +1,2 @@ -//! Primitives for financial systems +//! Primitives for financial systems. pub mod vesting_wallet; diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 31b68e330..b9ba3bfa4 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -46,7 +46,7 @@ sol! { #[allow(missing_docs)] event EtherReleased(uint256 amount); - /// Emitted when `amount` of ERC20 `token` has been released. + /// Emitted when `amount` of ERC-20 `token` has been released. /// /// * `token` - Address of the token being released. /// * `amount` - Number of tokens released. @@ -60,14 +60,14 @@ sol! { #[allow(missing_docs)] error ReleaseEtherFailed(); - /// Indicates an error related to the underlying ERC20 transfer. + /// Indicates an error related to the underlying ERC-20 transfer. /// /// * `token` - Address of the token being released. #[derive(Debug)] #[allow(missing_docs)] error ReleaseTokenFailed(address token); - /// The token address is not valid. (eg. `Address::ZERO`) + /// The token address is not valid. (eg. `Address::ZERO`). /// /// * `token` - Address of the token being released. #[derive(Debug)] @@ -99,24 +99,24 @@ pub enum Error { ReleaseEtherFailed(ReleaseEtherFailed), /// Indicates an error related to the underlying [`IErc20`] transfer. ReleaseTokenFailed(ReleaseTokenFailed), - /// The token address is not valid. (eg. `Address::ZERO`) + /// The token address is not valid. (eg. `Address::ZERO`). InvalidToken(InvalidToken), } sol_storage! { /// State of the [`VestingWallet`] Contract. pub struct VestingWallet { - /// Ownable contract + /// [`Ownable`] contract. Ownable ownable; /// Amount of eth already released. uint256 _released; - /// Amount of ERC20 tokens already released. + /// Amount of ERC-20 tokens already released. mapping(address => uint256) _erc20_released; /// Start timestamp. uint64 _start; /// Vesting duration. uint64 _duration; - /// SafeErc20 contract + /// [`SafeErc20`] contract. SafeErc20 safe_erc20; } } @@ -133,6 +133,10 @@ pub trait IVestingWallet { type Error: Into>; /// The contract should be able to receive Eth. + /// + /// # Arguments + /// + /// * `&self` - Read access to the contract's state. fn receive_ether(&self); /// Returns the address of the current owner. From 0a911b104f670f396fc965113f2ae1874ab059bd Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Mon, 18 Nov 2024 07:25:13 +0100 Subject: [PATCH 097/138] ref: remove redundant var "one" from gets_vesting_schedule_zero_duration --- contracts/src/finance/vesting_wallet.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index b9ba3bfa4..c8273e7bd 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -608,7 +608,6 @@ mod tests { contract._start.set(start); contract._duration.set(U64::ZERO); - let one = uint!(1_U256); let two = uint!(2_U256); assert_eq!( From 8f4b15ad5983dce7ce7d3338b6cd5325a4d838de Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Mon, 18 Nov 2024 07:27:27 +0100 Subject: [PATCH 098/138] ref: remove TODO for using existing IErc20 --- contracts/src/finance/vesting_wallet.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index c8273e7bd..13c42e4b0 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -75,7 +75,6 @@ sol! { error InvalidToken(address token); } -// TODO: use existing IErc20 pub use token::IErc20; #[allow(missing_docs)] mod token { From 4c4bc37d19bcac335d44a9838249eb80444a2425 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Mon, 18 Nov 2024 07:28:55 +0100 Subject: [PATCH 099/138] docs: use consistent naming for Ether --- contracts/src/finance/vesting_wallet.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 13c42e4b0..fd66d1a36 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -40,9 +40,9 @@ use crate::{ }; sol! { - /// Emitted when `amount` of ether has been released. + /// Emitted when `amount` of Ether has been released. /// - /// * `amount` - Total ether released. + /// * `amount` - Total Ether released. #[allow(missing_docs)] event EtherReleased(uint256 amount); @@ -107,7 +107,7 @@ sol_storage! { pub struct VestingWallet { /// [`Ownable`] contract. Ownable ownable; - /// Amount of eth already released. + /// Amount of Ether already released. uint256 _released; /// Amount of ERC-20 tokens already released. mapping(address => uint256) _erc20_released; @@ -131,7 +131,7 @@ pub trait IVestingWallet { /// The error type associated to this trait implementation. type Error: Into>; - /// The contract should be able to receive Eth. + /// The contract should be able to receive Ether. /// /// # Arguments /// @@ -216,7 +216,7 @@ pub trait IVestingWallet { /// * `&self` - Read access to the contract's state. fn end(&self) -> U256; - /// Amount of eth already released. + /// Amount of Ether already released. /// /// # Arguments /// @@ -233,7 +233,7 @@ pub trait IVestingWallet { #[selector(name = "released")] fn released_erc20(&self, token: Address) -> U256; - /// Getter for the amount of releasable eth. + /// Getter for the amount of releasable Ether. /// /// # Arguments /// @@ -265,7 +265,7 @@ pub trait IVestingWallet { #[selector(name = "releasable")] fn releasable_erc20(&self, token: Address) -> Result; - /// Release the native token (ether) that have already vested. + /// Release the native tokens (Ether) that have already vested. /// /// # Arguments /// @@ -273,7 +273,7 @@ pub trait IVestingWallet { /// /// # Errors /// - /// If ETH transfer fails, then the error [`Error::ReleaseEtherFailed`] is + /// If Ether transfer fails, then the error [`Error::ReleaseEtherFailed`] is /// returned. /// /// # Events @@ -311,7 +311,7 @@ pub trait IVestingWallet { #[selector(name = "release")] fn release_erc20(&mut self, token: Address) -> Result<(), Self::Error>; - /// Calculates the amount of ether that has already vested. Default + /// Calculates the amount of Ether that has already vested. Default /// implementation is a linear vesting curve. /// /// # Arguments From 932a7fdfe6a8b73993456d8fd7f48853a32d41fb Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Mon, 18 Nov 2024 07:29:32 +0100 Subject: [PATCH 100/138] ref: remove unused IErc20::transfer --- contracts/src/finance/vesting_wallet.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index fd66d1a36..e785f7b49 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -82,7 +82,6 @@ mod token { /// Interface of the ERC-20 token. interface IErc20 { function balanceOf(address account) external view returns (uint256); - function transfer(address to, uint256 value) external returns (bool); } } } From cd962a4cad16421178397fe5b588b4153cd54942 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Mon, 18 Nov 2024 07:35:28 +0100 Subject: [PATCH 101/138] ref: remove selector attr from trait def --- contracts/src/finance/vesting_wallet.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index e785f7b49..78d2d96e5 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -220,7 +220,6 @@ pub trait IVestingWallet { /// # Arguments /// /// * `&self` - Read access to the contract's state. - #[selector(name = "released")] fn released_eth(&self) -> U256; /// Amount of token already released. @@ -229,7 +228,6 @@ pub trait IVestingWallet { /// /// * `&self` - Read access to the contract's state. /// * `token` - Address of the token being released. - #[selector(name = "released")] fn released_erc20(&self, token: Address) -> U256; /// Getter for the amount of releasable Ether. @@ -242,7 +240,6 @@ pub trait IVestingWallet { /// /// If total allocation exceeds `U256::MAX`. /// If `total_allocation * (timestamp - self.start())` exceeds `U256::MAX`. - #[selector(name = "releasable")] fn releasable_eth(&self) -> U256; /// Getter for the amount of releasable `token` tokens. `token` should be @@ -261,7 +258,6 @@ pub trait IVestingWallet { /// # Panics /// /// If `total_allocation * (timestamp - self.start())` exceeds `U256::MAX`. - #[selector(name = "releasable")] fn releasable_erc20(&self, token: Address) -> Result; /// Release the native tokens (Ether) that have already vested. @@ -283,7 +279,6 @@ pub trait IVestingWallet { /// /// If total allocation exceeds `U256::MAX`. /// If `total_allocation * (timestamp - self.start())` exceeds `U256::MAX`. - #[selector(name = "release")] fn release_eth(&mut self) -> Result<(), Self::Error>; /// Release the tokens that have already vested. @@ -307,7 +302,6 @@ pub trait IVestingWallet { /// # Panics /// /// If `total_allocation * (timestamp - self.start())` exceeds `U256::MAX`. - #[selector(name = "release")] fn release_erc20(&mut self, token: Address) -> Result<(), Self::Error>; /// Calculates the amount of Ether that has already vested. Default @@ -322,7 +316,6 @@ pub trait IVestingWallet { /// /// If total allocation exceeds `U256::MAX`. /// If `total_allocation * (timestamp - self.start())` exceeds `U256::MAX`. - #[selector(name = "vestedAmount")] fn vested_amount_eth(&self, timestamp: u64) -> U256; /// Calculates the amount of tokens that has already vested. Default @@ -342,7 +335,6 @@ pub trait IVestingWallet { /// # Panics /// /// If `total_allocation * (timestamp - self.start())` exceeds `U256::MAX`. - #[selector(name = "vestedAmount")] fn vested_amount_erc20( &self, token: Address, From c18cb9dc443e82139dc367082968f0daa3c2422c Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Mon, 18 Nov 2024 07:44:29 +0100 Subject: [PATCH 102/138] revert: add back selector attr to trait def Refs: cd962a4cad16421178397fe5b588b4153cd54942 --- contracts/src/finance/vesting_wallet.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 78d2d96e5..e785f7b49 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -220,6 +220,7 @@ pub trait IVestingWallet { /// # Arguments /// /// * `&self` - Read access to the contract's state. + #[selector(name = "released")] fn released_eth(&self) -> U256; /// Amount of token already released. @@ -228,6 +229,7 @@ pub trait IVestingWallet { /// /// * `&self` - Read access to the contract's state. /// * `token` - Address of the token being released. + #[selector(name = "released")] fn released_erc20(&self, token: Address) -> U256; /// Getter for the amount of releasable Ether. @@ -240,6 +242,7 @@ pub trait IVestingWallet { /// /// If total allocation exceeds `U256::MAX`. /// If `total_allocation * (timestamp - self.start())` exceeds `U256::MAX`. + #[selector(name = "releasable")] fn releasable_eth(&self) -> U256; /// Getter for the amount of releasable `token` tokens. `token` should be @@ -258,6 +261,7 @@ pub trait IVestingWallet { /// # Panics /// /// If `total_allocation * (timestamp - self.start())` exceeds `U256::MAX`. + #[selector(name = "releasable")] fn releasable_erc20(&self, token: Address) -> Result; /// Release the native tokens (Ether) that have already vested. @@ -279,6 +283,7 @@ pub trait IVestingWallet { /// /// If total allocation exceeds `U256::MAX`. /// If `total_allocation * (timestamp - self.start())` exceeds `U256::MAX`. + #[selector(name = "release")] fn release_eth(&mut self) -> Result<(), Self::Error>; /// Release the tokens that have already vested. @@ -302,6 +307,7 @@ pub trait IVestingWallet { /// # Panics /// /// If `total_allocation * (timestamp - self.start())` exceeds `U256::MAX`. + #[selector(name = "release")] fn release_erc20(&mut self, token: Address) -> Result<(), Self::Error>; /// Calculates the amount of Ether that has already vested. Default @@ -316,6 +322,7 @@ pub trait IVestingWallet { /// /// If total allocation exceeds `U256::MAX`. /// If `total_allocation * (timestamp - self.start())` exceeds `U256::MAX`. + #[selector(name = "vestedAmount")] fn vested_amount_eth(&self, timestamp: u64) -> U256; /// Calculates the amount of tokens that has already vested. Default @@ -335,6 +342,7 @@ pub trait IVestingWallet { /// # Panics /// /// If `total_allocation * (timestamp - self.start())` exceeds `U256::MAX`. + #[selector(name = "vestedAmount")] fn vested_amount_erc20( &self, token: Address, From 3767df05e575fb5c59e55153b6de55c4efb3e764 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Mon, 18 Nov 2024 10:13:55 +0100 Subject: [PATCH 103/138] ref: use Call to get erc20 balance --- contracts/src/finance/vesting_wallet.rs | 31 ++++++++++++++----------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index e785f7b49..cd33998e5 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -22,11 +22,11 @@ //! adjustment in the vesting schedule to ensure the vested amount is as //! intended. use alloy_primitives::{Address, U256, U64}; -use alloy_sol_types::{sol, SolCall}; +use alloy_sol_types::sol; use openzeppelin_stylus_proc::interface_id; use stylus_sdk::{ block, - call::{self, transfer_eth, RawCall}, + call::{self, transfer_eth, Call}, contract, evm, function_selector, storage::TopLevelStorage, stylus_proc::{public, sol_storage, SolidityError}, @@ -78,7 +78,7 @@ sol! { pub use token::IErc20; #[allow(missing_docs)] mod token { - alloy_sol_types::sol! { + stylus_sdk::stylus_proc::sol_interface! { /// Interface of the ERC-20 token. interface IErc20 { function balanceOf(address account) external view returns (uint256); @@ -250,7 +250,7 @@ pub trait IVestingWallet { /// /// # Arguments /// - /// * `&self` - Read access to the contract's state. + /// * `&mut self` - Write access to the contract's state. /// * `token` - Address of the releasable token. /// /// # Errors @@ -262,7 +262,8 @@ pub trait IVestingWallet { /// /// If `total_allocation * (timestamp - self.start())` exceeds `U256::MAX`. #[selector(name = "releasable")] - fn releasable_erc20(&self, token: Address) -> Result; + fn releasable_erc20(&mut self, token: Address) + -> Result; /// Release the native tokens (Ether) that have already vested. /// @@ -330,7 +331,7 @@ pub trait IVestingWallet { /// /// # Arguments /// - /// * `&self` - Read access to the contract's state. + /// * `&mut self` - Write access to the contract's state. /// * `token` - Address of the token being released. /// * `timestamp` - Point in time for which to check the vested amount. /// @@ -344,7 +345,7 @@ pub trait IVestingWallet { /// If `total_allocation * (timestamp - self.start())` exceeds `U256::MAX`. #[selector(name = "vestedAmount")] fn vested_amount_erc20( - &self, + &mut self, token: Address, timestamp: u64, ) -> Result; @@ -404,7 +405,10 @@ impl IVestingWallet for VestingWallet { } #[selector(name = "releasable")] - fn releasable_erc20(&self, token: Address) -> Result { + fn releasable_erc20( + &mut self, + token: Address, + ) -> Result { let vested = self.vested_amount_erc20(token, block::timestamp())?; // SAFETY: total vested amount is by definition greater than or equal to // the released amount. @@ -455,7 +459,7 @@ impl IVestingWallet for VestingWallet { #[selector(name = "vestedAmount")] fn vested_amount_erc20( - &self, + &mut self, token: Address, timestamp: u64, ) -> Result { @@ -463,11 +467,10 @@ impl IVestingWallet for VestingWallet { return Err(InvalidToken { token }.into()); } - let call = IErc20::balanceOfCall { account: contract::address() }; - let balance = RawCall::new() - .limit_return_data(0, 32) - .call(token, &call.abi_encode()) - .map(|b| U256::from_be_slice(&b)) + let erc20 = IErc20::new(token); + let call = Call::new_in(self); + let balance = erc20 + .balance_of(call, contract::address()) .expect("should return the balance"); // SAFETY: total supply of a [`crate::token::erc20::Erc20`] cannot From 3339d41ac9e4eefefccd4d2d461f370831bd756b Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Mon, 18 Nov 2024 11:47:36 +0100 Subject: [PATCH 104/138] feat: add bench --- benches/src/lib.rs | 2 + benches/src/vesting_wallet.rs | 131 ++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 benches/src/vesting_wallet.rs diff --git a/benches/src/lib.rs b/benches/src/lib.rs index 44e36faf4..3f33a81dd 100644 --- a/benches/src/lib.rs +++ b/benches/src/lib.rs @@ -19,6 +19,7 @@ pub mod erc20; pub mod erc721; pub mod merkle_proofs; pub mod report; +pub mod vesting_wallet; #[derive(Debug, Deserialize)] struct ArbOtherFields { @@ -31,6 +32,7 @@ struct ArbOtherFields { /// Cache options for the contract. /// `Bid(0)` will likely cache the contract on the nitro test node. +#[derive(Clone)] pub enum CacheOpt { None, Bid(u32), diff --git a/benches/src/vesting_wallet.rs b/benches/src/vesting_wallet.rs new file mode 100644 index 000000000..f52c0afd9 --- /dev/null +++ b/benches/src/vesting_wallet.rs @@ -0,0 +1,131 @@ +use alloy::{ + network::{AnyNetwork, EthereumWallet}, + primitives::Address, + providers::ProviderBuilder, + sol, + sol_types::{SolCall, SolConstructor}, + uint, +}; +use alloy_primitives::U256; +use e2e::{receipt, Account}; + +use crate::{ + report::{ContractReport, FunctionReport}, + CacheOpt, +}; + +sol!( + #[sol(rpc)] + contract VestingWallet { + function owner() public view virtual returns (address owner); + function receiveEther() external payable virtual; + function start() external view returns (uint256 start); + function duration() external view returns (uint256 duration); + function end() external view returns (uint256 end); + function released() external view returns (uint256 released); + function released(address token) external view returns (uint256 released); + function releasable() external view returns (uint256 releasable); + function releasable(address token) external view returns (uint256 releasable); + function release() external; + function release(address token) external; + function vestedAmount(uint64 timestamp) external view returns (uint256 vestedAmount); + function vestedAmount(address token, uint64 timestamp) external view returns (uint256 vestedAmount); + } + + #[sol(rpc)] + contract Erc20 { + function mint(address account, uint256 amount) external; + } +); + +sol!("../examples/vesting-wallet/src/constructor.sol"); +sol!("../examples/erc20/src/constructor.sol"); + +const START_TIMESTAMP: u64 = 1000; +const DURATION_SECONDS: u64 = 1000; + +const TOKEN_NAME: &str = "Test Token"; +const TOKEN_SYMBOL: &str = "TTK"; +const CAP: U256 = uint!(1_000_000_U256); + +pub async fn bench() -> eyre::Result { + let reports = run_with(CacheOpt::None).await?; + let report = reports + .into_iter() + .try_fold(ContractReport::new("VestingWallet"), ContractReport::add)?; + + let cached_reports = run_with(CacheOpt::Bid(0)).await?; + let report = cached_reports + .into_iter() + .try_fold(report, ContractReport::add_cached)?; + + Ok(report) +} + +pub async fn run_with( + cache_opt: CacheOpt, +) -> 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, cache_opt.clone()).await?; + let erc20_addr = deploy_token(&alice, cache_opt).await?; + + let contract = VestingWallet::new(contract_addr, &alice_wallet); + let erc20 = Erc20::new(erc20_addr, &alice_wallet); + + let _ = receipt!(contract.receiveEther().value(uint!(1000_U256)))?; + let _ = receipt!(erc20.mint(contract_addr, uint!(1000_U256)))?; + + // IMPORTANT: Order matters! + use VestingWallet::*; + #[rustfmt::skip] + let receipts = vec![ + (startCall::SIGNATURE, receipt!(contract.start())?), + (durationCall::SIGNATURE, receipt!(contract.duration())?), + (endCall::SIGNATURE, receipt!(contract.end())?), + (released_0Call::SIGNATURE, receipt!(contract.released_0())?), + (released_1Call::SIGNATURE, receipt!(contract.released_1(erc20_addr))?), + (releasable_0Call::SIGNATURE, receipt!(contract.releasable_0())?), + (releasable_1Call::SIGNATURE, receipt!(contract.releasable_1(erc20_addr))?), + (release_0Call::SIGNATURE, receipt!(contract.release_0())?), + (release_1Call::SIGNATURE, receipt!(contract.release_1(erc20_addr))?), + (vestedAmount_0Call::SIGNATURE, receipt!(contract.vestedAmount_0(START_TIMESTAMP))?), + (vestedAmount_1Call::SIGNATURE, receipt!(contract.vestedAmount_1(erc20_addr, START_TIMESTAMP))?), + ]; + + receipts + .into_iter() + .map(FunctionReport::new) + .collect::>>() +} + +async fn deploy( + account: &Account, + cache_opt: CacheOpt, +) -> eyre::Result
{ + let args = VestingWalletExample::constructorCall { + beneficiary: account.address(), + startTimestamp: START_TIMESTAMP, + durationSeconds: DURATION_SECONDS, + }; + let args = alloy::hex::encode(args.abi_encode()); + crate::deploy(account, "vesting-wallet", Some(args), cache_opt).await +} + +async fn deploy_token( + account: &Account, + cache_opt: CacheOpt, +) -> eyre::Result
{ + let args = Erc20Example::constructorCall { + name_: TOKEN_NAME.to_owned(), + symbol_: TOKEN_SYMBOL.to_owned(), + cap_: CAP, + }; + let args = alloy::hex::encode(args.abi_encode()); + crate::deploy(account, "erc20", Some(args), cache_opt).await +} From 33fd13a622885908daeb7dcef9964b1c5b18c489 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Tue, 19 Nov 2024 07:46:53 +0100 Subject: [PATCH 105/138] ref: format VestingWallet solidity in benches --- benches/src/vesting_wallet.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/benches/src/vesting_wallet.rs b/benches/src/vesting_wallet.rs index f52c0afd9..f2e6eefe2 100644 --- a/benches/src/vesting_wallet.rs +++ b/benches/src/vesting_wallet.rs @@ -18,18 +18,18 @@ sol!( #[sol(rpc)] contract VestingWallet { function owner() public view virtual returns (address owner); - function receiveEther() external payable virtual; - function start() external view returns (uint256 start); - function duration() external view returns (uint256 duration); - function end() external view returns (uint256 end); - function released() external view returns (uint256 released); - function released(address token) external view returns (uint256 released); - function releasable() external view returns (uint256 releasable); - function releasable(address token) external view returns (uint256 releasable); - function release() external; - function release(address token) external; - function vestedAmount(uint64 timestamp) external view returns (uint256 vestedAmount); - function vestedAmount(address token, uint64 timestamp) external view returns (uint256 vestedAmount); + function receiveEther() external payable virtual; + function start() external view returns (uint256 start); + function duration() external view returns (uint256 duration); + function end() external view returns (uint256 end); + function released() external view returns (uint256 released); + function released(address token) external view returns (uint256 released); + function releasable() external view returns (uint256 releasable); + function releasable(address token) external view returns (uint256 releasable); + function release() external; + function release(address token) external; + function vestedAmount(uint64 timestamp) external view returns (uint256 vestedAmount); + function vestedAmount(address token, uint64 timestamp) external view returns (uint256 vestedAmount); } #[sol(rpc)] From 188c5b15a47661246513d3b8b69207a5931694bb Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Tue, 19 Nov 2024 07:49:22 +0100 Subject: [PATCH 106/138] feat: bench receiveEther --- benches/src/vesting_wallet.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/benches/src/vesting_wallet.rs b/benches/src/vesting_wallet.rs index f2e6eefe2..484d335cb 100644 --- a/benches/src/vesting_wallet.rs +++ b/benches/src/vesting_wallet.rs @@ -85,6 +85,7 @@ pub async fn run_with( use VestingWallet::*; #[rustfmt::skip] let receipts = vec![ + (receiveEtherCall::SIGNATURE, receipt!(contract.receiveEther())?), (startCall::SIGNATURE, receipt!(contract.start())?), (durationCall::SIGNATURE, receipt!(contract.duration())?), (endCall::SIGNATURE, receipt!(contract.end())?), From d87382c97f3ac2c61f0c176208e902d06b7a715c Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 19 Nov 2024 07:50:48 +0100 Subject: [PATCH 107/138] docs: Apply comment updates from code review Co-authored-by: Daniel Bigos --- contracts/src/finance/vesting_wallet.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index cd33998e5..be8a85344 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -60,14 +60,14 @@ sol! { #[allow(missing_docs)] error ReleaseEtherFailed(); - /// Indicates an error related to the underlying ERC-20 transfer. + /// Indicates an error related to the underlying ERC-20 `token` transfer. /// /// * `token` - Address of the token being released. #[derive(Debug)] #[allow(missing_docs)] error ReleaseTokenFailed(address token); - /// The token address is not valid. (eg. `Address::ZERO`). + /// The token address is not valid (eg. `Address::ZERO`). /// /// * `token` - Address of the token being released. #[derive(Debug)] @@ -504,7 +504,7 @@ impl VestingWallet { total_allocation } else { // SAFETY: `timestamp` is guaranteed to be greater than - // `self.start()`, as checked by earlier bounds. + // `self.start()` as checked by earlier bounds. let elapsed = timestamp - self.start(); let scaled_allocation = total_allocation From 3306f5ef4316330fdcb88a779d8819bdd11ce0e1 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Tue, 19 Nov 2024 07:51:50 +0100 Subject: [PATCH 108/138] ref: move enum Error closer to sol errors --- contracts/src/finance/vesting_wallet.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index be8a85344..12e196c4e 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -75,17 +75,6 @@ sol! { error InvalidToken(address token); } -pub use token::IErc20; -#[allow(missing_docs)] -mod token { - stylus_sdk::stylus_proc::sol_interface! { - /// Interface of the ERC-20 token. - interface IErc20 { - function balanceOf(address account) external view returns (uint256); - } - } -} - /// An error that occurred in the [`VestingWallet`] contract. #[derive(SolidityError, Debug)] pub enum Error { @@ -101,6 +90,17 @@ pub enum Error { InvalidToken(InvalidToken), } +pub use token::IErc20; +#[allow(missing_docs)] +mod token { + stylus_sdk::stylus_proc::sol_interface! { + /// Interface of the ERC-20 token. + interface IErc20 { + function balanceOf(address account) external view returns (uint256); + } + } +} + sol_storage! { /// State of the [`VestingWallet`] Contract. pub struct VestingWallet { From e5929d373af7bd4588feb540197af3b4324befd6 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Tue, 19 Nov 2024 07:52:54 +0100 Subject: [PATCH 109/138] ref: move receive_ether below ownable fns --- contracts/src/finance/vesting_wallet.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 12e196c4e..abcf4ff0f 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -130,13 +130,6 @@ pub trait IVestingWallet { /// The error type associated to this trait implementation. type Error: Into>; - /// The contract should be able to receive Ether. - /// - /// # Arguments - /// - /// * `&self` - Read access to the contract's state. - fn receive_ether(&self); - /// Returns the address of the current owner. /// /// Re-export of [`Ownable::owner`]. @@ -194,6 +187,13 @@ pub trait IVestingWallet { /// Emits an [`ownable::OwnershipTransferred`] event. fn renounce_ownership(&mut self) -> Result<(), Self::Error>; + /// The contract should be able to receive Ether. + /// + /// # Arguments + /// + /// * `&self` - Read access to the contract's state. + fn receive_ether(&self); + /// Getter for the start timestamp. /// /// # Arguments @@ -355,9 +355,6 @@ pub trait IVestingWallet { impl IVestingWallet for VestingWallet { type Error = Error; - #[payable] - fn receive_ether(&self) {} - fn owner(&self) -> Address { self.ownable.owner() } @@ -373,6 +370,9 @@ impl IVestingWallet for VestingWallet { Ok(self.ownable.renounce_ownership()?) } + #[payable] + fn receive_ether(&self) {} + fn start(&self) -> U256 { U256::from(self._start.get()) } From 3acb769b5114b602831a6482d1fb57b8ccd19017 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Tue, 19 Nov 2024 07:59:31 +0100 Subject: [PATCH 110/138] ref: format constructor.sol --- examples/vesting-wallet/src/constructor.sol | 22 ++++++++------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/examples/vesting-wallet/src/constructor.sol b/examples/vesting-wallet/src/constructor.sol index 47e9cf508..c94740a77 100644 --- a/examples/vesting-wallet/src/constructor.sol +++ b/examples/vesting-wallet/src/constructor.sol @@ -5,29 +5,23 @@ contract VestingWalletExample { address private _owner; uint256 private _released; - mapping(address token => uint256) private _erc20Released; + mapping(address => uint256) private _erc20Released; uint64 private _start; uint64 private _duration; error OwnableInvalidOwner(address owner); - event OwnershipTransferred( - address indexed previousOwner, - address indexed newOwner - ); - constructor(address beneficiary, uint64 startTimestamp, uint64 durationSeconds) payable { - if (beneficiary == address(0)) { + constructor( + address beneficiary, + uint64 startTimestamp, + uint64 durationSeconds + ) payable { + if (beneficiary == address(0)) { revert OwnableInvalidOwner(address(0)); } - _transferOwnership(beneficiary); + _owner = beneficiary; _start = startTimestamp; _duration = durationSeconds; } - - function _transferOwnership(address newOwner) internal virtual { - address oldOwner = _owner; - _owner = newOwner; - emit OwnershipTransferred(oldOwner, newOwner); - } } From 4499d71e114f78a6b535be1cbf7abaeb65758a54 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Tue, 19 Nov 2024 08:00:57 +0100 Subject: [PATCH 111/138] docs: format end's SAFETY comment --- contracts/src/finance/vesting_wallet.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index abcf4ff0f..2b58ea218 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -382,8 +382,8 @@ impl IVestingWallet for VestingWallet { } fn end(&self) -> U256 { - // SAFETY: both `start` and `duration` are stored as u64, so they cannot - // exceed `U256::MAX` + // SAFETY: both `start` and `duration` are stored as u64, + // so they cannot exceed `U256::MAX` self.start() + self.duration() } From 770fe2defd2bb8771eba913117aa8fd261a5b4cf Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Tue, 19 Nov 2024 08:05:09 +0100 Subject: [PATCH 112/138] ref: emit EtherReleased after transferring --- contracts/src/finance/vesting_wallet.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 2b58ea218..5fd4da7c0 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -424,10 +424,12 @@ impl IVestingWallet for VestingWallet { .expect("should not exceed `U256::MAX` for `_released`"); self._released.set(released); + transfer_eth(self.ownable.owner(), amount) + .map_err(|_| Error::ReleaseEtherFailed(ReleaseEtherFailed {}))?; + evm::log(EtherReleased { amount }); - transfer_eth(self.ownable.owner(), amount) - .map_err(|_| Error::ReleaseEtherFailed(ReleaseEtherFailed {})) + Ok(()) } #[selector(name = "release")] From b57df133b191c83a531292c8712ae1113d01b5a2 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Tue, 19 Nov 2024 08:16:26 +0100 Subject: [PATCH 113/138] fix: checked_add for total allocation --- contracts/src/finance/vesting_wallet.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 5fd4da7c0..2bbc7c613 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -36,7 +36,6 @@ use stylus_sdk::{ use crate::{ access::ownable::{self, IOwnable, Ownable}, token::erc20::utils::safe_erc20::{ISafeErc20, SafeErc20}, - utils::math::storage::AddAssignUnchecked, }; sol! { @@ -307,6 +306,7 @@ pub trait IVestingWallet { /// /// # Panics /// + /// If total allocation exceeds `U256::MAX`. /// If `total_allocation * (timestamp - self.start())` exceeds `U256::MAX`. #[selector(name = "release")] fn release_erc20(&mut self, token: Address) -> Result<(), Self::Error>; @@ -342,6 +342,7 @@ pub trait IVestingWallet { /// /// # Panics /// + /// If total allocation exceeds `U256::MAX`. /// If `total_allocation * (timestamp - self.start())` exceeds `U256::MAX`. #[selector(name = "vestedAmount")] fn vested_amount_erc20( @@ -441,9 +442,12 @@ impl IVestingWallet for VestingWallet { Error::ReleaseTokenFailed(ReleaseTokenFailed { token }) })?; - // SAFETY: total supply of a [`crate::token::erc20::Erc20`] cannot - // exceed `U256::MAX`. - self._erc20_released.setter(token).add_assign_unchecked(amount); + let released = self._erc20_released.get(token); + self._erc20_released.setter(token).set( + released + .checked_add(amount) + .expect("total released should not exceed `U256::MAX`"), + ); evm::log(ERC20Released { token, amount }); @@ -475,9 +479,9 @@ impl IVestingWallet for VestingWallet { .balance_of(call, contract::address()) .expect("should return the balance"); - // SAFETY: total supply of a [`crate::token::erc20::Erc20`] cannot - // exceed `U256::MAX`. - let total_allocation = balance + self.released_erc20(token); + let total_allocation = balance + .checked_add(self.released_erc20(token)) + .expect("total allocation should not exceed `U256::MAX`"); Ok(self.vesting_schedule(total_allocation, U64::from(timestamp))) } From 93b317e568fe537cbb821689e0c18ad92516ce9a Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Tue, 19 Nov 2024 08:17:26 +0100 Subject: [PATCH 114/138] docs: clarify panic error for vesting_schedule --- contracts/src/finance/vesting_wallet.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 2bbc7c613..6460de51a 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -240,7 +240,7 @@ pub trait IVestingWallet { /// # Panics /// /// If total allocation exceeds `U256::MAX`. - /// If `total_allocation * (timestamp - self.start())` exceeds `U256::MAX`. + /// If vested amount exceeds `U256::MAX`. #[selector(name = "releasable")] fn releasable_eth(&self) -> U256; @@ -259,7 +259,7 @@ pub trait IVestingWallet { /// /// # Panics /// - /// If `total_allocation * (timestamp - self.start())` exceeds `U256::MAX`. + /// If vested amount exceeds `U256::MAX`. #[selector(name = "releasable")] fn releasable_erc20(&mut self, token: Address) -> Result; @@ -282,7 +282,7 @@ pub trait IVestingWallet { /// # Panics /// /// If total allocation exceeds `U256::MAX`. - /// If `total_allocation * (timestamp - self.start())` exceeds `U256::MAX`. + /// If vested amount exceeds `U256::MAX`. #[selector(name = "release")] fn release_eth(&mut self) -> Result<(), Self::Error>; @@ -307,7 +307,7 @@ pub trait IVestingWallet { /// # Panics /// /// If total allocation exceeds `U256::MAX`. - /// If `total_allocation * (timestamp - self.start())` exceeds `U256::MAX`. + /// If vested amount exceeds `U256::MAX`. #[selector(name = "release")] fn release_erc20(&mut self, token: Address) -> Result<(), Self::Error>; @@ -322,7 +322,7 @@ pub trait IVestingWallet { /// # Panics /// /// If total allocation exceeds `U256::MAX`. - /// If `total_allocation * (timestamp - self.start())` exceeds `U256::MAX`. + /// If vested amount exceeds `U256::MAX`. #[selector(name = "vestedAmount")] fn vested_amount_eth(&self, timestamp: u64) -> U256; @@ -343,7 +343,7 @@ pub trait IVestingWallet { /// # Panics /// /// If total allocation exceeds `U256::MAX`. - /// If `total_allocation * (timestamp - self.start())` exceeds `U256::MAX`. + /// If vested amount exceeds `U256::MAX`. #[selector(name = "vestedAmount")] fn vested_amount_erc20( &mut self, @@ -500,7 +500,7 @@ impl VestingWallet { /// /// # Panics /// - /// If `total_allocation * (timestamp - self.start())` exceeds `U256::MAX`. + /// If vested amount exceeds `U256::MAX`. fn vesting_schedule(&self, total_allocation: U256, timestamp: U64) -> U256 { let timestamp = U256::from(timestamp); From aa35f89ea0dcae91753399f45c0f3b955f487289 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Tue, 19 Nov 2024 08:32:20 +0100 Subject: [PATCH 115/138] ref: add back unchecked additions for erc20 --- contracts/src/finance/vesting_wallet.rs | 35 ++++++++++++------------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 6460de51a..7b643f15b 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -36,6 +36,7 @@ use stylus_sdk::{ use crate::{ access::ownable::{self, IOwnable, Ownable}, token::erc20::utils::safe_erc20::{ISafeErc20, SafeErc20}, + utils::math::storage::AddAssignUnchecked, }; sol! { @@ -240,7 +241,7 @@ pub trait IVestingWallet { /// # Panics /// /// If total allocation exceeds `U256::MAX`. - /// If vested amount exceeds `U256::MAX`. + /// If scaled total allocation (mid calculation) exceeds `U256::MAX`. #[selector(name = "releasable")] fn releasable_eth(&self) -> U256; @@ -259,7 +260,8 @@ pub trait IVestingWallet { /// /// # Panics /// - /// If vested amount exceeds `U256::MAX`. + /// If total allocation exceeds `U256::MAX`. + /// If scaled total allocation (mid calculation) exceeds `U256::MAX`. #[selector(name = "releasable")] fn releasable_erc20(&mut self, token: Address) -> Result; @@ -282,7 +284,7 @@ pub trait IVestingWallet { /// # Panics /// /// If total allocation exceeds `U256::MAX`. - /// If vested amount exceeds `U256::MAX`. + /// If scaled total allocation (mid calculation) exceeds `U256::MAX`. #[selector(name = "release")] fn release_eth(&mut self) -> Result<(), Self::Error>; @@ -307,7 +309,7 @@ pub trait IVestingWallet { /// # Panics /// /// If total allocation exceeds `U256::MAX`. - /// If vested amount exceeds `U256::MAX`. + /// If scaled total allocation (mid calculation) exceeds `U256::MAX`. #[selector(name = "release")] fn release_erc20(&mut self, token: Address) -> Result<(), Self::Error>; @@ -322,7 +324,7 @@ pub trait IVestingWallet { /// # Panics /// /// If total allocation exceeds `U256::MAX`. - /// If vested amount exceeds `U256::MAX`. + /// If scaled total allocation (mid calculation) exceeds `U256::MAX`. #[selector(name = "vestedAmount")] fn vested_amount_eth(&self, timestamp: u64) -> U256; @@ -343,7 +345,7 @@ pub trait IVestingWallet { /// # Panics /// /// If total allocation exceeds `U256::MAX`. - /// If vested amount exceeds `U256::MAX`. + /// If scaled total allocation (mid calculation) exceeds `U256::MAX`. #[selector(name = "vestedAmount")] fn vested_amount_erc20( &mut self, @@ -419,11 +421,10 @@ impl IVestingWallet for VestingWallet { #[selector(name = "release")] fn release_eth(&mut self) -> Result<(), Self::Error> { let amount = self.releasable_eth(); - let released = self - .released_eth() - .checked_add(amount) - .expect("should not exceed `U256::MAX` for `_released`"); - self._released.set(released); + // SAFETY: total vested amount is by definition smaller than or equal to + // the total allocation, which is already verified to be smaller than + // `U256::MAX` in [`Self::releasable_eth`]. + self._released.set(self._released.get() + amount); transfer_eth(self.ownable.owner(), amount) .map_err(|_| Error::ReleaseEtherFailed(ReleaseEtherFailed {}))?; @@ -442,12 +443,10 @@ impl IVestingWallet for VestingWallet { Error::ReleaseTokenFailed(ReleaseTokenFailed { token }) })?; - let released = self._erc20_released.get(token); - self._erc20_released.setter(token).set( - released - .checked_add(amount) - .expect("total released should not exceed `U256::MAX`"), - ); + // SAFETY: total vested amount is by definition smaller than or equal to + // the total allocation, which is already verified to be smaller than + // `U256::MAX` in [`Self::releasable_erc20`]. + self._erc20_released.setter(token).add_assign_unchecked(amount); evm::log(ERC20Released { token, amount }); @@ -500,7 +499,7 @@ impl VestingWallet { /// /// # Panics /// - /// If vested amount exceeds `U256::MAX`. + /// If scaled total allocation (mid calculation) exceeds `U256::MAX`. fn vesting_schedule(&self, total_allocation: U256, timestamp: U64) -> U256 { let timestamp = U256::from(timestamp); From ed4ce9879aa4995ffb05e2419fd71ca98b90b564 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Tue, 19 Nov 2024 08:43:22 +0100 Subject: [PATCH 116/138] ref: remove zero token address check --- contracts/src/finance/vesting_wallet.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 7b643f15b..efe305513 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -30,7 +30,6 @@ use stylus_sdk::{ contract, evm, function_selector, storage::TopLevelStorage, stylus_proc::{public, sol_storage, SolidityError}, - types::AddressVM, }; use crate::{ @@ -468,15 +467,11 @@ impl IVestingWallet for VestingWallet { token: Address, timestamp: u64, ) -> Result { - if !Address::has_code(&token) { - return Err(InvalidToken { token }.into()); - } - let erc20 = IErc20::new(token); let call = Call::new_in(self); let balance = erc20 .balance_of(call, contract::address()) - .expect("should return the balance"); + .map_err(|_| InvalidToken { token })?; let total_allocation = balance .checked_add(self.released_erc20(token)) From a4043b6c226c152206ad5c4fe24ab8221ef4d24a Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Tue, 19 Nov 2024 08:52:57 +0100 Subject: [PATCH 117/138] ref(tests): separate deploy fns for individual contracts --- .../vesting-wallet/tests/vesting-wallet.rs | 53 +++++++++++-------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/examples/vesting-wallet/tests/vesting-wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs index 9605fac1b..6acab0db0 100644 --- a/examples/vesting-wallet/tests/vesting-wallet.rs +++ b/examples/vesting-wallet/tests/vesting-wallet.rs @@ -225,20 +225,36 @@ mod erc20_vesting { account: &Account, start: u64, duration: u64, - allocation: u64, - ) -> eyre::Result<(Address, Address)> { + ) -> eyre::Result
{ let contract_addr = account .as_deployer() .with_constructor(ctr(account.address(), start, duration)) .deploy() .await? .address()?; + Ok(contract_addr) + } + async fn deploy_erc20( + account: &Account, + mint_to: Address, + allocation: u64, + ) -> eyre::Result
{ let erc20_address = erc20::deploy(&account.wallet).await?; let erc20 = ERC20Mock::new(erc20_address, &account.wallet); - let _ = watch!(erc20.mint(contract_addr, U256::from(allocation)))?; + let _ = watch!(erc20.mint(mint_to, U256::from(allocation)))?; + Ok(erc20_address) + } - Ok((contract_addr, erc20_address)) + async fn deploy_erc20_return_false( + account: &Account, + mint_to: Address, + allocation: u64, + ) -> eyre::Result
{ + let erc20_address = erc20_return_false::deploy(&alice.wallet).await?; + let erc20 = ERC20ReturnFalseMock::new(erc20_address, &alice.wallet); + let _ = watch!(erc20.mint(mint_to, U256::from(allocation)))?; + Ok(erc20_address) } async fn run_check_release( @@ -251,8 +267,9 @@ mod erc20_vesting { BALANCE, BALANCE * time_passed / DURATION, )); - let (contract_addr, erc20_address) = - deploy(&alice, start, DURATION, BALANCE).await?; + let contract_addr = deploy(&alice, start, DURATION).await?; + let erc20_address = + deploy_erc20(&alice, contract_addr, BALANCE).await?; let contract = VestingWallet::new(contract_addr, &alice.wallet); let erc20 = ERC20Mock::new(erc20_address, &alice.wallet); @@ -321,8 +338,9 @@ mod erc20_vesting { #[e2e::test] async fn check_vesting_schedule(alice: Account) -> eyre::Result<()> { let start = block_timestamp(&alice).await?; - let (contract_addr, erc20_address) = - deploy(&alice, start, DURATION, BALANCE).await?; + let contract_addr = deploy(&alice, start, DURATION).await?; + let erc20_address = + deploy_erc20(&alice, contract_addr, BALANCE).await?; let contract = VestingWallet::new(contract_addr, &alice.wallet); @@ -352,7 +370,7 @@ mod erc20_vesting { alice: Account, ) -> eyre::Result<()> { let start = block_timestamp(&alice).await?; - let contract_addr = deploy(&alice, start, DURATION, BALANCE).await?.0; + let contract_addr = deploy(&alice, start, DURATION).await?; let contract = VestingWallet::new(contract_addr, &alice.wallet); @@ -371,7 +389,7 @@ mod erc20_vesting { alice: Account, ) -> eyre::Result<()> { let start = block_timestamp(&alice).await?; - let contract_addr = deploy(&alice, start, DURATION, BALANCE).await?.0; + let contract_addr = deploy(&alice, start, DURATION).await?; let contract = VestingWallet::new(contract_addr, &alice.wallet); @@ -394,16 +412,9 @@ mod erc20_vesting { }; let start = block_timestamp(&alice).await?; - let contract_addr = alice - .as_deployer() - .with_constructor(ctr(alice.address(), start, DURATION)) - .deploy() - .await? - .address()?; - - let erc20_address = erc20_return_false::deploy(&alice.wallet).await?; - let erc20 = ERC20ReturnFalseMock::new(erc20_address, &alice.wallet); - let _ = watch!(erc20.mint(contract_addr, U256::from(BALANCE)))?; + let contract_addr = deploy(&alice, start, DURATION).await?; + let erc20_address = + deploy_erc20_return_false(&alice, contract_addr, BALANCE).await?; let contract = VestingWallet::new(contract_addr, &alice.wallet); @@ -422,7 +433,7 @@ mod erc20_vesting { alice: Account, ) -> eyre::Result<()> { let start = block_timestamp(&alice).await?; - let contract_addr = deploy(&alice, start, DURATION, BALANCE).await?.0; + let contract_addr = deploy(&alice, start, DURATION).await?; let contract = VestingWallet::new(contract_addr, &alice.wallet); From 2040925f92995a9f519b7d0d69f5b2db2c6e204d Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Tue, 19 Nov 2024 08:57:00 +0100 Subject: [PATCH 118/138] ref(tests): add deploy fn for eth tests --- .../vesting-wallet/tests/vesting-wallet.rs | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/examples/vesting-wallet/tests/vesting-wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs index 6acab0db0..51ec8ea5f 100644 --- a/examples/vesting-wallet/tests/vesting-wallet.rs +++ b/examples/vesting-wallet/tests/vesting-wallet.rs @@ -106,6 +106,25 @@ async fn rejects_zero_address_for_beneficiary( mod ether_vesting { use super::*; + async fn deploy( + account: &Account, + start: u64, + duration: u64, + allocation: u64, + ) -> eyre::Result
{ + let contract_addr = account + .as_deployer() + .with_constructor(ctr(account.address(), start, DURATION)) + .deploy() + .await? + .address()?; + let contract = VestingWallet::new(contract_addr, &account.wallet); + + let _ = watch!(contract.receiveEther().value(U256::from(allocation)))?; + + Ok(contract_addr) + } + async fn run_check_release( alice: Account, time_passed: u64, @@ -116,16 +135,7 @@ mod ether_vesting { BALANCE, BALANCE * time_passed / DURATION, )); - - let contract_addr = alice - .as_deployer() - .with_constructor(ctr(alice.address(), start, DURATION)) - .deploy() - .await? - .address()?; - let contract = VestingWallet::new(contract_addr, &alice.wallet); - - let _ = watch!(contract.receiveEther().value(U256::from(BALANCE)))?; + let contract_addr = deploy(&alice, start, DURATION, BALANCE).await?; let old_alice_balance = alice.wallet.get_balance(alice.address()).await?; @@ -189,15 +199,9 @@ mod ether_vesting { #[e2e::test] async fn check_vesting_schedule(alice: Account) -> eyre::Result<()> { let start = block_timestamp(&alice).await?; - let contract_addr = alice - .as_deployer() - .with_constructor(ctr(alice.address(), start, DURATION)) - .deploy() - .await? - .address()?; - let contract = VestingWallet::new(contract_addr, &alice.wallet); + let contract_addr = deploy(&alice, start, DURATION, BALANCE).await?; - let _ = watch!(contract.receiveEther().value(U256::from(BALANCE)))?; + let contract = VestingWallet::new(contract_addr, &alice.wallet); for i in 0..64 { let timestamp = i * DURATION / 60 + start; From 986a584020b36f6297827b769b24e85e25cf68cb Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Tue, 19 Nov 2024 09:06:02 +0100 Subject: [PATCH 119/138] fix(tests): fix failing test compilation --- examples/vesting-wallet/tests/vesting-wallet.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/examples/vesting-wallet/tests/vesting-wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs index 51ec8ea5f..40f04cd61 100644 --- a/examples/vesting-wallet/tests/vesting-wallet.rs +++ b/examples/vesting-wallet/tests/vesting-wallet.rs @@ -114,7 +114,7 @@ mod ether_vesting { ) -> eyre::Result
{ let contract_addr = account .as_deployer() - .with_constructor(ctr(account.address(), start, DURATION)) + .with_constructor(ctr(account.address(), start, duration)) .deploy() .await? .address()?; @@ -136,6 +136,7 @@ mod ether_vesting { BALANCE * time_passed / DURATION, )); let contract_addr = deploy(&alice, start, DURATION, BALANCE).await?; + let contract = VestingWallet::new(contract_addr, &alice.wallet); let old_alice_balance = alice.wallet.get_balance(alice.address()).await?; @@ -255,8 +256,12 @@ mod erc20_vesting { mint_to: Address, allocation: u64, ) -> eyre::Result
{ - let erc20_address = erc20_return_false::deploy(&alice.wallet).await?; - let erc20 = ERC20ReturnFalseMock::new(erc20_address, &alice.wallet); + use mock::{ + erc20_return_false, erc20_return_false::ERC20ReturnFalseMock, + }; + + let erc20_address = erc20_return_false::deploy(&account.wallet).await?; + let erc20 = ERC20ReturnFalseMock::new(erc20_address, &account.wallet); let _ = watch!(erc20.mint(mint_to, U256::from(allocation)))?; Ok(erc20_address) } @@ -411,10 +416,6 @@ mod erc20_vesting { async fn release_erc20_reverts_on_failed_transfer( alice: Account, ) -> eyre::Result<()> { - use mock::{ - erc20_return_false, erc20_return_false::ERC20ReturnFalseMock, - }; - let start = block_timestamp(&alice).await?; let contract_addr = deploy(&alice, start, DURATION).await?; let erc20_address = From 09ef93f14fd7a53ed02120c549c5822b506a34f2 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Tue, 19 Nov 2024 12:04:13 +0100 Subject: [PATCH 120/138] ref(test): check_vesting_schedule->check_vested_amount --- examples/vesting-wallet/tests/vesting-wallet.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/vesting-wallet/tests/vesting-wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs index 40f04cd61..4dd9cc506 100644 --- a/examples/vesting-wallet/tests/vesting-wallet.rs +++ b/examples/vesting-wallet/tests/vesting-wallet.rs @@ -198,7 +198,7 @@ mod ether_vesting { } #[e2e::test] - async fn check_vesting_schedule(alice: Account) -> eyre::Result<()> { + async fn check_vested_amount(alice: Account) -> eyre::Result<()> { let start = block_timestamp(&alice).await?; let contract_addr = deploy(&alice, start, DURATION, BALANCE).await?; @@ -345,7 +345,7 @@ mod erc20_vesting { } #[e2e::test] - async fn check_vesting_schedule(alice: Account) -> eyre::Result<()> { + async fn check_vested_amount(alice: Account) -> eyre::Result<()> { let start = block_timestamp(&alice).await?; let contract_addr = deploy(&alice, start, DURATION).await?; let erc20_address = From 3277ccfd24c45f47a8ebb3eec3dc080297cc068c Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Tue, 19 Nov 2024 12:18:59 +0100 Subject: [PATCH 121/138] ref(tests): fix expect_err msg in safe_erc20 --- examples/safe-erc20/tests/erc20_that_does_not_return.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/safe-erc20/tests/erc20_that_does_not_return.rs b/examples/safe-erc20/tests/erc20_that_does_not_return.rs index 077096211..9fbffe3f9 100644 --- a/examples/safe-erc20/tests/erc20_that_does_not_return.rs +++ b/examples/safe-erc20/tests/erc20_that_does_not_return.rs @@ -352,7 +352,7 @@ mod approvals { spender_addr, value )) - .expect_err("should not exceed U256::MAX"); + .expect_err("should exceed U256::MAX"); assert!(err.panicked_with(PanicCode::ArithmeticOverflow)); From a7abc28b764e859bd8680b470e1c050f8c92f822 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Tue, 19 Nov 2024 12:21:01 +0100 Subject: [PATCH 122/138] test: add vested_amount_reverts_on_scaled_allocation_overflow --- .../vesting-wallet/tests/vesting-wallet.rs | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/examples/vesting-wallet/tests/vesting-wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs index 4dd9cc506..fc938e7e4 100644 --- a/examples/vesting-wallet/tests/vesting-wallet.rs +++ b/examples/vesting-wallet/tests/vesting-wallet.rs @@ -7,7 +7,10 @@ use alloy::{ providers::Provider, sol, }; -use e2e::{receipt, send, watch, Account, EventExt, ReceiptExt, Revert}; +use e2e::{ + receipt, send, watch, Account, EventExt, Panic, PanicCode, ReceiptExt, + Revert, +}; use mock::{erc20, erc20::ERC20Mock}; use crate::VestingWalletExample::constructorCall; @@ -243,11 +246,11 @@ mod erc20_vesting { async fn deploy_erc20( account: &Account, mint_to: Address, - allocation: u64, + allocation: U256, ) -> eyre::Result
{ let erc20_address = erc20::deploy(&account.wallet).await?; let erc20 = ERC20Mock::new(erc20_address, &account.wallet); - let _ = watch!(erc20.mint(mint_to, U256::from(allocation)))?; + let _ = watch!(erc20.mint(mint_to, allocation))?; Ok(erc20_address) } @@ -278,7 +281,7 @@ mod erc20_vesting { )); let contract_addr = deploy(&alice, start, DURATION).await?; let erc20_address = - deploy_erc20(&alice, contract_addr, BALANCE).await?; + deploy_erc20(&alice, contract_addr, U256::from(BALANCE)).await?; let contract = VestingWallet::new(contract_addr, &alice.wallet); let erc20 = ERC20Mock::new(erc20_address, &alice.wallet); @@ -349,7 +352,7 @@ mod erc20_vesting { let start = block_timestamp(&alice).await?; let contract_addr = deploy(&alice, start, DURATION).await?; let erc20_address = - deploy_erc20(&alice, contract_addr, BALANCE).await?; + deploy_erc20(&alice, contract_addr, U256::from(BALANCE)).await?; let contract = VestingWallet::new(contract_addr, &alice.wallet); @@ -451,4 +454,24 @@ mod erc20_vesting { Ok(()) } + + #[e2e::test] + async fn vested_amount_reverts_on_scaled_allocation_overflow( + alice: Account, + ) -> eyre::Result<()> { + let start = block_timestamp(&alice).await?; + let timestamp = DURATION / 2 + start; + let contract_addr = deploy(&alice, start, DURATION).await?; + let erc20_address = + deploy_erc20(&alice, contract_addr, U256::MAX).await?; + + let contract = VestingWallet::new(contract_addr, &alice.wallet); + + let err = send!(contract.vestedAmount_1(erc20_address, timestamp)) + .expect_err("should exceed `U256::MAX`"); + + assert!(err.panicked_with(PanicCode::ArithmeticOverflow)); + + Ok(()) + } } From b748068ec5361abf234a397a349fc60176f1857d Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Tue, 19 Nov 2024 12:33:57 +0100 Subject: [PATCH 123/138] docs: use generic ERC-20 when referring to tokens --- contracts/src/finance/vesting_wallet.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index efe305513..26ca21944 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -245,7 +245,7 @@ pub trait IVestingWallet { fn releasable_eth(&self) -> U256; /// Getter for the amount of releasable `token` tokens. `token` should be - /// the address of an [`crate::token::erc20::Erc20`] contract. + /// the address of an ERC-20 contract. /// /// # Arguments /// From 65ca111e0fd264aec71d92f2896cc6a9fb0ac370 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Wed, 20 Nov 2024 10:02:52 +0100 Subject: [PATCH 124/138] feat: add vesting_wallet bench to report --- benches/src/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/benches/src/main.rs b/benches/src/main.rs index 52278d77b..b289a0be2 100644 --- a/benches/src/main.rs +++ b/benches/src/main.rs @@ -1,5 +1,6 @@ use benches::{ access_control, erc20, erc721, merkle_proofs, report::BenchmarkReport, + vesting_wallet, }; use futures::FutureExt; @@ -10,6 +11,7 @@ async fn main() -> eyre::Result<()> { erc20::bench().boxed(), erc721::bench().boxed(), merkle_proofs::bench().boxed(), + vesting_wallet::bench().boxed(), ]) .await? .into_iter() From d1e4ee7a97a0fb4f531908f6c5ed41221e7995fa Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Wed, 20 Nov 2024 10:40:02 +0100 Subject: [PATCH 125/138] ref: temporarily comment out erc20-related benches --- benches/src/lib.rs | 1 - benches/src/vesting_wallet.rs | 60 +++++++++++++++++------------------ 2 files changed, 29 insertions(+), 32 deletions(-) diff --git a/benches/src/lib.rs b/benches/src/lib.rs index 3f33a81dd..39ed3a946 100644 --- a/benches/src/lib.rs +++ b/benches/src/lib.rs @@ -32,7 +32,6 @@ struct ArbOtherFields { /// Cache options for the contract. /// `Bid(0)` will likely cache the contract on the nitro test node. -#[derive(Clone)] pub enum CacheOpt { None, Bid(u32), diff --git a/benches/src/vesting_wallet.rs b/benches/src/vesting_wallet.rs index 484d335cb..7fa185b75 100644 --- a/benches/src/vesting_wallet.rs +++ b/benches/src/vesting_wallet.rs @@ -6,7 +6,6 @@ use alloy::{ sol_types::{SolCall, SolConstructor}, uint, }; -use alloy_primitives::U256; use e2e::{receipt, Account}; use crate::{ @@ -31,22 +30,17 @@ sol!( function vestedAmount(uint64 timestamp) external view returns (uint256 vestedAmount); function vestedAmount(address token, uint64 timestamp) external view returns (uint256 vestedAmount); } - - #[sol(rpc)] - contract Erc20 { - function mint(address account, uint256 amount) external; - } ); sol!("../examples/vesting-wallet/src/constructor.sol"); -sol!("../examples/erc20/src/constructor.sol"); +// sol!("../examples/erc20/src/constructor.sol"); const START_TIMESTAMP: u64 = 1000; const DURATION_SECONDS: u64 = 1000; -const TOKEN_NAME: &str = "Test Token"; -const TOKEN_SYMBOL: &str = "TTK"; -const CAP: U256 = uint!(1_000_000_U256); +// const TOKEN_NAME: &str = "Test Token"; +// const TOKEN_SYMBOL: &str = "TTK"; +// const CAP: U256 = uint!(1_000_000_U256); pub async fn bench() -> eyre::Result { let reports = run_with(CacheOpt::None).await?; @@ -72,16 +66,20 @@ pub async fn run_with( .wallet(EthereumWallet::from(alice.signer.clone())) .on_http(alice.url().parse()?); - let contract_addr = deploy(&alice, cache_opt.clone()).await?; - let erc20_addr = deploy_token(&alice, cache_opt).await?; + let contract_addr = deploy(&alice, cache_opt).await?; + // let erc20_addr = deploy_token(&alice, cache_opt).await?; let contract = VestingWallet::new(contract_addr, &alice_wallet); - let erc20 = Erc20::new(erc20_addr, &alice_wallet); + // let erc20 = Erc20::new(erc20_addr, &alice_wallet); let _ = receipt!(contract.receiveEther().value(uint!(1000_U256)))?; - let _ = receipt!(erc20.mint(contract_addr, uint!(1000_U256)))?; + // let _ = receipt!(erc20.mint(contract_addr, uint!(1000_U256)))?; + + let timestamp = START_TIMESTAMP + DURATION_SECONDS / 2; // IMPORTANT: Order matters! + // TODO: uncomment ERC-20-related benches once solution to + // `error` ProgramNotActivated()` is found. use VestingWallet::*; #[rustfmt::skip] let receipts = vec![ @@ -90,13 +88,13 @@ pub async fn run_with( (durationCall::SIGNATURE, receipt!(contract.duration())?), (endCall::SIGNATURE, receipt!(contract.end())?), (released_0Call::SIGNATURE, receipt!(contract.released_0())?), - (released_1Call::SIGNATURE, receipt!(contract.released_1(erc20_addr))?), + // (released_1Call::SIGNATURE, receipt!(contract.released_1(erc20_addr))?), (releasable_0Call::SIGNATURE, receipt!(contract.releasable_0())?), - (releasable_1Call::SIGNATURE, receipt!(contract.releasable_1(erc20_addr))?), + // (releasable_1Call::SIGNATURE, receipt!(contract.releasable_1(erc20_addr))?), (release_0Call::SIGNATURE, receipt!(contract.release_0())?), - (release_1Call::SIGNATURE, receipt!(contract.release_1(erc20_addr))?), - (vestedAmount_0Call::SIGNATURE, receipt!(contract.vestedAmount_0(START_TIMESTAMP))?), - (vestedAmount_1Call::SIGNATURE, receipt!(contract.vestedAmount_1(erc20_addr, START_TIMESTAMP))?), + // (release_1Call::SIGNATURE, receipt!(contract.release_1(erc20_addr))?), + (vestedAmount_0Call::SIGNATURE, receipt!(contract.vestedAmount_0(timestamp))?), + // (vestedAmount_1Call::SIGNATURE, receipt!(contract.vestedAmount_1(erc20_addr, timestamp))?), ]; receipts @@ -118,15 +116,15 @@ async fn deploy( crate::deploy(account, "vesting-wallet", Some(args), cache_opt).await } -async fn deploy_token( - account: &Account, - cache_opt: CacheOpt, -) -> eyre::Result
{ - let args = Erc20Example::constructorCall { - name_: TOKEN_NAME.to_owned(), - symbol_: TOKEN_SYMBOL.to_owned(), - cap_: CAP, - }; - let args = alloy::hex::encode(args.abi_encode()); - crate::deploy(account, "erc20", Some(args), cache_opt).await -} +// async fn deploy_token( +// account: &Account, +// cache_opt: CacheOpt, +// ) -> eyre::Result
{ +// let args = Erc20Example::constructorCall { +// name_: TOKEN_NAME.to_owned(), +// symbol_: TOKEN_SYMBOL.to_owned(), +// cap_: CAP, +// }; +// let args = alloy::hex::encode(args.abi_encode()); +// crate::deploy(account, "erc20", Some(args), cache_opt).await +// } From cf5a1005f320010dcbaa146dfe9cca1855f67a66 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Wed, 20 Nov 2024 12:46:14 +0100 Subject: [PATCH 126/138] revert: add back erc20-related benches Refs: d1e4ee7a97a0fb4f531908f6c5ed41221e7995fa --- benches/src/lib.rs | 1 + benches/src/vesting_wallet.rs | 60 ++++++++++++++++++----------------- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/benches/src/lib.rs b/benches/src/lib.rs index 39ed3a946..3f33a81dd 100644 --- a/benches/src/lib.rs +++ b/benches/src/lib.rs @@ -32,6 +32,7 @@ struct ArbOtherFields { /// Cache options for the contract. /// `Bid(0)` will likely cache the contract on the nitro test node. +#[derive(Clone)] pub enum CacheOpt { None, Bid(u32), diff --git a/benches/src/vesting_wallet.rs b/benches/src/vesting_wallet.rs index 7fa185b75..484d335cb 100644 --- a/benches/src/vesting_wallet.rs +++ b/benches/src/vesting_wallet.rs @@ -6,6 +6,7 @@ use alloy::{ sol_types::{SolCall, SolConstructor}, uint, }; +use alloy_primitives::U256; use e2e::{receipt, Account}; use crate::{ @@ -30,17 +31,22 @@ sol!( function vestedAmount(uint64 timestamp) external view returns (uint256 vestedAmount); function vestedAmount(address token, uint64 timestamp) external view returns (uint256 vestedAmount); } + + #[sol(rpc)] + contract Erc20 { + function mint(address account, uint256 amount) external; + } ); sol!("../examples/vesting-wallet/src/constructor.sol"); -// sol!("../examples/erc20/src/constructor.sol"); +sol!("../examples/erc20/src/constructor.sol"); const START_TIMESTAMP: u64 = 1000; const DURATION_SECONDS: u64 = 1000; -// const TOKEN_NAME: &str = "Test Token"; -// const TOKEN_SYMBOL: &str = "TTK"; -// const CAP: U256 = uint!(1_000_000_U256); +const TOKEN_NAME: &str = "Test Token"; +const TOKEN_SYMBOL: &str = "TTK"; +const CAP: U256 = uint!(1_000_000_U256); pub async fn bench() -> eyre::Result { let reports = run_with(CacheOpt::None).await?; @@ -66,20 +72,16 @@ pub async fn run_with( .wallet(EthereumWallet::from(alice.signer.clone())) .on_http(alice.url().parse()?); - let contract_addr = deploy(&alice, cache_opt).await?; - // let erc20_addr = deploy_token(&alice, cache_opt).await?; + let contract_addr = deploy(&alice, cache_opt.clone()).await?; + let erc20_addr = deploy_token(&alice, cache_opt).await?; let contract = VestingWallet::new(contract_addr, &alice_wallet); - // let erc20 = Erc20::new(erc20_addr, &alice_wallet); + let erc20 = Erc20::new(erc20_addr, &alice_wallet); let _ = receipt!(contract.receiveEther().value(uint!(1000_U256)))?; - // let _ = receipt!(erc20.mint(contract_addr, uint!(1000_U256)))?; - - let timestamp = START_TIMESTAMP + DURATION_SECONDS / 2; + let _ = receipt!(erc20.mint(contract_addr, uint!(1000_U256)))?; // IMPORTANT: Order matters! - // TODO: uncomment ERC-20-related benches once solution to - // `error` ProgramNotActivated()` is found. use VestingWallet::*; #[rustfmt::skip] let receipts = vec![ @@ -88,13 +90,13 @@ pub async fn run_with( (durationCall::SIGNATURE, receipt!(contract.duration())?), (endCall::SIGNATURE, receipt!(contract.end())?), (released_0Call::SIGNATURE, receipt!(contract.released_0())?), - // (released_1Call::SIGNATURE, receipt!(contract.released_1(erc20_addr))?), + (released_1Call::SIGNATURE, receipt!(contract.released_1(erc20_addr))?), (releasable_0Call::SIGNATURE, receipt!(contract.releasable_0())?), - // (releasable_1Call::SIGNATURE, receipt!(contract.releasable_1(erc20_addr))?), + (releasable_1Call::SIGNATURE, receipt!(contract.releasable_1(erc20_addr))?), (release_0Call::SIGNATURE, receipt!(contract.release_0())?), - // (release_1Call::SIGNATURE, receipt!(contract.release_1(erc20_addr))?), - (vestedAmount_0Call::SIGNATURE, receipt!(contract.vestedAmount_0(timestamp))?), - // (vestedAmount_1Call::SIGNATURE, receipt!(contract.vestedAmount_1(erc20_addr, timestamp))?), + (release_1Call::SIGNATURE, receipt!(contract.release_1(erc20_addr))?), + (vestedAmount_0Call::SIGNATURE, receipt!(contract.vestedAmount_0(START_TIMESTAMP))?), + (vestedAmount_1Call::SIGNATURE, receipt!(contract.vestedAmount_1(erc20_addr, START_TIMESTAMP))?), ]; receipts @@ -116,15 +118,15 @@ async fn deploy( crate::deploy(account, "vesting-wallet", Some(args), cache_opt).await } -// async fn deploy_token( -// account: &Account, -// cache_opt: CacheOpt, -// ) -> eyre::Result
{ -// let args = Erc20Example::constructorCall { -// name_: TOKEN_NAME.to_owned(), -// symbol_: TOKEN_SYMBOL.to_owned(), -// cap_: CAP, -// }; -// let args = alloy::hex::encode(args.abi_encode()); -// crate::deploy(account, "erc20", Some(args), cache_opt).await -// } +async fn deploy_token( + account: &Account, + cache_opt: CacheOpt, +) -> eyre::Result
{ + let args = Erc20Example::constructorCall { + name_: TOKEN_NAME.to_owned(), + symbol_: TOKEN_SYMBOL.to_owned(), + cap_: CAP, + }; + let args = alloy::hex::encode(args.abi_encode()); + crate::deploy(account, "erc20", Some(args), cache_opt).await +} From ea75250d7dfacf290ab6627ec547eb02a35bea57 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Wed, 20 Nov 2024 12:47:11 +0100 Subject: [PATCH 127/138] ref: remove vesting wallet from benches report --- benches/src/main.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/benches/src/main.rs b/benches/src/main.rs index b289a0be2..52278d77b 100644 --- a/benches/src/main.rs +++ b/benches/src/main.rs @@ -1,6 +1,5 @@ use benches::{ access_control, erc20, erc721, merkle_proofs, report::BenchmarkReport, - vesting_wallet, }; use futures::FutureExt; @@ -11,7 +10,6 @@ async fn main() -> eyre::Result<()> { erc20::bench().boxed(), erc721::bench().boxed(), merkle_proofs::bench().boxed(), - vesting_wallet::bench().boxed(), ]) .await? .into_iter() From ee24a1f0a8710f1a68b25fe42eb8c683131084cd Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh Date: Mon, 25 Nov 2024 13:53:42 +0400 Subject: [PATCH 128/138] add grammar fixes --- contracts/src/finance/vesting_wallet.rs | 26 +++++++++---------- .../vesting-wallet/tests/vesting-wallet.rs | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 26ca21944..07fa7b243 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -11,7 +11,7 @@ //! By setting the duration to 0, one can configure this contract to behave like //! an asset timelock that hold tokens for a beneficiary until a specified time. //! -//! NOTE: Since the wallet is [`crate::access::ownable::Ownable`], and ownership +//! NOTE: Since the wallet is [`Ownable`], and ownership //! can be transferred, it is possible to sell unvested tokens. Preventing this //! in a smart contract is difficult, considering that: 1) a beneficiary address //! could be a counterfactually deployed contract, 2) there is likely to be a @@ -79,7 +79,7 @@ sol! { pub enum Error { /// Error type from [`Ownable`] contract [`ownable::Error`]. Ownable(ownable::Error), - /// Error type from [`call::Call`] contract [`call::Error`]. + /// Error type from [`Call`] contract [`call::Error`]. StylusError(call::Error), /// Indicates an error related to the underlying Ether transfer. ReleaseEtherFailed(ReleaseEtherFailed), @@ -152,7 +152,7 @@ pub trait IVestingWallet { /// /// If called by any account other than the owner, then the error /// [`ownable::Error::UnauthorizedAccount`] is returned. - /// If `new_owner` is the zero address, then the error + /// If `new_owner` is the `Address::ZERO`, then the error /// [`ownable::Error::InvalidOwner`] is returned. /// /// # Events @@ -240,7 +240,7 @@ pub trait IVestingWallet { /// # Panics /// /// If total allocation exceeds `U256::MAX`. - /// If scaled total allocation (mid calculation) exceeds `U256::MAX`. + /// If scaled, total allocation (mid calculation) exceeds `U256::MAX`. #[selector(name = "releasable")] fn releasable_eth(&self) -> U256; @@ -260,7 +260,7 @@ pub trait IVestingWallet { /// # Panics /// /// If total allocation exceeds `U256::MAX`. - /// If scaled total allocation (mid calculation) exceeds `U256::MAX`. + /// If scaled, total allocation (mid calculation) exceeds `U256::MAX`. #[selector(name = "releasable")] fn releasable_erc20(&mut self, token: Address) -> Result; @@ -308,12 +308,12 @@ pub trait IVestingWallet { /// # Panics /// /// If total allocation exceeds `U256::MAX`. - /// If scaled total allocation (mid calculation) exceeds `U256::MAX`. + /// If scaled, total allocation (mid calculation) exceeds `U256::MAX`. #[selector(name = "release")] fn release_erc20(&mut self, token: Address) -> Result<(), Self::Error>; - /// Calculates the amount of Ether that has already vested. Default - /// implementation is a linear vesting curve. + /// Calculates the amount of Ether that has already vested. + /// The Default implementation is a linear vesting curve. /// /// # Arguments /// @@ -323,12 +323,12 @@ pub trait IVestingWallet { /// # Panics /// /// If total allocation exceeds `U256::MAX`. - /// If scaled total allocation (mid calculation) exceeds `U256::MAX`. + /// If scaled, total allocation (mid calculation) exceeds `U256::MAX`. #[selector(name = "vestedAmount")] fn vested_amount_eth(&self, timestamp: u64) -> U256; - /// Calculates the amount of tokens that has already vested. Default - /// implementation is a linear vesting curve. + /// Calculates the amount of tokens that has already vested. + /// The Default implementation is a linear vesting curve. /// /// # Arguments /// @@ -344,7 +344,7 @@ pub trait IVestingWallet { /// # Panics /// /// If total allocation exceeds `U256::MAX`. - /// If scaled total allocation (mid calculation) exceeds `U256::MAX`. + /// If scaled, total allocation (mid calculation) exceeds `U256::MAX`. #[selector(name = "vestedAmount")] fn vested_amount_erc20( &mut self, @@ -494,7 +494,7 @@ impl VestingWallet { /// /// # Panics /// - /// If scaled total allocation (mid calculation) exceeds `U256::MAX`. + /// If scaled, total allocation (mid calculation) exceeds `U256::MAX`. fn vesting_schedule(&self, total_allocation: U256, timestamp: U64) -> U256 { let timestamp = U256::from(timestamp); diff --git a/examples/vesting-wallet/tests/vesting-wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs index fc938e7e4..5aa0ea350 100644 --- a/examples/vesting-wallet/tests/vesting-wallet.rs +++ b/examples/vesting-wallet/tests/vesting-wallet.rs @@ -60,7 +60,7 @@ async fn block_timestamp(account: &Account) -> eyre::Result { fn assert_in_delta(expected: U256, actual: U256) { let diff = expected.abs_diff(actual); let delta = U256::from(1); - assert!(diff <= delta,"Your result of {actual} should be within {delta} of the expected result {expected}"); + assert!(diff <= delta, "Your result of {actual} should be within {delta} of the expected result {expected}"); } #[e2e::test] From f826a72a559cb877e5ec90ee88660a022d3513c1 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Tue, 26 Nov 2024 12:25:53 +0100 Subject: [PATCH 129/138] fix: return SafeErc20 errors on release_erc20 --- contracts/src/finance/vesting_wallet.rs | 20 ++++++------------- contracts/src/token/erc20/utils/safe_erc20.rs | 8 +++++++- examples/vesting-wallet/tests/abi/mod.rs | 2 +- .../vesting-wallet/tests/vesting-wallet.rs | 2 +- 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 07fa7b243..a19624b2d 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -21,6 +21,7 @@ //! automatically (i.e. a rebase token), make sure to account the supply/balance //! adjustment in the vesting schedule to ensure the vested amount is as //! intended. + use alloy_primitives::{Address, U256, U64}; use alloy_sol_types::sol; use openzeppelin_stylus_proc::interface_id; @@ -34,7 +35,7 @@ use stylus_sdk::{ use crate::{ access::ownable::{self, IOwnable, Ownable}, - token::erc20::utils::safe_erc20::{ISafeErc20, SafeErc20}, + token::erc20::utils::safe_erc20::{self, ISafeErc20, SafeErc20}, utils::math::storage::AddAssignUnchecked, }; @@ -59,13 +60,6 @@ sol! { #[allow(missing_docs)] error ReleaseEtherFailed(); - /// Indicates an error related to the underlying ERC-20 `token` transfer. - /// - /// * `token` - Address of the token being released. - #[derive(Debug)] - #[allow(missing_docs)] - error ReleaseTokenFailed(address token); - /// The token address is not valid (eg. `Address::ZERO`). /// /// * `token` - Address of the token being released. @@ -81,10 +75,10 @@ pub enum Error { Ownable(ownable::Error), /// Error type from [`Call`] contract [`call::Error`]. StylusError(call::Error), + /// Error type from [`SafeErc20`] contract [`safe_erc20::Error`]. + SafeErc20(safe_erc20::Error), /// Indicates an error related to the underlying Ether transfer. ReleaseEtherFailed(ReleaseEtherFailed), - /// Indicates an error related to the underlying [`IErc20`] transfer. - ReleaseTokenFailed(ReleaseTokenFailed), /// The token address is not valid. (eg. `Address::ZERO`). InvalidToken(InvalidToken), } @@ -299,7 +293,7 @@ pub trait IVestingWallet { /// If the `token` address is not a contract, then the error /// [`Error::InvalidToken`] is returned. /// If the contract fails to execute the call, then the error - /// [`Error::ReleaseTokenFailed`] is returned. + /// [`safe_erc20::Error::SafeErc20FailedOperation`] is returned. /// /// # Events /// @@ -438,9 +432,7 @@ impl IVestingWallet for VestingWallet { let amount = self.releasable_erc20(token)?; let owner = self.ownable.owner(); - self.safe_erc20.safe_transfer(token, owner, amount).map_err(|_| { - Error::ReleaseTokenFailed(ReleaseTokenFailed { token }) - })?; + self.safe_erc20.safe_transfer(token, owner, amount)?; // SAFETY: total vested amount is by definition smaller than or equal to // the total allocation, which is already verified to be smaller than diff --git a/contracts/src/token/erc20/utils/safe_erc20.rs b/contracts/src/token/erc20/utils/safe_erc20.rs index 0d005f9ff..28115e227 100644 --- a/contracts/src/token/erc20/utils/safe_erc20.rs +++ b/contracts/src/token/erc20/utils/safe_erc20.rs @@ -10,7 +10,7 @@ use alloy_primitives::{Address, U256}; use alloy_sol_types::{sol, SolCall}; use stylus_sdk::{ - call::RawCall, + call::{MethodError, RawCall}, contract::address, evm::gas_left, function_selector, @@ -54,6 +54,12 @@ pub enum Error { SafeErc20FailedDecreaseAllowance(SafeErc20FailedDecreaseAllowance), } +impl MethodError for Error { + fn encode(self) -> alloc::vec::Vec { + self.into() + } +} + pub use token::*; #[allow(missing_docs)] mod token { diff --git a/examples/vesting-wallet/tests/abi/mod.rs b/examples/vesting-wallet/tests/abi/mod.rs index ed2244f4a..361eecfa7 100644 --- a/examples/vesting-wallet/tests/abi/mod.rs +++ b/examples/vesting-wallet/tests/abi/mod.rs @@ -21,7 +21,7 @@ sol!( error OwnableUnauthorizedAccount(address account); error OwnableInvalidOwner(address owner); error ReleaseEtherFailed(); - error ReleaseTokenFailed(address token); + error SafeErc20FailedOperation(address token); error InvalidToken(address token); #[derive(Debug, PartialEq)] diff --git a/examples/vesting-wallet/tests/vesting-wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs index 5aa0ea350..8fcbaf2e9 100644 --- a/examples/vesting-wallet/tests/vesting-wallet.rs +++ b/examples/vesting-wallet/tests/vesting-wallet.rs @@ -429,7 +429,7 @@ mod erc20_vesting { let err = send!(contract.release_1(erc20_address)) .expect_err("should not release when transfer fails"); - assert!(err.reverted_with(VestingWallet::ReleaseTokenFailed { + assert!(err.reverted_with(VestingWallet::SafeErc20FailedOperation { token: erc20_address })); From 68cf3608ba479105ad7650179ddfc4310d013bb7 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Tue, 26 Nov 2024 12:52:24 +0100 Subject: [PATCH 130/138] fix: pass through the underlying call error when releasing eth --- contracts/src/finance/vesting_wallet.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index a19624b2d..ccd206ee2 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -27,7 +27,7 @@ use alloy_sol_types::sol; use openzeppelin_stylus_proc::interface_id; use stylus_sdk::{ block, - call::{self, transfer_eth, Call}, + call::{self, call, Call}, contract, evm, function_selector, storage::TopLevelStorage, stylus_proc::{public, sol_storage, SolidityError}, @@ -73,12 +73,10 @@ sol! { pub enum Error { /// Error type from [`Ownable`] contract [`ownable::Error`]. Ownable(ownable::Error), - /// Error type from [`Call`] contract [`call::Error`]. - StylusError(call::Error), + /// Indicates an error related to the underlying Ether transfer. + ReleaseEtherFailed(call::Error), /// Error type from [`SafeErc20`] contract [`safe_erc20::Error`]. SafeErc20(safe_erc20::Error), - /// Indicates an error related to the underlying Ether transfer. - ReleaseEtherFailed(ReleaseEtherFailed), /// The token address is not valid. (eg. `Address::ZERO`). InvalidToken(InvalidToken), } @@ -419,8 +417,9 @@ impl IVestingWallet for VestingWallet { // `U256::MAX` in [`Self::releasable_eth`]. self._released.set(self._released.get() + amount); - transfer_eth(self.ownable.owner(), amount) - .map_err(|_| Error::ReleaseEtherFailed(ReleaseEtherFailed {}))?; + let owner = self.ownable.owner(); + + call(Call::new_in(self).value(amount), owner, &[])?; evm::log(EtherReleased { amount }); From cde06396585cf9155cebd24b66247862f1042446 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Tue, 26 Nov 2024 12:53:32 +0100 Subject: [PATCH 131/138] ref(test): explicitly import BlockTransactionsKind --- examples/vesting-wallet/tests/vesting-wallet.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/examples/vesting-wallet/tests/vesting-wallet.rs b/examples/vesting-wallet/tests/vesting-wallet.rs index 8fcbaf2e9..50f0b48e8 100644 --- a/examples/vesting-wallet/tests/vesting-wallet.rs +++ b/examples/vesting-wallet/tests/vesting-wallet.rs @@ -5,6 +5,7 @@ use alloy::{ eips::BlockId, primitives::{Address, U256}, providers::Provider, + rpc::types::BlockTransactionsKind, sol, }; use e2e::{ @@ -38,10 +39,7 @@ fn ctr( async fn block_timestamp(account: &Account) -> eyre::Result { let timestamp = account .wallet - .get_block( - BlockId::latest(), - alloy::rpc::types::BlockTransactionsKind::Full, - ) + .get_block(BlockId::latest(), BlockTransactionsKind::Full) .await? .expect("latest block should exist") .header From 729a646c3d5f5a5921c604a6e9ee2c15500f4d2d Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Tue, 26 Nov 2024 13:08:28 +0100 Subject: [PATCH 132/138] ref(test): add init method to motsu tests --- contracts/src/finance/vesting_wallet.rs | 44 ++++++++++++------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index ccd206ee2..6c7ff90d5 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -520,39 +520,44 @@ mod tests { const TOKEN: Address = address!("A11CEacF9aa32246d767FCCD72e02d6bCbcC375d"); const DURATION: u64 = 4 * 365 * 86400; // 4 years - fn start() -> U64 { - U64::from(block::timestamp() + 3600) // 1 hour + fn start() -> u64 { + block::timestamp() + 3600 // 1 hour + } + + fn init( + contract: &mut VestingWallet, + start: u64, + duration: u64, + ) -> (U64, U64) { + let start = U64::from(start); + let duration = U64::from(duration); + contract._start.set(start); + contract._duration.set(duration); + (start, duration) } #[motsu::test] fn reads_start(contract: VestingWallet) { - let start = start(); - contract._start.set(start); + let (start, _) = init(contract, start(), 0); assert_eq!(U256::from(start), contract.start()); } #[motsu::test] fn reads_duration(contract: VestingWallet) { - contract._duration.set(U64::from(DURATION)); - assert_eq!(U256::from(DURATION), contract.duration()); + let (_, duration) = init(contract, 0, DURATION); + assert_eq!(U256::from(duration), contract.duration()); } #[motsu::test] fn reads_end(contract: VestingWallet) { - let start = start(); - let duration = U64::from(DURATION); - contract._start.set(start); - contract._duration.set(duration); + let (start, duration) = init(contract, start(), DURATION); assert_eq!(U256::from(start + duration), contract.end()); } #[motsu::test] fn reads_max_end(contract: VestingWallet) { - let start = U64::MAX; - let duration = U64::MAX; - contract._start.set(start); - contract._duration.set(duration); - assert_eq!(U256::from(start) + U256::from(duration), contract.end()); + init(contract, u64::MAX, u64::MAX); + assert_eq!(U256::from(U64::MAX) + U256::from(U64::MAX), contract.end()); } #[motsu::test] @@ -571,10 +576,7 @@ mod tests { #[motsu::test] fn gets_vesting_schedule(contract: VestingWallet) { - let start = start(); - let duration = U64::from(DURATION); - contract._start.set(start); - contract._duration.set(duration); + let (start, duration) = init(contract, start(), DURATION); let one = uint!(1_U256); let two = uint!(2_U256); @@ -596,9 +598,7 @@ mod tests { #[motsu::test] fn gets_vesting_schedule_zero_duration(contract: VestingWallet) { - let start = start(); - contract._start.set(start); - contract._duration.set(U64::ZERO); + let (start, _) = init(contract, start(), 0); let two = uint!(2_U256); From 14e736e17c44e30d4030e5a7f5076b39cbce8a89 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Tue, 26 Nov 2024 13:09:56 +0100 Subject: [PATCH 133/138] chore: update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d57edde15..1810fe62c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- +- Implement `MethodError` for `safe_erc20::Error`. #402 ### Fixed From 2a272f3f3fda2e7dd877d281c9a463a696dadacb Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Wed, 27 Nov 2024 08:14:56 +0100 Subject: [PATCH 134/138] ref: use checked_add in release fns --- contracts/src/finance/vesting_wallet.rs | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 6c7ff90d5..256d5c0c6 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -36,7 +36,6 @@ use stylus_sdk::{ use crate::{ access::ownable::{self, IOwnable, Ownable}, token::erc20::utils::safe_erc20::{self, ISafeErc20, SafeErc20}, - utils::math::storage::AddAssignUnchecked, }; sol! { @@ -412,10 +411,13 @@ impl IVestingWallet for VestingWallet { #[selector(name = "release")] fn release_eth(&mut self) -> Result<(), Self::Error> { let amount = self.releasable_eth(); - // SAFETY: total vested amount is by definition smaller than or equal to - // the total allocation, which is already verified to be smaller than - // `U256::MAX` in [`Self::releasable_eth`]. - self._released.set(self._released.get() + amount); + + let released = self + ._released + .get() + .checked_add(amount) + .expect("total released should not exceed `U256::MAX`"); + self._released.set(released); let owner = self.ownable.owner(); @@ -433,10 +435,12 @@ impl IVestingWallet for VestingWallet { self.safe_erc20.safe_transfer(token, owner, amount)?; - // SAFETY: total vested amount is by definition smaller than or equal to - // the total allocation, which is already verified to be smaller than - // `U256::MAX` in [`Self::releasable_erc20`]. - self._erc20_released.setter(token).add_assign_unchecked(amount); + let released = self + ._erc20_released + .get(token) + .checked_add(amount) + .expect("total released should not exceed `U256::MAX`"); + self._erc20_released.setter(token).set(released); evm::log(ERC20Released { token, amount }); From eba07b45b6c21241130da9160cb278d55b146272 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Thu, 28 Nov 2024 12:12:50 +0100 Subject: [PATCH 135/138] fix: remove redundant mut self from vested_amount & releasable --- contracts/src/finance/vesting_wallet.rs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 256d5c0c6..8e3b183ce 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -240,7 +240,7 @@ pub trait IVestingWallet { /// /// # Arguments /// - /// * `&mut self` - Write access to the contract's state. + /// * `&self` - Read access to the contract's state. /// * `token` - Address of the releasable token. /// /// # Errors @@ -253,8 +253,7 @@ pub trait IVestingWallet { /// If total allocation exceeds `U256::MAX`. /// If scaled, total allocation (mid calculation) exceeds `U256::MAX`. #[selector(name = "releasable")] - fn releasable_erc20(&mut self, token: Address) - -> Result; + fn releasable_erc20(&self, token: Address) -> Result; /// Release the native tokens (Ether) that have already vested. /// @@ -323,7 +322,7 @@ pub trait IVestingWallet { /// /// # Arguments /// - /// * `&mut self` - Write access to the contract's state. + /// * `&self` - Read access to the contract's state. /// * `token` - Address of the token being released. /// * `timestamp` - Point in time for which to check the vested amount. /// @@ -338,7 +337,7 @@ pub trait IVestingWallet { /// If scaled, total allocation (mid calculation) exceeds `U256::MAX`. #[selector(name = "vestedAmount")] fn vested_amount_erc20( - &mut self, + &self, token: Address, timestamp: u64, ) -> Result; @@ -398,10 +397,7 @@ impl IVestingWallet for VestingWallet { } #[selector(name = "releasable")] - fn releasable_erc20( - &mut self, - token: Address, - ) -> Result { + fn releasable_erc20(&self, token: Address) -> Result { let vested = self.vested_amount_erc20(token, block::timestamp())?; // SAFETY: total vested amount is by definition greater than or equal to // the released amount. @@ -458,14 +454,13 @@ impl IVestingWallet for VestingWallet { #[selector(name = "vestedAmount")] fn vested_amount_erc20( - &mut self, + &self, token: Address, timestamp: u64, ) -> Result { let erc20 = IErc20::new(token); - let call = Call::new_in(self); let balance = erc20 - .balance_of(call, contract::address()) + .balance_of(Call::new(), contract::address()) .map_err(|_| InvalidToken { token })?; let total_allocation = balance From fa0ebab5d4a28a50145b5b8e242e40d6da3065c2 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Thu, 28 Nov 2024 12:14:28 +0100 Subject: [PATCH 136/138] fix: call safe_transfer after state updates --- contracts/src/finance/vesting_wallet.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/src/finance/vesting_wallet.rs b/contracts/src/finance/vesting_wallet.rs index 8e3b183ce..24b0e2a89 100644 --- a/contracts/src/finance/vesting_wallet.rs +++ b/contracts/src/finance/vesting_wallet.rs @@ -429,8 +429,6 @@ impl IVestingWallet for VestingWallet { let amount = self.releasable_erc20(token)?; let owner = self.ownable.owner(); - self.safe_erc20.safe_transfer(token, owner, amount)?; - let released = self ._erc20_released .get(token) @@ -438,6 +436,8 @@ impl IVestingWallet for VestingWallet { .expect("total released should not exceed `U256::MAX`"); self._erc20_released.setter(token).set(released); + self.safe_erc20.safe_transfer(token, owner, amount)?; + evm::log(ERC20Released { token, amount }); Ok(()) From 0f1f60c2379d57f37cff4b98d0dc24a6c004d9b2 Mon Sep 17 00:00:00 2001 From: Daniel Bigos Date: Thu, 28 Nov 2024 18:05:05 +0100 Subject: [PATCH 137/138] ref: update GUIDELINES.md --- GUIDELINES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GUIDELINES.md b/GUIDELINES.md index 6cee6da08..1ba87d1e8 100644 --- a/GUIDELINES.md +++ b/GUIDELINES.md @@ -3,7 +3,7 @@ ## Setup 1. Install [Docker]. -1. Install the [Solidity Compiler] version `0.8.24` +1. Install the [Solidity Compiler] version `0.8.24`. (NOTE: it is important to use this exact version to avoid compatibility issues). 1. Install toolchain providing `cargo` using [rustup]. 1. Install the cargo stylus tool with `cargo install --force cargo-stylus`. From f5892d2a2a4ae1e6eceb2c7cf0a4a95984c7a6b7 Mon Sep 17 00:00:00 2001 From: Daniel Bigos Date: Thu, 28 Nov 2024 18:05:17 +0100 Subject: [PATCH 138/138] ref: update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0efc7ddcd..19f5f6885 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `VestingWallet` contract. #402 -- `IErc1155Burnable` extension. #417 +- `Erc1155Burnable` extension. #417 ### Changed