From 82b3173d79a724ddac14d702339666d4b45f048c Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 26 Sep 2024 10:19:14 -0500 Subject: [PATCH 01/90] start erc4626 --- packages/token/src/erc20/extensions.cairo | 1 + .../token/src/erc20/extensions/erc4626.cairo | 2 + .../erc20/extensions/erc4626/erc4626.cairo | 77 +++++++++++++++++++ .../erc20/extensions/erc4626/interface.cairo | 28 +++++++ 4 files changed, 108 insertions(+) create mode 100644 packages/token/src/erc20/extensions/erc4626.cairo create mode 100644 packages/token/src/erc20/extensions/erc4626/erc4626.cairo create mode 100644 packages/token/src/erc20/extensions/erc4626/interface.cairo diff --git a/packages/token/src/erc20/extensions.cairo b/packages/token/src/erc20/extensions.cairo index e45cdcbf3..20aaef469 100644 --- a/packages/token/src/erc20/extensions.cairo +++ b/packages/token/src/erc20/extensions.cairo @@ -1,3 +1,4 @@ pub mod erc20_votes; +pub mod erc4626; pub use erc20_votes::ERC20VotesComponent; diff --git a/packages/token/src/erc20/extensions/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626.cairo new file mode 100644 index 000000000..62fa2d71e --- /dev/null +++ b/packages/token/src/erc20/extensions/erc4626.cairo @@ -0,0 +1,2 @@ +pub mod erc4626; +pub mod interface; diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo new file mode 100644 index 000000000..2ea0ccfca --- /dev/null +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.17.0 (token/erc20/extensions/erc4626/erc4626.cairo) + +/// # ERC4626 Component +/// +/// ADD MEEEEEEEEEEEEEEEEE AHHHH +#[starknet::component] +pub mod ERC4626Component { + use crate::erc20::extensions::erc4626::interface::IERC4626; + use crate::erc20::ERC20Component; + use crate::erc20::interface::IERC20; + use starknet::ContractAddress; + //use starknet::storage::{ + // Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess + //}; + + // This default decimals is only used when the DefaultConfig + // is in scope in the implementing contract. + pub const DEFAULT_DECIMALS: u8 = 18; + + #[storage] + pub struct Storage {} + + #[event] + #[derive(Drop, PartialEq, starknet::Event)] + pub enum Event {} + + pub mod Errors {} + + /// Constants expected to be defined at the contract level used to configure the component + /// behaviour. + /// + /// + //pub trait ImmutableConfig { + // const ASSET: ContractAddress; + // const DECIMALS: u128; +// + // fn validate() {} + //} + + #[embeddable_as(ERC4626Impl)] + impl ERC4626< + TContractState, + +HasComponent, + +ERC20Component::HasComponent, + +ERC20Component::ERC20HooksTrait, + +Drop + > of IERC4626> { + fn asset(self: @ComponentState) -> ContractAddress { + let this = starknet::get_contract_address(); + return this; + } + } + + #[generate_trait] + pub impl InternalImpl< + TContractState, + +HasComponent, + //impl Immutable: ImmutableConfig, + +Drop + > of InternalTrait { + fn initializer( + ref self: ComponentState) { + //ImmutableConfig::validate(); + } + } +} + +/// Implementation of the default ERC2981Component ImmutableConfig. +/// +/// See +/// https://github.com/starknet-io/SNIPs/blob/963848f0752bde75c7087c2446d83b7da8118b25/SNIPS/snip-107.md#defaultconfig-implementation +/// +/// The default decimals is set to `DEFAULT_DECIMALS`. +//pub impl DefaultConfig of ERC2981Component::ImmutableConfig { +// const UNDERLYING_DECIMALS: u8 = ERC4626::DEFAULT_DECIMALS; +//} \ No newline at end of file diff --git a/packages/token/src/erc20/extensions/erc4626/interface.cairo b/packages/token/src/erc20/extensions/erc4626/interface.cairo new file mode 100644 index 000000000..23c8fcfe9 --- /dev/null +++ b/packages/token/src/erc20/extensions/erc4626/interface.cairo @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.17.0 (token/erc20/extensions/erc4626/interface.cairo) + +use starknet::ContractAddress; + +#[starknet::interface] +pub trait IERC4626 { + fn asset(self: @TState) -> ContractAddress; + //fn total_assets(self: @TState) -> u256; + //fn convert_to_shares(self: @TState, assets: u256) -> u256; + //fn convert_to_assets(self: @TState, shares: u256) -> u256; + //fn max_deposit(self: @TState, receiver: ContractAddress) -> u256; + //fn preview_deposit(self: @TState, assets: u256) -> u256; + //fn deposit(ref self: TState, assets: u256, receiver: ContractAddress) -> u256; + //fn max_mint(self: @TState, receiver: ContractAddress) -> u256; + //fn previous_mint(self: @TState, shares: u256) -> u256; + //fn mint(ref self: TState, shares: u256, receiver: ContractAddress) -> u256; + //fn max_withdrawal(self: @TState, owner: ContractAddress) -> u256; + //fn preview_withdrawal(self: @TState, assets: u256) -> u256; + //fn withdraw( + // ref self: TState, assets: u256, receiver: ContractAddress, owner: ContractAddress + //) -> u256; + //fn max_redeem(self: @TState, owner: ContractAddress) -> u256; + //fn preview_redeem(self: @TState, shares: u256) -> u256; + //fn redeem( + // ref self: TState, shares: u256, receiver: ContractAddress, owner: ContractAddress + //) -> u256; +} From f08dd48f346dcc47e47eded3921494587c542cc0 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 27 Sep 2024 11:36:49 -0500 Subject: [PATCH 02/90] add fns from interface --- .../erc20/extensions/erc4626/erc4626.cairo | 109 +++++++++++++++++- .../erc20/extensions/erc4626/interface.cairo | 22 ++-- 2 files changed, 117 insertions(+), 14 deletions(-) diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo index 2ea0ccfca..b551809b8 100644 --- a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -6,6 +6,7 @@ /// ADD MEEEEEEEEEEEEEEEEE AHHHH #[starknet::component] pub mod ERC4626Component { + use core::num::traits::Bounded; use crate::erc20::extensions::erc4626::interface::IERC4626; use crate::erc20::ERC20Component; use crate::erc20::interface::IERC20; @@ -23,9 +24,39 @@ pub mod ERC4626Component { #[event] #[derive(Drop, PartialEq, starknet::Event)] - pub enum Event {} + pub enum Event { + Deposit: Deposit, + Withdraw: Withdraw, + } - pub mod Errors {} + #[derive(Drop, PartialEq, starknet::Event)] + pub struct Deposit { + #[key] + pub sender: ContractAddress, + #[key] + pub owner: ContractAddress, + pub assets: u256, + pub shares: u256 + } + + #[derive(Drop, PartialEq, starknet::Event)] + pub struct Withdraw { + #[key] + pub sender: ContractAddress, + #[key] + pub receiver: ContractAddress, + #[key] + pub owner: ContractAddress, + pub assets: u256, + pub shares: u256 + } + + pub mod Errors { + pub const EXCEEDED_MAX_DEPOSIT: felt252 = 'ERC4626: exceeds max deposit'; + pub const EXCEEDED_MAX_MINT: felt252 = 'ERC4626: exceeds max mint'; + pub const EXCEEDED_MAX_WITHDRAWAL: felt252 = 'ERC4626: exceeds max withdrawal'; + pub const EXCEEDED_MAX_REDEEM: felt252 = 'ERC4626: exceeds max redeem'; + } /// Constants expected to be defined at the contract level used to configure the component /// behaviour. @@ -42,7 +73,7 @@ pub mod ERC4626Component { impl ERC4626< TContractState, +HasComponent, - +ERC20Component::HasComponent, + impl ERC20: ERC20Component::HasComponent, +ERC20Component::ERC20HooksTrait, +Drop > of IERC4626> { @@ -50,6 +81,78 @@ pub mod ERC4626Component { let this = starknet::get_contract_address(); return this; } + + fn total_assets(self: @ComponentState) -> u256 { + let this = starknet::get_contract_address(); + let erc20_component = get_dep_component!(self, ERC20); + erc20_component.balance_of(this) + } + + fn convert_to_shares(self: @ComponentState, assets: u256) -> u256 { + //self._convert_to_shares(assets) + 1 + } + + fn convert_to_assets(self: @ComponentState, shares: u256) -> u256 { + //self._convert_to_assets(shares) + 1 + } + + fn max_deposit(self: @ComponentState, receiver: ContractAddress) -> u256 { + Bounded::MAX + } + + fn preview_deposit(self: @ComponentState, assets: u256) -> u256 { + //self._convertToShares(assets, Math.Rounding.Floor); + 1 + } + + fn deposit(ref self: ComponentState, assets: u256, receiver: ContractAddress) -> u256 { + let max_assets = self.max_deposit(receiver); + assert(assets < max_assets, Errors::EXCEEDED_MAX_DEPOSIT); + + let shares = self.preview_deposit(assets); + let _caller = starknet::get_caller_address(); + //self._deposit(caller, receiver, assets, shares); + shares + } + + fn max_mint(self: @ComponentState, receiver: ContractAddress) -> u256 { + Bounded::MAX + } + + fn preview_mint(self: @ComponentState, shares: u256) -> u256 { + //return _convertToAssets(shares, Math.Rounding.Ceil); + 1 + } + + fn mint( + ref self: ComponentState, shares: u256, receiver: ContractAddress + ) -> u256 { + let max_shares = self.max_mint(receiver); + assert(shares < max_shares, Errors::EXCEEDED_MAX_MINT); + + let assets = self.preview_mint(shares); + let _caller = starknet::get_caller_address(); + //self._deposit(caller, receiver, assets, shares); + assets + } + + fn max_withdrawal(self: @ComponentState, owner: ContractAddress) -> u256 { + //return _convertToAssets(balanceOf(owner), Math.Rounding.Floor); + + //let erc20_component = get_dep_component!(self, ERC20); + //let owner_bal = erc20_component.balance_of(owner); + //self._convert_to_assets(owner_bal); + 1 + } + + fn preview_withdrawal(self: @ComponentState, assets: u256) -> u256 { + //return _convertToShares(assets, Math.Rounding.Ceil); + + // self._convert_to_shares(assets); + 1 + } } #[generate_trait] diff --git a/packages/token/src/erc20/extensions/erc4626/interface.cairo b/packages/token/src/erc20/extensions/erc4626/interface.cairo index 23c8fcfe9..f25348b02 100644 --- a/packages/token/src/erc20/extensions/erc4626/interface.cairo +++ b/packages/token/src/erc20/extensions/erc4626/interface.cairo @@ -6,17 +6,17 @@ use starknet::ContractAddress; #[starknet::interface] pub trait IERC4626 { fn asset(self: @TState) -> ContractAddress; - //fn total_assets(self: @TState) -> u256; - //fn convert_to_shares(self: @TState, assets: u256) -> u256; - //fn convert_to_assets(self: @TState, shares: u256) -> u256; - //fn max_deposit(self: @TState, receiver: ContractAddress) -> u256; - //fn preview_deposit(self: @TState, assets: u256) -> u256; - //fn deposit(ref self: TState, assets: u256, receiver: ContractAddress) -> u256; - //fn max_mint(self: @TState, receiver: ContractAddress) -> u256; - //fn previous_mint(self: @TState, shares: u256) -> u256; - //fn mint(ref self: TState, shares: u256, receiver: ContractAddress) -> u256; - //fn max_withdrawal(self: @TState, owner: ContractAddress) -> u256; - //fn preview_withdrawal(self: @TState, assets: u256) -> u256; + fn total_assets(self: @TState) -> u256; + fn convert_to_shares(self: @TState, assets: u256) -> u256; + fn convert_to_assets(self: @TState, shares: u256) -> u256; + fn max_deposit(self: @TState, receiver: ContractAddress) -> u256; + fn preview_deposit(self: @TState, assets: u256) -> u256; + fn deposit(ref self: TState, assets: u256, receiver: ContractAddress) -> u256; + fn max_mint(self: @TState, receiver: ContractAddress) -> u256; + fn preview_mint(self: @TState, shares: u256) -> u256; + fn mint(ref self: TState, shares: u256, receiver: ContractAddress) -> u256; + fn max_withdrawal(self: @TState, owner: ContractAddress) -> u256; + fn preview_withdrawal(self: @TState, assets: u256) -> u256; //fn withdraw( // ref self: TState, assets: u256, receiver: ContractAddress, owner: ContractAddress //) -> u256; From c558c599c7838059e892e1c559b03bba08c1fc6a Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 30 Sep 2024 20:30:15 -0500 Subject: [PATCH 03/90] start business logic --- .../erc20/extensions/erc4626/erc4626.cairo | 110 +++++++++++++++--- .../erc20/extensions/erc4626/interface.cairo | 16 +-- 2 files changed, 103 insertions(+), 23 deletions(-) diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo index b551809b8..b18ac0f6f 100644 --- a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -9,7 +9,9 @@ pub mod ERC4626Component { use core::num::traits::Bounded; use crate::erc20::extensions::erc4626::interface::IERC4626; use crate::erc20::ERC20Component; + use crate::erc20::ERC20Component::InternalImpl as ERC20InternalImpl; use crate::erc20::interface::IERC20; + use crate::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; use starknet::ContractAddress; //use starknet::storage::{ // Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess @@ -56,36 +58,37 @@ pub mod ERC4626Component { pub const EXCEEDED_MAX_MINT: felt252 = 'ERC4626: exceeds max mint'; pub const EXCEEDED_MAX_WITHDRAWAL: felt252 = 'ERC4626: exceeds max withdrawal'; pub const EXCEEDED_MAX_REDEEM: felt252 = 'ERC4626: exceeds max redeem'; + pub const TOKEN_TRANSFER_FAILED: felt252 = 'ERC4626: Token transfer failed'; } /// Constants expected to be defined at the contract level used to configure the component /// behaviour. /// /// - //pub trait ImmutableConfig { - // const ASSET: ContractAddress; - // const DECIMALS: u128; -// - // fn validate() {} - //} + pub trait ImmutableConfig { + const ASSET: ContractAddress; + const UNDERLYING_DECIMALS: u128; + const DECIMALS_OFFSET: u8; + + fn validate() {} + } #[embeddable_as(ERC4626Impl)] impl ERC4626< TContractState, +HasComponent, + impl Immutable: ImmutableConfig, impl ERC20: ERC20Component::HasComponent, +ERC20Component::ERC20HooksTrait, +Drop > of IERC4626> { fn asset(self: @ComponentState) -> ContractAddress { - let this = starknet::get_contract_address(); - return this; + Immutable::ASSET } fn total_assets(self: @ComponentState) -> u256 { let this = starknet::get_contract_address(); - let erc20_component = get_dep_component!(self, ERC20); - erc20_component.balance_of(this) + IERC20Dispatcher{ contract_address: Immutable::ASSET }.balance_of(this) } fn convert_to_shares(self: @ComponentState, assets: u256) -> u256 { @@ -112,8 +115,8 @@ pub mod ERC4626Component { assert(assets < max_assets, Errors::EXCEEDED_MAX_DEPOSIT); let shares = self.preview_deposit(assets); - let _caller = starknet::get_caller_address(); - //self._deposit(caller, receiver, assets, shares); + let caller = starknet::get_caller_address(); + self._deposit(caller, receiver, assets, shares); shares } @@ -133,8 +136,8 @@ pub mod ERC4626Component { assert(shares < max_shares, Errors::EXCEEDED_MAX_MINT); let assets = self.preview_mint(shares); - let _caller = starknet::get_caller_address(); - //self._deposit(caller, receiver, assets, shares); + let caller = starknet::get_caller_address(); + self._deposit(caller, receiver, assets, shares); assets } @@ -153,19 +156,96 @@ pub mod ERC4626Component { // self._convert_to_shares(assets); 1 } + + fn withdraw( + ref self: ComponentState, assets: u256, receiver: ContractAddress, owner: ContractAddress + ) -> u256 { + let max_assets = self.max_withdrawal(owner); + assert(assets < max_assets, Errors::EXCEEDED_MAX_WITHDRAWAL); + + let shares = self.preview_withdrawal(assets); + let _caller = starknet::get_caller_address(); + //self._withdraw(_caller, receiver, owner, assets, shares); + shares + } + + fn max_redeem(self: @ComponentState, owner: ContractAddress) -> u256 { + let erc20_component = get_dep_component!(self, ERC20); + erc20_component.balance_of(owner) + } + + fn preview_redeem(self: @ComponentState, shares: u256) -> u256 { + //self._convert_to_assets(shares) + 1 + } + + fn redeem( + ref self: ComponentState, shares: u256, receiver: ContractAddress, owner: ContractAddress + ) -> u256 { + let max_shares = self.max_redeem(owner); + assert(shares < max_shares, Errors::EXCEEDED_MAX_REDEEM); + + let assets = self.preview_redeem(shares); + let _caller = starknet::get_caller_address(); + //self._withdraw(_caller, receiver, owner, assets, shares); + assets + } + } #[generate_trait] pub impl InternalImpl< TContractState, +HasComponent, - //impl Immutable: ImmutableConfig, + impl Immutable: ImmutableConfig, + impl ERC20: ERC20Component::HasComponent, + +ERC20Component::ERC20HooksTrait, +Drop > of InternalTrait { fn initializer( ref self: ComponentState) { //ImmutableConfig::validate(); } + + fn _deposit( + ref self: ComponentState, + caller: ContractAddress, + receiver: ContractAddress, + assets: u256, + shares: u256 + ) { + // Transfer assets first + let this = starknet::get_contract_address(); + let asset_dispatcher = IERC20Dispatcher { contract_address: Immutable::ASSET }; + assert(asset_dispatcher.transfer_from(caller, this, assets), Errors::TOKEN_TRANSFER_FAILED); + + // Mint shares after transferring assets + let mut erc20_component = get_dep_component_mut!(ref self, ERC20); + erc20_component.mint(receiver, shares); + self.emit(Deposit { sender: caller, owner: receiver, assets, shares }); + } + + fn _withdraw( + ref self: ComponentState, + caller: ContractAddress, + receiver: ContractAddress, + owner: ContractAddress, + assets: u256, + shares: u256 + ) { + // Burn shares first + let mut erc20_component = get_dep_component_mut!(ref self, ERC20); + if (caller != owner) { + erc20_component._spend_allowance(owner, caller, shares); + } + erc20_component.burn(owner, shares); + + // Transfer assets after burn + let asset_dispatcher = IERC20Dispatcher { contract_address: Immutable::ASSET }; + asset_dispatcher.transfer(receiver, assets); + + self.emit(Withdraw { sender: caller, receiver, owner, assets, shares }); + } } } diff --git a/packages/token/src/erc20/extensions/erc4626/interface.cairo b/packages/token/src/erc20/extensions/erc4626/interface.cairo index f25348b02..21e9bd6ec 100644 --- a/packages/token/src/erc20/extensions/erc4626/interface.cairo +++ b/packages/token/src/erc20/extensions/erc4626/interface.cairo @@ -17,12 +17,12 @@ pub trait IERC4626 { fn mint(ref self: TState, shares: u256, receiver: ContractAddress) -> u256; fn max_withdrawal(self: @TState, owner: ContractAddress) -> u256; fn preview_withdrawal(self: @TState, assets: u256) -> u256; - //fn withdraw( - // ref self: TState, assets: u256, receiver: ContractAddress, owner: ContractAddress - //) -> u256; - //fn max_redeem(self: @TState, owner: ContractAddress) -> u256; - //fn preview_redeem(self: @TState, shares: u256) -> u256; - //fn redeem( - // ref self: TState, shares: u256, receiver: ContractAddress, owner: ContractAddress - //) -> u256; + fn withdraw( + ref self: TState, assets: u256, receiver: ContractAddress, owner: ContractAddress + ) -> u256; + fn max_redeem(self: @TState, owner: ContractAddress) -> u256; + fn preview_redeem(self: @TState, shares: u256) -> u256; + fn redeem( + ref self: TState, shares: u256, receiver: ContractAddress, owner: ContractAddress + ) -> u256; } From 0b572e5113164ed04f465e162a0608665b624be9 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 30 Sep 2024 20:30:45 -0500 Subject: [PATCH 04/90] add mul_div and tests --- packages/utils/src/math.cairo | 146 ++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) diff --git a/packages/utils/src/math.cairo b/packages/utils/src/math.cairo index a21e245d7..0222a8b7e 100644 --- a/packages/utils/src/math.cairo +++ b/packages/utils/src/math.cairo @@ -1,6 +1,9 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts for Cairo v0.17.0 (utils/math.cairo) +use core::integer::{u512_safe_div_rem_by_u256}; +use core::math::u256_mul_mod_n; +use core::num::traits::WideMul; use core::traits::{Into, BitAnd, BitXor}; /// Returns the average of two numbers. The result is rounded down. @@ -19,3 +22,146 @@ pub fn average< // (a + b) / 2 can overflow. (a & b) + (a ^ b) / 2_u8.into() } + +#[derive(Drop, Copy, Debug)] +pub enum Rounding { + Floor, // Toward negative infinity + Ceil, // Toward positive infinity + Trunc, // Toward zero + Expand // Away from zero +} + +fn cast_rounding(rounding: Rounding) -> u8 { + match rounding { + Rounding::Floor => 0, + Rounding::Ceil => 1, + Rounding::Trunc => 2, + Rounding::Expand => 3 + } +} + +fn round_up(rounding: Rounding) -> bool { + let u8_rounding = cast_rounding(rounding); + u8_rounding % 2 == 1 +} + +pub fn u256_mul_div(x: u256, y: u256, denominator: u256, rounding: Rounding) -> u256 { + let q = _raw_u256_mul_div(x, y, denominator); + + // Prepare vars for bitwise op + let felt_is_rounded: felt252 = round_up(rounding).into(); + let mm = u256_mul_mod_n(x, y, denominator.try_into().unwrap()); + let mm_gt_0: felt252 = (mm > 0).into(); + + q + BitAnd::bitand(felt_is_rounded.into(), mm_gt_0.into()) +} + +pub fn _raw_u256_mul_div(x: u256, y: u256, denominator: u256) -> u256 { + assert(denominator != 0, 'Math: division by zero'); + let p = x.wide_mul(y); + let (q, _) = u512_safe_div_rem_by_u256(p, denominator.try_into().unwrap()); + q.try_into().expect('Math: quotient > u256') +} + +#[cfg(test)] +mod Test { + use super::u256_mul_div; + use super::Rounding; + use core::num::traits::Bounded; + + #[test] + #[should_panic(expected: 'Math: division by zero')] + fn test_mul_div_divide_by_zero() { + let x = 1; + let y = 1; + let denominator = 0; + + u256_mul_div(x, y, denominator, Rounding::Floor); + } + + #[test] + #[should_panic(expected: 'Math: quotient > u256')] + fn test_mul_div_result_gt_u256() { + let x = 5; + let y = Bounded::MAX; + let denominator = 2; + + u256_mul_div(x, y, denominator, Rounding::Floor); + } + + #[test] + fn test_mul_div_round_down_small_values() { + let round_down = array![Rounding::Floor, Rounding::Trunc]; + let args_list = array![ + // (x, y, denominator, expected result) + (3, 4, 5, 2), + (3, 5, 5, 3) + ].span(); + + for round in round_down { + for args in args_list { + let (x, y, denominator, expected) = args; + assert_eq!(u256_mul_div(*x, *y, *denominator, round), *expected); + } + } + } + + #[test] + fn test_mul_div_round_down_large_values() { + let round_down = array![Rounding::Floor, Rounding::Trunc]; + let u256_max: u256 = Bounded::MAX; + let args_list = array![ + // (x, y, denominator, expected result) + (42, u256_max - 1, u256_max, 41), + (17, u256_max, u256_max, 17), + (u256_max - 1, u256_max - 1, u256_max, u256_max - 2), + (u256_max, u256_max - 1, u256_max, u256_max - 1), + (u256_max, u256_max, u256_max, u256_max) + ].span(); + + for round in round_down { + for args in args_list { + let (x, y, denominator, expected) = args; + assert_eq!(u256_mul_div(*x, *y, *denominator, round), *expected); + }; + }; + } + + #[test] + fn test_mul_div_round_up_small_values() { + let round_up = array![Rounding::Ceil, Rounding::Expand]; + let args_list = array![ + // (x, y, denominator, expected result) + (3, 4, 5, 3), + (3, 5, 5, 3) + ].span(); + + for round in round_up { + for args in args_list { + let (x, y, denominator, expected) = args; + assert_eq!(u256_mul_div(*x, *y, *denominator, round), *expected); + } + } + } + + #[test] + fn test_mul_div_round_up_large_values() { + let round_up = array![Rounding::Floor, Rounding::Trunc]; + let u256_max: u256 = Bounded::MAX; + let args_list = array![ + // (x, y, denominator, expected result) + (42, u256_max - 1, u256_max, 42), + (17, u256_max, u256_max, 17), + (u256_max - 1, u256_max - 1, u256_max, u256_max - 1), + (u256_max, u256_max - 1, u256_max, u256_max - 1), + (u256_max, u256_max, u256_max, u256_max) + ].span(); + + for round in round_up { + for args in args_list { + let (x, y, denominator, expected) = args; + assert_eq!(u256_mul_div(*x, *y, *denominator, round), *expected); + }; + }; + } +} From 3cadd55085f9544e423aa3c4cd561721b691459b Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 30 Sep 2024 20:47:47 -0500 Subject: [PATCH 05/90] simplify math --- packages/utils/src/math.cairo | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/utils/src/math.cairo b/packages/utils/src/math.cairo index 0222a8b7e..ccc1bb9a0 100644 --- a/packages/utils/src/math.cairo +++ b/packages/utils/src/math.cairo @@ -1,8 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts for Cairo v0.17.0 (utils/math.cairo) -use core::integer::{u512_safe_div_rem_by_u256}; -use core::math::u256_mul_mod_n; +use core::integer::u512_safe_div_rem_by_u256; use core::num::traits::WideMul; use core::traits::{Into, BitAnd, BitXor}; @@ -46,21 +45,21 @@ fn round_up(rounding: Rounding) -> bool { } pub fn u256_mul_div(x: u256, y: u256, denominator: u256, rounding: Rounding) -> u256 { - let q = _raw_u256_mul_div(x, y, denominator); + let (q, r) = _raw_u256_mul_div(x, y, denominator); // Prepare vars for bitwise op - let felt_is_rounded: felt252 = round_up(rounding).into(); - let mm = u256_mul_mod_n(x, y, denominator.try_into().unwrap()); - let mm_gt_0: felt252 = (mm > 0).into(); + let felt_is_round_up: felt252 = round_up(rounding).into(); + let has_remainder: felt252 = (r > 0).into(); - q + BitAnd::bitand(felt_is_rounded.into(), mm_gt_0.into()) + q + BitAnd::bitand(felt_is_round_up.into(), has_remainder.into()) } -pub fn _raw_u256_mul_div(x: u256, y: u256, denominator: u256) -> u256 { +pub fn _raw_u256_mul_div(x: u256, y: u256, denominator: u256) -> (u256, u256) { assert(denominator != 0, 'Math: division by zero'); let p = x.wide_mul(y); - let (q, _) = u512_safe_div_rem_by_u256(p, denominator.try_into().unwrap()); - q.try_into().expect('Math: quotient > u256') + let (mut q, r) = u512_safe_div_rem_by_u256(p, denominator.try_into().unwrap()); + let q = q.try_into().expect('Math: quotient > u256'); + (q, r) } #[cfg(test)] @@ -146,7 +145,7 @@ mod Test { #[test] fn test_mul_div_round_up_large_values() { - let round_up = array![Rounding::Floor, Rounding::Trunc]; + let round_up = array![Rounding::Ceil, Rounding::Expand]; let u256_max: u256 = Bounded::MAX; let args_list = array![ // (x, y, denominator, expected result) From f66bce4cd8e4ae6ce1d4e0bf12a9175e005ba9ec Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 1 Oct 2024 02:05:58 -0500 Subject: [PATCH 06/90] add fix me comments --- .../src/erc20/extensions/erc4626/erc4626.cairo | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo index b18ac0f6f..9b16a2722 100644 --- a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -13,9 +13,6 @@ pub mod ERC4626Component { use crate::erc20::interface::IERC20; use crate::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; use starknet::ContractAddress; - //use starknet::storage::{ - // Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess - //}; // This default decimals is only used when the DefaultConfig // is in scope in the implementing contract. @@ -64,7 +61,7 @@ pub mod ERC4626Component { /// Constants expected to be defined at the contract level used to configure the component /// behaviour. /// - /// + /// ADD ME... pub trait ImmutableConfig { const ASSET: ContractAddress; const UNDERLYING_DECIMALS: u128; @@ -92,11 +89,13 @@ pub mod ERC4626Component { } fn convert_to_shares(self: @ComponentState, assets: u256) -> u256 { + // FIX ME //self._convert_to_shares(assets) 1 } fn convert_to_assets(self: @ComponentState, shares: u256) -> u256 { + // FIX ME //self._convert_to_assets(shares) 1 } @@ -106,6 +105,7 @@ pub mod ERC4626Component { } fn preview_deposit(self: @ComponentState, assets: u256) -> u256 { + // FIX ME //self._convertToShares(assets, Math.Rounding.Floor); 1 } @@ -125,6 +125,7 @@ pub mod ERC4626Component { } fn preview_mint(self: @ComponentState, shares: u256) -> u256 { + // FIX ME //return _convertToAssets(shares, Math.Rounding.Ceil); 1 } @@ -142,6 +143,7 @@ pub mod ERC4626Component { } fn max_withdrawal(self: @ComponentState, owner: ContractAddress) -> u256 { + // FIX ME //return _convertToAssets(balanceOf(owner), Math.Rounding.Floor); //let erc20_component = get_dep_component!(self, ERC20); @@ -151,8 +153,8 @@ pub mod ERC4626Component { } fn preview_withdrawal(self: @ComponentState, assets: u256) -> u256 { + // FIX ME //return _convertToShares(assets, Math.Rounding.Ceil); - // self._convert_to_shares(assets); 1 } @@ -165,6 +167,7 @@ pub mod ERC4626Component { let shares = self.preview_withdrawal(assets); let _caller = starknet::get_caller_address(); + // FIX ME //self._withdraw(_caller, receiver, owner, assets, shares); shares } @@ -175,6 +178,7 @@ pub mod ERC4626Component { } fn preview_redeem(self: @ComponentState, shares: u256) -> u256 { + // FIX ME //self._convert_to_assets(shares) 1 } @@ -187,6 +191,7 @@ pub mod ERC4626Component { let assets = self.preview_redeem(shares); let _caller = starknet::get_caller_address(); + // FIX ME //self._withdraw(_caller, receiver, owner, assets, shares); assets } From 5d7b1d997b20b08c921a7e2bcd9885291939a523 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 1 Oct 2024 02:06:20 -0500 Subject: [PATCH 07/90] fix fmt --- .../erc20/extensions/erc4626/erc4626.cairo | 33 +++++++++++-------- packages/utils/src/math.cairo | 24 ++++++-------- 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo index 9b16a2722..954f13280 100644 --- a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -7,9 +7,9 @@ #[starknet::component] pub mod ERC4626Component { use core::num::traits::Bounded; - use crate::erc20::extensions::erc4626::interface::IERC4626; - use crate::erc20::ERC20Component; use crate::erc20::ERC20Component::InternalImpl as ERC20InternalImpl; + use crate::erc20::ERC20Component; + use crate::erc20::extensions::erc4626::interface::IERC4626; use crate::erc20::interface::IERC20; use crate::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; use starknet::ContractAddress; @@ -85,7 +85,7 @@ pub mod ERC4626Component { fn total_assets(self: @ComponentState) -> u256 { let this = starknet::get_contract_address(); - IERC20Dispatcher{ contract_address: Immutable::ASSET }.balance_of(this) + IERC20Dispatcher { contract_address: Immutable::ASSET }.balance_of(this) } fn convert_to_shares(self: @ComponentState, assets: u256) -> u256 { @@ -110,7 +110,9 @@ pub mod ERC4626Component { 1 } - fn deposit(ref self: ComponentState, assets: u256, receiver: ContractAddress) -> u256 { + fn deposit( + ref self: ComponentState, assets: u256, receiver: ContractAddress + ) -> u256 { let max_assets = self.max_deposit(receiver); assert(assets < max_assets, Errors::EXCEEDED_MAX_DEPOSIT); @@ -160,7 +162,10 @@ pub mod ERC4626Component { } fn withdraw( - ref self: ComponentState, assets: u256, receiver: ContractAddress, owner: ContractAddress + ref self: ComponentState, + assets: u256, + receiver: ContractAddress, + owner: ContractAddress ) -> u256 { let max_assets = self.max_withdrawal(owner); assert(assets < max_assets, Errors::EXCEEDED_MAX_WITHDRAWAL); @@ -184,7 +189,10 @@ pub mod ERC4626Component { } fn redeem( - ref self: ComponentState, shares: u256, receiver: ContractAddress, owner: ContractAddress + ref self: ComponentState, + shares: u256, + receiver: ContractAddress, + owner: ContractAddress ) -> u256 { let max_shares = self.max_redeem(owner); assert(shares < max_shares, Errors::EXCEEDED_MAX_REDEEM); @@ -195,7 +203,6 @@ pub mod ERC4626Component { //self._withdraw(_caller, receiver, owner, assets, shares); assets } - } #[generate_trait] @@ -207,9 +214,7 @@ pub mod ERC4626Component { +ERC20Component::ERC20HooksTrait, +Drop > of InternalTrait { - fn initializer( - ref self: ComponentState) { - //ImmutableConfig::validate(); + fn initializer(ref self: ComponentState) {//ImmutableConfig::validate(); } fn _deposit( @@ -222,7 +227,9 @@ pub mod ERC4626Component { // Transfer assets first let this = starknet::get_contract_address(); let asset_dispatcher = IERC20Dispatcher { contract_address: Immutable::ASSET }; - assert(asset_dispatcher.transfer_from(caller, this, assets), Errors::TOKEN_TRANSFER_FAILED); + assert( + asset_dispatcher.transfer_from(caller, this, assets), Errors::TOKEN_TRANSFER_FAILED + ); // Mint shares after transferring assets let mut erc20_component = get_dep_component_mut!(ref self, ERC20); @@ -253,7 +260,6 @@ pub mod ERC4626Component { } } } - /// Implementation of the default ERC2981Component ImmutableConfig. /// /// See @@ -262,4 +268,5 @@ pub mod ERC4626Component { /// The default decimals is set to `DEFAULT_DECIMALS`. //pub impl DefaultConfig of ERC2981Component::ImmutableConfig { // const UNDERLYING_DECIMALS: u8 = ERC4626::DEFAULT_DECIMALS; -//} \ No newline at end of file +//} + diff --git a/packages/utils/src/math.cairo b/packages/utils/src/math.cairo index ccc1bb9a0..f2630ca53 100644 --- a/packages/utils/src/math.cairo +++ b/packages/utils/src/math.cairo @@ -64,9 +64,9 @@ pub fn _raw_u256_mul_div(x: u256, y: u256, denominator: u256) -> (u256, u256) { #[cfg(test)] mod Test { - use super::u256_mul_div; - use super::Rounding; use core::num::traits::Bounded; + use super::Rounding; + use super::u256_mul_div; #[test] #[should_panic(expected: 'Math: division by zero')] @@ -91,11 +91,8 @@ mod Test { #[test] fn test_mul_div_round_down_small_values() { let round_down = array![Rounding::Floor, Rounding::Trunc]; - let args_list = array![ - // (x, y, denominator, expected result) - (3, 4, 5, 2), - (3, 5, 5, 3) - ].span(); + let args_list = array![// (x, y, denominator, expected result) + (3, 4, 5, 2), (3, 5, 5, 3)].span(); for round in round_down { for args in args_list { @@ -116,7 +113,8 @@ mod Test { (u256_max - 1, u256_max - 1, u256_max, u256_max - 2), (u256_max, u256_max - 1, u256_max, u256_max - 1), (u256_max, u256_max, u256_max, u256_max) - ].span(); + ] + .span(); for round in round_down { for args in args_list { @@ -129,11 +127,8 @@ mod Test { #[test] fn test_mul_div_round_up_small_values() { let round_up = array![Rounding::Ceil, Rounding::Expand]; - let args_list = array![ - // (x, y, denominator, expected result) - (3, 4, 5, 3), - (3, 5, 5, 3) - ].span(); + let args_list = array![// (x, y, denominator, expected result) + (3, 4, 5, 3), (3, 5, 5, 3)].span(); for round in round_up { for args in args_list { @@ -154,7 +149,8 @@ mod Test { (u256_max - 1, u256_max - 1, u256_max, u256_max - 1), (u256_max, u256_max - 1, u256_max, u256_max - 1), (u256_max, u256_max, u256_max, u256_max) - ].span(); + ] + .span(); for round in round_up { for args in args_list { From 33bd0d476594b1af99e1c3d2090277c7cf0bfb1c Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 1 Oct 2024 02:23:06 -0500 Subject: [PATCH 08/90] fix fmt --- .../token/src/erc20/extensions/erc4626/erc4626.cairo | 3 ++- packages/utils/src/math.cairo | 10 ++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo index 954f13280..acc31a086 100644 --- a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -214,7 +214,7 @@ pub mod ERC4626Component { +ERC20Component::ERC20HooksTrait, +Drop > of InternalTrait { - fn initializer(ref self: ComponentState) {//ImmutableConfig::validate(); + fn initializer(ref self: ComponentState) { //ImmutableConfig::validate(); } fn _deposit( @@ -270,3 +270,4 @@ pub mod ERC4626Component { // const UNDERLYING_DECIMALS: u8 = ERC4626::DEFAULT_DECIMALS; //} + diff --git a/packages/utils/src/math.cairo b/packages/utils/src/math.cairo index f2630ca53..ecf1203c7 100644 --- a/packages/utils/src/math.cairo +++ b/packages/utils/src/math.cairo @@ -91,8 +91,9 @@ mod Test { #[test] fn test_mul_div_round_down_small_values() { let round_down = array![Rounding::Floor, Rounding::Trunc]; - let args_list = array![// (x, y, denominator, expected result) - (3, 4, 5, 2), (3, 5, 5, 3)].span(); + let args_list = array![ // (x, y, denominator, expected result) + (3, 4, 5, 2), (3, 5, 5, 3)] + .span(); for round in round_down { for args in args_list { @@ -127,8 +128,9 @@ mod Test { #[test] fn test_mul_div_round_up_small_values() { let round_up = array![Rounding::Ceil, Rounding::Expand]; - let args_list = array![// (x, y, denominator, expected result) - (3, 4, 5, 3), (3, 5, 5, 3)].span(); + let args_list = array![ // (x, y, denominator, expected result) + (3, 4, 5, 3), (3, 5, 5, 3)] + .span(); for round in round_up { for args in args_list { From ce22c2f001ec0e4afd6b508eddeec5bb836b2a59 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 2 Oct 2024 11:18:33 -0500 Subject: [PATCH 09/90] add convert_to logic, add metadata impl --- .../erc20/extensions/erc4626/erc4626.cairo | 135 +++++++++++------- 1 file changed, 86 insertions(+), 49 deletions(-) diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo index acc31a086..e93310492 100644 --- a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -6,20 +6,26 @@ /// ADD MEEEEEEEEEEEEEEEEE AHHHH #[starknet::component] pub mod ERC4626Component { - use core::num::traits::Bounded; + use core::num::traits::{Bounded, Zero}; use crate::erc20::ERC20Component::InternalImpl as ERC20InternalImpl; use crate::erc20::ERC20Component; use crate::erc20::extensions::erc4626::interface::IERC4626; - use crate::erc20::interface::IERC20; + use crate::erc20::interface::{IERC20, IERC20Metadata}; use crate::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; use starknet::ContractAddress; + use openzeppelin_utils::math; + use openzeppelin_utils::math::Rounding; + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; - // This default decimals is only used when the DefaultConfig + // The defualt values are only used when the DefaultConfig // is in scope in the implementing contract. - pub const DEFAULT_DECIMALS: u8 = 18; + pub const DEFAULT_UNDERLYING_DECIMALS: u8 = 18; + pub const DEFAULT_DECIMALS_OFFSET: u8 = 0; #[storage] - pub struct Storage {} + pub struct Storage { + ERC4626_asset: ContractAddress + } #[event] #[derive(Drop, PartialEq, starknet::Event)] @@ -55,7 +61,8 @@ pub mod ERC4626Component { pub const EXCEEDED_MAX_MINT: felt252 = 'ERC4626: exceeds max mint'; pub const EXCEEDED_MAX_WITHDRAWAL: felt252 = 'ERC4626: exceeds max withdrawal'; pub const EXCEEDED_MAX_REDEEM: felt252 = 'ERC4626: exceeds max redeem'; - pub const TOKEN_TRANSFER_FAILED: felt252 = 'ERC4626: Token transfer failed'; + pub const TOKEN_TRANSFER_FAILED: felt252 = 'ERC4626: token transfer failed'; + pub const INVALID_ASSET_ADDRESS: felt252 = 'ERC4626: asset address set to 0'; } /// Constants expected to be defined at the contract level used to configure the component @@ -63,8 +70,7 @@ pub mod ERC4626Component { /// /// ADD ME... pub trait ImmutableConfig { - const ASSET: ContractAddress; - const UNDERLYING_DECIMALS: u128; + const UNDERLYING_DECIMALS: u8; const DECIMALS_OFFSET: u8; fn validate() {} @@ -80,24 +86,20 @@ pub mod ERC4626Component { +Drop > of IERC4626> { fn asset(self: @ComponentState) -> ContractAddress { - Immutable::ASSET + self.ERC4626_asset.read() } fn total_assets(self: @ComponentState) -> u256 { let this = starknet::get_contract_address(); - IERC20Dispatcher { contract_address: Immutable::ASSET }.balance_of(this) + IERC20Dispatcher { contract_address: self.ERC4626_asset.read() }.balance_of(this) } fn convert_to_shares(self: @ComponentState, assets: u256) -> u256 { - // FIX ME - //self._convert_to_shares(assets) - 1 + self._convert_to_shares(assets, Rounding::Floor) } fn convert_to_assets(self: @ComponentState, shares: u256) -> u256 { - // FIX ME - //self._convert_to_assets(shares) - 1 + self._convert_to_assets(shares, Rounding::Floor) } fn max_deposit(self: @ComponentState, receiver: ContractAddress) -> u256 { @@ -105,9 +107,7 @@ pub mod ERC4626Component { } fn preview_deposit(self: @ComponentState, assets: u256) -> u256 { - // FIX ME - //self._convertToShares(assets, Math.Rounding.Floor); - 1 + self._convert_to_shares(assets, Rounding::Floor) } fn deposit( @@ -127,9 +127,7 @@ pub mod ERC4626Component { } fn preview_mint(self: @ComponentState, shares: u256) -> u256 { - // FIX ME - //return _convertToAssets(shares, Math.Rounding.Ceil); - 1 + self._convert_to_assets(shares, Rounding::Ceil) } fn mint( @@ -145,20 +143,13 @@ pub mod ERC4626Component { } fn max_withdrawal(self: @ComponentState, owner: ContractAddress) -> u256 { - // FIX ME - //return _convertToAssets(balanceOf(owner), Math.Rounding.Floor); - - //let erc20_component = get_dep_component!(self, ERC20); - //let owner_bal = erc20_component.balance_of(owner); - //self._convert_to_assets(owner_bal); - 1 + let erc20_component = get_dep_component!(self, ERC20); + let owner_bal = erc20_component.balance_of(owner); + self._convert_to_assets(owner_bal, Rounding::Floor) } fn preview_withdrawal(self: @ComponentState, assets: u256) -> u256 { - // FIX ME - //return _convertToShares(assets, Math.Rounding.Ceil); - // self._convert_to_shares(assets); - 1 + self._convert_to_shares(assets, Rounding::Ceil) } fn withdraw( @@ -171,9 +162,9 @@ pub mod ERC4626Component { assert(assets < max_assets, Errors::EXCEEDED_MAX_WITHDRAWAL); let shares = self.preview_withdrawal(assets); - let _caller = starknet::get_caller_address(); - // FIX ME - //self._withdraw(_caller, receiver, owner, assets, shares); + let caller = starknet::get_caller_address(); + self._withdraw(caller, receiver, owner, assets, shares); + shares } @@ -183,9 +174,7 @@ pub mod ERC4626Component { } fn preview_redeem(self: @ComponentState, shares: u256) -> u256 { - // FIX ME - //self._convert_to_assets(shares) - 1 + self._convert_to_assets(shares, Rounding::Floor) } fn redeem( @@ -198,13 +187,38 @@ pub mod ERC4626Component { assert(shares < max_shares, Errors::EXCEEDED_MAX_REDEEM); let assets = self.preview_redeem(shares); - let _caller = starknet::get_caller_address(); - // FIX ME - //self._withdraw(_caller, receiver, owner, assets, shares); + let caller = starknet::get_caller_address(); + self._withdraw(caller, receiver, owner, assets, shares); + assets } } + #[embeddable_as(ERC4626MetadataImpl)] + impl ERC4626Metadata< + TContractState, + +HasComponent, + impl Immutable: ImmutableConfig, + impl ERC20: ERC20Component::HasComponent, + > of IERC20Metadata> { + /// Returns the name of the token. + fn name(self: @ComponentState) -> ByteArray { + let erc20_component = get_dep_component!(self, ERC20); + erc20_component.ERC20_name.read() + } + + /// Returns the ticker symbol of the token, usually a shorter version of the name. + fn symbol(self: @ComponentState) -> ByteArray { + let erc20_component = get_dep_component!(self, ERC20); + erc20_component.ERC20_symbol.read() + } + + /// Returns the number of decimals used to get its user representation. + fn decimals(self: @ComponentState) -> u8 { + Immutable::UNDERLYING_DECIMALS + Immutable::DECIMALS_OFFSET + } + } + #[generate_trait] pub impl InternalImpl< TContractState, @@ -214,7 +228,10 @@ pub mod ERC4626Component { +ERC20Component::ERC20HooksTrait, +Drop > of InternalTrait { - fn initializer(ref self: ComponentState) { //ImmutableConfig::validate(); + fn initializer(ref self: ComponentState, asset_address: ContractAddress) { + //ImmutableConfig::validate(); + assert(!asset_address.is_zero(), Errors::INVALID_ASSET_ADDRESS); + self.ERC4626_asset.write(asset_address); } fn _deposit( @@ -226,7 +243,7 @@ pub mod ERC4626Component { ) { // Transfer assets first let this = starknet::get_contract_address(); - let asset_dispatcher = IERC20Dispatcher { contract_address: Immutable::ASSET }; + let asset_dispatcher = IERC20Dispatcher { contract_address: self.ERC4626_asset.read() }; assert( asset_dispatcher.transfer_from(caller, this, assets), Errors::TOKEN_TRANSFER_FAILED ); @@ -253,21 +270,41 @@ pub mod ERC4626Component { erc20_component.burn(owner, shares); // Transfer assets after burn - let asset_dispatcher = IERC20Dispatcher { contract_address: Immutable::ASSET }; + let asset_dispatcher = IERC20Dispatcher { contract_address: self.ERC4626_asset.read() }; asset_dispatcher.transfer(receiver, assets); self.emit(Withdraw { sender: caller, receiver, owner, assets, shares }); } + + fn _convert_to_shares(self: @ComponentState, assets: u256, rounding: Rounding) -> u256 { + let IERC20 = IERC20Dispatcher { contract_address: self.ERC4626_asset.read() }; + let total_supply = IERC20.total_supply(); + math::u256_mul_div(assets, total_supply + 10 ^ Immutable::DECIMALS_OFFSET.into(), self.total_assets() + 1, rounding) + } + + fn _convert_to_assets(self: @ComponentState, shares: u256, rounding: Rounding) -> u256 { + let IERC20 = IERC20Dispatcher { contract_address: self.ERC4626_asset.read() }; + let total_supply = IERC20.total_supply(); + math::u256_mul_div( + shares, + self.total_assets() + 1, + total_supply + 10 ^ Immutable::DECIMALS_OFFSET.into(), + rounding + ) + } } } + /// Implementation of the default ERC2981Component ImmutableConfig. /// /// See /// https://github.com/starknet-io/SNIPs/blob/963848f0752bde75c7087c2446d83b7da8118b25/SNIPS/snip-107.md#defaultconfig-implementation /// -/// The default decimals is set to `DEFAULT_DECIMALS`. -//pub impl DefaultConfig of ERC2981Component::ImmutableConfig { -// const UNDERLYING_DECIMALS: u8 = ERC4626::DEFAULT_DECIMALS; -//} +/// The default underlying decimals is set to `18`. +/// The default decimals offset is set to `0`. +pub impl DefaultConfig of ERC4626Component::ImmutableConfig { + const UNDERLYING_DECIMALS: u8 = ERC4626Component::DEFAULT_UNDERLYING_DECIMALS; + const DECIMALS_OFFSET: u8 = ERC4626Component::DEFAULT_DECIMALS_OFFSET; +} From 4fefde2cd8da4eb5691a4d5b7032c63107b39ef5 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 2 Oct 2024 11:19:18 -0500 Subject: [PATCH 10/90] reexports --- packages/token/src/erc20/extensions/erc4626.cairo | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/token/src/erc20/extensions/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626.cairo index 62fa2d71e..49a364a97 100644 --- a/packages/token/src/erc20/extensions/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626.cairo @@ -1,2 +1,6 @@ pub mod erc4626; pub mod interface; + +pub use erc4626::ERC4626Component; +pub use erc4626::DefaultConfig; +pub use interface::IERC4626; From 80c401393b840ce6c5b215f7973626683fc07865 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 2 Oct 2024 11:20:04 -0500 Subject: [PATCH 11/90] add erc4626 mock --- .../token/src/tests/mocks/erc4626_mocks.cairo | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 packages/token/src/tests/mocks/erc4626_mocks.cairo diff --git a/packages/token/src/tests/mocks/erc4626_mocks.cairo b/packages/token/src/tests/mocks/erc4626_mocks.cairo new file mode 100644 index 000000000..431c5177a --- /dev/null +++ b/packages/token/src/tests/mocks/erc4626_mocks.cairo @@ -0,0 +1,55 @@ +#[starknet::contract] +pub(crate) mod ERC4626Mock { + use crate::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use crate::erc20::extensions::erc4626::DefaultConfig; + use crate::erc20::extensions::erc4626::ERC4626Component; + use crate::erc20::extensions::erc4626::ERC4626Component::InternalTrait as ERC4626InternalTrait; + use starknet::ContractAddress; + + component!(path: ERC4626Component, storage: erc4626, event: ERC4626Event); + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + + // ERC4626 + #[abi(embed_v0)] + impl ERC4626ComponentImpl = ERC4626Component::ERC4626Impl; + // ERC4626MetadataImpl is a custom impl of IERC20Metadata + #[abi(embed_v0)] + impl ERC4626MetadataImpl = ERC4626Component::ERC4626MetadataImpl; + + // ERC20 + #[abi(embed_v0)] + impl ERC20Impl = ERC20Component::ERC20Impl; + #[abi(embed_v0)] + impl ERC20CamelOnlyImpl = ERC20Component::ERC20CamelOnlyImpl; + + impl ERC4626InternalImpl = ERC4626Component::InternalImpl; + impl ERC20InternalImpl = ERC20Component::InternalImpl; + + #[storage] + pub struct Storage { + #[substorage(v0)] + pub erc4626: ERC4626Component::Storage, + #[substorage(v0)] + pub erc20: ERC20Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC4626Event: ERC4626Component::Event, + #[flat] + ERC20Event: ERC20Component::Event + } + + #[constructor] + fn constructor( + ref self: ContractState, + name: ByteArray, + symbol: ByteArray, + underlying_asset: ContractAddress + ) { + self.erc20.initializer(name, symbol); + self.erc4626.initializer(underlying_asset); + } +} From 7e4177fd23aecc4aa631fe7d27580a3ca234e05e Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 2 Oct 2024 11:20:30 -0500 Subject: [PATCH 12/90] add erc4626 mock --- packages/token/src/tests/mocks.cairo | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/token/src/tests/mocks.cairo b/packages/token/src/tests/mocks.cairo index 9574a4b7a..3c9aed416 100644 --- a/packages/token/src/tests/mocks.cairo +++ b/packages/token/src/tests/mocks.cairo @@ -1,11 +1,12 @@ pub(crate) mod account_mocks; -pub(crate) mod erc1155_mocks; -pub(crate) mod erc1155_receiver_mocks; +//pub(crate) mod erc1155_mocks; +//pub(crate) mod erc1155_receiver_mocks; pub(crate) mod erc20_mocks; pub(crate) mod erc20_votes_mocks; -pub(crate) mod erc2981_mocks; -pub(crate) mod erc721_enumerable_mocks; -pub(crate) mod erc721_mocks; -pub(crate) mod erc721_receiver_mocks; -pub(crate) mod non_implementing_mock; -pub(crate) mod src5_mocks; +pub(crate) mod erc4626_mocks; +//pub(crate) mod erc2981_mocks; +//pub(crate) mod erc721_enumerable_mocks; +//pub(crate) mod erc721_mocks; +//pub(crate) mod erc721_receiver_mocks; +//pub(crate) mod non_implementing_mock; +//pub(crate) mod src5_mocks; From 3def69336b0f0e65248f8d39f70b598c396c0600 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 2 Oct 2024 11:21:03 -0500 Subject: [PATCH 13/90] start erc4626 tests --- .../token/src/tests/erc20/test_erc4626.cairo | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 packages/token/src/tests/erc20/test_erc4626.cairo diff --git a/packages/token/src/tests/erc20/test_erc4626.cairo b/packages/token/src/tests/erc20/test_erc4626.cairo new file mode 100644 index 000000000..e1a4fcaa0 --- /dev/null +++ b/packages/token/src/tests/erc20/test_erc4626.cairo @@ -0,0 +1,51 @@ +//use core::num::traits::Bounded; +//use core::num::traits::Zero; +use crate::erc20::ERC20Component::InternalImpl as ERC20InternalImpl; +//use crate::erc20::extensions::erc4626::ERC4626Component::{Deposit, Withdraw}; +use crate::erc20::extensions::erc4626::ERC4626Component::{ERC4626Impl, ERC4626MetadataImpl, InternalImpl}; +use crate::erc20::extensions::erc4626::{ERC4626Component, DefaultConfig}; +use crate::tests::mocks::erc4626_mocks::ERC4626Mock; +//use openzeppelin_testing as utils; +use openzeppelin_testing::constants::{NAME, SYMBOL}; +//use openzeppelin_testing::events::EventSpyExt; +//use snforge_std::EventSpy; +//use snforge_std::{ +// start_cheat_block_timestamp_global, start_cheat_caller_address, spy_events, +// start_cheat_chain_id_global, test_address +//}; +//use starknet::storage::{StorageMapReadAccess, StoragePointerReadAccess}; +use starknet::{ContractAddress, contract_address_const}; + +fn ASSET_ADDRESS() -> ContractAddress { + contract_address_const::<'ASSET_ADDRESS'>() +} + +// +// Setup +// + +type ComponentState = ERC4626Component::ComponentState; + +fn CONTRACT_STATE() -> ERC4626Mock::ContractState { + ERC4626Mock::contract_state_for_testing() +} +fn COMPONENT_STATE() -> ComponentState { + ERC4626Component::component_state_for_testing() +} + +fn setup() -> ComponentState { + let mut state = COMPONENT_STATE(); + let mut mock_state = CONTRACT_STATE(); + + mock_state.erc20.initializer(NAME(), SYMBOL()); + state.initializer(ASSET_ADDRESS()); + state +} + +#[test] +fn test_default_decimals() { + let state = setup(); + + let decimals = state.decimals(); + assert_eq!(decimals, 18); +} From 77fdfa61660b8bcf11a66a3faee549b5e4b91979 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 2 Oct 2024 11:21:38 -0500 Subject: [PATCH 14/90] comment out mods and tests to improve performance --- packages/test_common/src/lib.cairo | 4 ++-- packages/token/src/lib.cairo | 6 +++--- packages/token/src/tests.cairo | 12 ++++++------ packages/token/src/tests/erc20.cairo | 1 + 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/test_common/src/lib.cairo b/packages/test_common/src/lib.cairo index 8013cedc4..dd2bb2209 100644 --- a/packages/test_common/src/lib.cairo +++ b/packages/test_common/src/lib.cairo @@ -1,7 +1,7 @@ pub mod account; -pub mod erc1155; +//pub mod erc1155; pub mod erc20; -pub mod erc721; +//pub mod erc721; pub mod eth_account; pub mod ownable; pub mod upgrades; diff --git a/packages/token/src/lib.cairo b/packages/token/src/lib.cairo index e158d0e7d..71f45bc01 100644 --- a/packages/token/src/lib.cairo +++ b/packages/token/src/lib.cairo @@ -1,6 +1,6 @@ -pub mod common; -pub mod erc1155; +//pub mod common; +//pub mod erc1155; pub mod erc20; -pub mod erc721; +//pub mod erc721; pub mod tests; diff --git a/packages/token/src/tests.cairo b/packages/token/src/tests.cairo index d37791a03..9b29ef1fa 100644 --- a/packages/token/src/tests.cairo +++ b/packages/token/src/tests.cairo @@ -1,10 +1,10 @@ -#[cfg(test)] -pub mod erc1155; +//#[cfg(test)] +//pub mod erc1155; #[cfg(test)] pub mod erc20; -#[cfg(test)] -pub mod erc2981; -#[cfg(test)] -pub mod erc721; +//#[cfg(test)] +//pub mod erc2981; +//#[cfg(test)] +//pub mod erc721; pub(crate) mod mocks; diff --git a/packages/token/src/tests/erc20.cairo b/packages/token/src/tests/erc20.cairo index 213861a92..0af3bf97e 100644 --- a/packages/token/src/tests/erc20.cairo +++ b/packages/token/src/tests/erc20.cairo @@ -1,3 +1,4 @@ mod test_dual20; mod test_erc20; mod test_erc20_votes; +mod test_erc4626; From dc66804b9e6a919e7648dc9ef46e62e87827002d Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 3 Oct 2024 11:24:59 -0500 Subject: [PATCH 15/90] add offset config in mock --- packages/token/src/tests/mocks/erc4626_mocks.cairo | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/token/src/tests/mocks/erc4626_mocks.cairo b/packages/token/src/tests/mocks/erc4626_mocks.cairo index 431c5177a..11b77a678 100644 --- a/packages/token/src/tests/mocks/erc4626_mocks.cairo +++ b/packages/token/src/tests/mocks/erc4626_mocks.cairo @@ -1,7 +1,6 @@ #[starknet::contract] pub(crate) mod ERC4626Mock { use crate::erc20::{ERC20Component, ERC20HooksEmptyImpl}; - use crate::erc20::extensions::erc4626::DefaultConfig; use crate::erc20::extensions::erc4626::ERC4626Component; use crate::erc20::extensions::erc4626::ERC4626Component::InternalTrait as ERC4626InternalTrait; use starknet::ContractAddress; @@ -42,6 +41,11 @@ pub(crate) mod ERC4626Mock { ERC20Event: ERC20Component::Event } + pub impl OffsetConfig of ERC4626Component::ImmutableConfig { + const UNDERLYING_DECIMALS: u8 = ERC4626Component::DEFAULT_UNDERLYING_DECIMALS; + const DECIMALS_OFFSET: u8 = 1; + } + #[constructor] fn constructor( ref self: ContractState, From b05f57e13f31713841528e9e46b70421b6bcdb19 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 3 Oct 2024 11:25:15 -0500 Subject: [PATCH 16/90] add overflow assertion and test --- .../erc20/extensions/erc4626/erc4626.cairo | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo index e93310492..96c6f25ec 100644 --- a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -63,6 +63,7 @@ pub mod ERC4626Component { pub const EXCEEDED_MAX_REDEEM: felt252 = 'ERC4626: exceeds max redeem'; pub const TOKEN_TRANSFER_FAILED: felt252 = 'ERC4626: token transfer failed'; pub const INVALID_ASSET_ADDRESS: felt252 = 'ERC4626: asset address set to 0'; + pub const DECIMALS_OVERFLOW: felt252 = 'ERC4626: decimals overflow'; } /// Constants expected to be defined at the contract level used to configure the component @@ -73,7 +74,9 @@ pub mod ERC4626Component { const UNDERLYING_DECIMALS: u8; const DECIMALS_OFFSET: u8; - fn validate() {} + fn validate() { + assert(Bounded::MAX - Self::UNDERLYING_DECIMALS >= Self::DECIMALS_OFFSET, Errors::DECIMALS_OVERFLOW) + } } #[embeddable_as(ERC4626Impl)] @@ -229,7 +232,7 @@ pub mod ERC4626Component { +Drop > of InternalTrait { fn initializer(ref self: ComponentState, asset_address: ContractAddress) { - //ImmutableConfig::validate(); + ImmutableConfig::validate(); assert(!asset_address.is_zero(), Errors::INVALID_ASSET_ADDRESS); self.ERC4626_asset.write(asset_address); } @@ -307,4 +310,32 @@ pub impl DefaultConfig of ERC4626Component::ImmutableConfig { const DECIMALS_OFFSET: u8 = ERC4626Component::DEFAULT_DECIMALS_OFFSET; } +#[cfg(test)] +mod Test { + use crate::tests::mocks::erc4626_mocks::ERC4626Mock; + use starknet::contract_address_const; + use super::ERC4626Component::InternalImpl; + use super::ERC4626Component; + + type ComponentState = ERC4626Component::ComponentState; + + fn COMPONENT_STATE() -> ComponentState { + ERC4626Component::component_state_for_testing() + } + + // Invalid fee denominator + impl InvalidImmutableConfig of ERC4626Component::ImmutableConfig { + const UNDERLYING_DECIMALS: u8 = 255; + const DECIMALS_OFFSET: u8 = 1; + } + + #[test] + #[should_panic(expected: 'ERC4626: decimals overflow')] + fn test_initializer_invalid_config_panics() { + let mut state = COMPONENT_STATE(); + let asset = contract_address_const::<'ASSET'>(); + + state.initializer(asset); + } +} From ea3bf066cfba2aaebf02171265269735ceab93df Mon Sep 17 00:00:00 2001 From: andrew Date: Sat, 5 Oct 2024 23:14:00 -0500 Subject: [PATCH 17/90] add power fn --- packages/utils/src/math.cairo | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/utils/src/math.cairo b/packages/utils/src/math.cairo index ecf1203c7..e54e2efc1 100644 --- a/packages/utils/src/math.cairo +++ b/packages/utils/src/math.cairo @@ -5,6 +5,23 @@ use core::integer::u512_safe_div_rem_by_u256; use core::num::traits::WideMul; use core::traits::{Into, BitAnd, BitXor}; +pub fn power, +PartialEq, +TryInto, +Into, +Into>( + base: T, exp: T +) -> T { + assert!(base != 0_u8.into(), "Math: base cannot be zero"); + let mut base: u256 = base.into(); + let mut exp: u256 = exp.into(); + let mut result: u256 = 1; + let mut i: u256 = 0; + + while (i < exp) { + result *= base; + i += 1; + }; + + result.try_into().unwrap() +} + /// Returns the average of two numbers. The result is rounded down. pub fn average< T, From f5ce03b6edc88e749bc3d9c1379b0acbd449fa81 Mon Sep 17 00:00:00 2001 From: andrew Date: Sat, 5 Oct 2024 23:19:46 -0500 Subject: [PATCH 18/90] add erc20reentrant mock --- packages/token/src/tests/mocks.cairo | 1 + .../src/tests/mocks/erc20_reentrant.cairo | 158 ++++++++++++++++++ 2 files changed, 159 insertions(+) create mode 100644 packages/token/src/tests/mocks/erc20_reentrant.cairo diff --git a/packages/token/src/tests/mocks.cairo b/packages/token/src/tests/mocks.cairo index 3c9aed416..8e5e59d57 100644 --- a/packages/token/src/tests/mocks.cairo +++ b/packages/token/src/tests/mocks.cairo @@ -2,6 +2,7 @@ pub(crate) mod account_mocks; //pub(crate) mod erc1155_mocks; //pub(crate) mod erc1155_receiver_mocks; pub(crate) mod erc20_mocks; +pub(crate) mod erc20_reentrant; pub(crate) mod erc20_votes_mocks; pub(crate) mod erc4626_mocks; //pub(crate) mod erc2981_mocks; diff --git a/packages/token/src/tests/mocks/erc20_reentrant.cairo b/packages/token/src/tests/mocks/erc20_reentrant.cairo new file mode 100644 index 000000000..734bd93ed --- /dev/null +++ b/packages/token/src/tests/mocks/erc20_reentrant.cairo @@ -0,0 +1,158 @@ +use starknet::ContractAddress; + +#[derive(Drop, Serde, PartialEq, Debug, starknet::Store)] +pub(crate) enum Type { + No, + Before, + After +} + +#[starknet::interface] +pub(crate) trait IERC20ReentrantHelpers { + fn schedule_reenter( + ref self: TState, + when: Type, + target: ContractAddress, + selector: felt252, + calldata: Span + ); + fn function_call(ref self: TState); + fn unsafe_mint(ref self: TState, recipient: ContractAddress, amount: u256); +} + +#[starknet::interface] +pub(crate) trait IERC20Reentrant { + fn schedule_reenter( + ref self: TState, + when: Type, + target: ContractAddress, + selector: felt252, + calldata: Span + ); + fn function_call(ref self: TState); + fn unsafe_mint(ref self: TState, recipient: ContractAddress, amount: u256); + + // IERC20 + fn total_supply(self: @TState) -> u256; + fn balance_of(self: @TState, account: ContractAddress) -> u256; + fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress) -> u256; + fn transfer(ref self: TState, recipient: ContractAddress, amount: u256) -> bool; + fn transfer_from( + ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + fn approve(ref self: TState, spender: ContractAddress, amount: u256) -> bool; +} + +#[starknet::contract] +pub(crate) mod ERC20ReentrantMock { + use crate::erc20::ERC20Component; + use starknet::ContractAddress; + use starknet::storage::{Vec, MutableVecTrait}; + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + use starknet::syscalls::call_contract_syscall; + use super::Type; + + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + + #[abi(embed_v0)] + impl ERC20Impl = ERC20Component::ERC20Impl; + #[abi(embed_v0)] + impl ERC20MetadataImpl = ERC20Component::ERC20MetadataImpl; + #[abi(embed_v0)] + impl ERC20CamelOnlyImpl = ERC20Component::ERC20CamelOnlyImpl; + impl InternalImpl = ERC20Component::InternalImpl; + + #[storage] + pub struct Storage { + #[substorage(v0)] + pub erc20: ERC20Component::Storage, + reenter_type: Type, + reenter_target: ContractAddress, + reenter_selector: felt252, + reenter_calldata: Vec + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC20Event: ERC20Component::Event + } + + // + // Hooks + // + + impl ERC20VotesHooksImpl of ERC20Component::ERC20HooksTrait { + fn before_update( + ref self: ERC20Component::ComponentState, + from: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) { + let mut contract_state = self.get_contract_mut(); + + if (contract_state.reenter_type.read() == Type::Before) { + contract_state.reenter_type.write(Type::No); + contract_state.function_call(); + } + } + + fn after_update( + ref self: ERC20Component::ComponentState, + from: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) { + let mut contract_state = self.get_contract_mut(); + + if (contract_state.reenter_type.read() == Type::After) { + contract_state.reenter_type.write(Type::No); + contract_state.function_call(); + } + } + } + + #[abi(embed_v0)] + pub impl ERC20ReentrantHelpers of super::IERC20ReentrantHelpers { + fn schedule_reenter( + ref self: ContractState, + when: Type, + target: ContractAddress, + selector: felt252, + calldata: Span + ) { + self.reenter_target.write(target); + self.reenter_selector.write(selector); + for elem in calldata { + self.reenter_calldata.append().write(*elem); + } + } + + fn function_call(ref self: ContractState) { + let target = self.reenter_target.read(); + let selector = self.reenter_selector.read(); + let mut calldata = array![]; + for i in 0..self.reenter_calldata.len() { + calldata.append(self.reenter_calldata.at(i).read()); + }; + + call_contract_syscall(target, selector, calldata.span()).unwrap(); + } + + fn unsafe_mint(ref self: ContractState, recipient: ContractAddress, amount: u256) { + self.erc20.mint(recipient, amount); + } + } + + #[constructor] + fn constructor( + ref self: ContractState, + name: ByteArray, + symbol: ByteArray + ) { + self.erc20.initializer(name, symbol); + self.reenter_type.write(Type::No); + } + +} \ No newline at end of file From e70061dcfa67a5ed0e3cabdb93feb79bcb2ce45d Mon Sep 17 00:00:00 2001 From: andrew Date: Sat, 5 Oct 2024 23:21:00 -0500 Subject: [PATCH 19/90] fix interface fns --- .../erc20/extensions/erc4626/interface.cairo | 51 ++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/packages/token/src/erc20/extensions/erc4626/interface.cairo b/packages/token/src/erc20/extensions/erc4626/interface.cairo index 21e9bd6ec..3eb4adcc3 100644 --- a/packages/token/src/erc20/extensions/erc4626/interface.cairo +++ b/packages/token/src/erc20/extensions/erc4626/interface.cairo @@ -15,8 +15,8 @@ pub trait IERC4626 { fn max_mint(self: @TState, receiver: ContractAddress) -> u256; fn preview_mint(self: @TState, shares: u256) -> u256; fn mint(ref self: TState, shares: u256, receiver: ContractAddress) -> u256; - fn max_withdrawal(self: @TState, owner: ContractAddress) -> u256; - fn preview_withdrawal(self: @TState, assets: u256) -> u256; + fn max_withdraw(self: @TState, owner: ContractAddress) -> u256; + fn preview_withdraw(self: @TState, assets: u256) -> u256; fn withdraw( ref self: TState, assets: u256, receiver: ContractAddress, owner: ContractAddress ) -> u256; @@ -26,3 +26,50 @@ pub trait IERC4626 { ref self: TState, shares: u256, receiver: ContractAddress, owner: ContractAddress ) -> u256; } + +#[starknet::interface] +pub trait ERC4626ABI { + // IERC4626 + fn asset(self: @TState) -> ContractAddress; + fn total_assets(self: @TState) -> u256; + fn convert_to_shares(self: @TState, assets: u256) -> u256; + fn convert_to_assets(self: @TState, shares: u256) -> u256; + fn max_deposit(self: @TState, receiver: ContractAddress) -> u256; + fn preview_deposit(self: @TState, assets: u256) -> u256; + fn deposit(ref self: TState, assets: u256, receiver: ContractAddress) -> u256; + fn max_mint(self: @TState, receiver: ContractAddress) -> u256; + fn preview_mint(self: @TState, shares: u256) -> u256; + fn mint(ref self: TState, shares: u256, receiver: ContractAddress) -> u256; + fn max_withdraw(self: @TState, owner: ContractAddress) -> u256; + fn preview_withdraw(self: @TState, assets: u256) -> u256; + fn withdraw( + ref self: TState, assets: u256, receiver: ContractAddress, owner: ContractAddress + ) -> u256; + fn max_redeem(self: @TState, owner: ContractAddress) -> u256; + fn preview_redeem(self: @TState, shares: u256) -> u256; + fn redeem( + ref self: TState, shares: u256, receiver: ContractAddress, owner: ContractAddress + ) -> u256; + + // IERC20 + fn total_supply(self: @TState) -> u256; + fn balance_of(self: @TState, account: ContractAddress) -> u256; + fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress) -> u256; + fn transfer(ref self: TState, recipient: ContractAddress, amount: u256) -> bool; + fn transfer_from( + ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + fn approve(ref self: TState, spender: ContractAddress, amount: u256) -> bool; + + // IERC20Metadata + fn name(self: @TState) -> ByteArray; + fn symbol(self: @TState) -> ByteArray; + fn decimals(self: @TState) -> u8; + + // IERC20CamelOnly + fn totalSupply(self: @TState) -> u256; + fn balanceOf(self: @TState, account: ContractAddress) -> u256; + fn transferFrom( + ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; +} From ca52772bbecd2402ce56bbda49b9ae0691e7178e Mon Sep 17 00:00:00 2001 From: andrew Date: Sat, 5 Oct 2024 23:21:43 -0500 Subject: [PATCH 20/90] fix logic, add power --- .../erc20/extensions/erc4626/erc4626.cairo | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo index 96c6f25ec..b109a60c9 100644 --- a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -59,7 +59,7 @@ pub mod ERC4626Component { pub mod Errors { pub const EXCEEDED_MAX_DEPOSIT: felt252 = 'ERC4626: exceeds max deposit'; pub const EXCEEDED_MAX_MINT: felt252 = 'ERC4626: exceeds max mint'; - pub const EXCEEDED_MAX_WITHDRAWAL: felt252 = 'ERC4626: exceeds max withdrawal'; + pub const EXCEEDED_MAX_WITHDRAW: felt252 = 'ERC4626: exceeds max withdraw'; pub const EXCEEDED_MAX_REDEEM: felt252 = 'ERC4626: exceeds max redeem'; pub const TOKEN_TRANSFER_FAILED: felt252 = 'ERC4626: token transfer failed'; pub const INVALID_ASSET_ADDRESS: felt252 = 'ERC4626: asset address set to 0'; @@ -145,13 +145,13 @@ pub mod ERC4626Component { assets } - fn max_withdrawal(self: @ComponentState, owner: ContractAddress) -> u256 { + fn max_withdraw(self: @ComponentState, owner: ContractAddress) -> u256 { let erc20_component = get_dep_component!(self, ERC20); let owner_bal = erc20_component.balance_of(owner); self._convert_to_assets(owner_bal, Rounding::Floor) } - fn preview_withdrawal(self: @ComponentState, assets: u256) -> u256 { + fn preview_withdraw(self: @ComponentState, assets: u256) -> u256 { self._convert_to_shares(assets, Rounding::Ceil) } @@ -161,10 +161,10 @@ pub mod ERC4626Component { receiver: ContractAddress, owner: ContractAddress ) -> u256 { - let max_assets = self.max_withdrawal(owner); - assert(assets < max_assets, Errors::EXCEEDED_MAX_WITHDRAWAL); + let max_assets = self.max_withdraw(owner); + assert(assets <= max_assets, Errors::EXCEEDED_MAX_WITHDRAW); - let shares = self.preview_withdrawal(assets); + let shares = self.preview_withdraw(assets); let caller = starknet::get_caller_address(); self._withdraw(caller, receiver, owner, assets, shares); @@ -187,7 +187,7 @@ pub mod ERC4626Component { owner: ContractAddress ) -> u256 { let max_shares = self.max_redeem(owner); - assert(shares < max_shares, Errors::EXCEEDED_MAX_REDEEM); + assert(shares <= max_shares, Errors::EXCEEDED_MAX_REDEEM); let assets = self.preview_redeem(shares); let caller = starknet::get_caller_address(); @@ -280,18 +280,25 @@ pub mod ERC4626Component { } fn _convert_to_shares(self: @ComponentState, assets: u256, rounding: Rounding) -> u256 { - let IERC20 = IERC20Dispatcher { contract_address: self.ERC4626_asset.read() }; - let total_supply = IERC20.total_supply(); - math::u256_mul_div(assets, total_supply + 10 ^ Immutable::DECIMALS_OFFSET.into(), self.total_assets() + 1, rounding) + let mut erc20_component = get_dep_component!(self, ERC20); + let total_supply = erc20_component.total_supply(); + + math::u256_mul_div( + assets, + total_supply + math::power(10, Immutable::DECIMALS_OFFSET.into()), + self.total_assets() + 1, + rounding + ) } fn _convert_to_assets(self: @ComponentState, shares: u256, rounding: Rounding) -> u256 { - let IERC20 = IERC20Dispatcher { contract_address: self.ERC4626_asset.read() }; - let total_supply = IERC20.total_supply(); + let mut erc20_component = get_dep_component!(self, ERC20); + let total_supply = erc20_component.total_supply(); + math::u256_mul_div( shares, self.total_assets() + 1, - total_supply + 10 ^ Immutable::DECIMALS_OFFSET.into(), + total_supply + math::power(10, Immutable::DECIMALS_OFFSET.into()), rounding ) } From 80727707ba825691ee1aa2cf09f9d553e1fbf654 Mon Sep 17 00:00:00 2001 From: andrew Date: Sat, 5 Oct 2024 23:22:26 -0500 Subject: [PATCH 21/90] add starting tests-no assets, no shares --- .../token/src/tests/erc20/test_erc4626.cairo | 354 ++++++++++++++++-- 1 file changed, 325 insertions(+), 29 deletions(-) diff --git a/packages/token/src/tests/erc20/test_erc4626.cairo b/packages/token/src/tests/erc20/test_erc4626.cairo index e1a4fcaa0..cef5fae40 100644 --- a/packages/token/src/tests/erc20/test_erc4626.cairo +++ b/packages/token/src/tests/erc20/test_erc4626.cairo @@ -1,51 +1,347 @@ -//use core::num::traits::Bounded; -//use core::num::traits::Zero; +use core::num::traits::Bounded; +//use crate::erc20::ERC20Component; use crate::erc20::ERC20Component::InternalImpl as ERC20InternalImpl; -//use crate::erc20::extensions::erc4626::ERC4626Component::{Deposit, Withdraw}; +use crate::erc20::extensions::erc4626::interface::{ERC4626ABIDispatcher, ERC4626ABIDispatcherTrait}; use crate::erc20::extensions::erc4626::ERC4626Component::{ERC4626Impl, ERC4626MetadataImpl, InternalImpl}; -use crate::erc20::extensions::erc4626::{ERC4626Component, DefaultConfig}; -use crate::tests::mocks::erc4626_mocks::ERC4626Mock; -//use openzeppelin_testing as utils; -use openzeppelin_testing::constants::{NAME, SYMBOL}; -//use openzeppelin_testing::events::EventSpyExt; -//use snforge_std::EventSpy; -//use snforge_std::{ -// start_cheat_block_timestamp_global, start_cheat_caller_address, spy_events, -// start_cheat_chain_id_global, test_address -//}; -//use starknet::storage::{StorageMapReadAccess, StoragePointerReadAccess}; +use crate::erc20::extensions::erc4626::ERC4626Component; +use crate::erc20::extensions::erc4626::ERC4626Component::{Deposit, Withdraw}; +use crate::erc20::extensions::erc4626::DefaultConfig; +//use crate::tests::mocks::erc4626_mocks::ERC4626Mock; +use openzeppelin_testing::events::EventSpyExt; +use crate::tests::mocks::erc20_reentrant::Type; +use crate::tests::mocks::erc20_reentrant::{IERC20ReentrantDispatcher, IERC20ReentrantDispatcherTrait}; +//use crate::tests::mocks::erc20_reentrant::ERC20ReentrantMock; +use openzeppelin_testing as utils; +use openzeppelin_utils::serde::SerializedAppend; +use openzeppelin_utils::math; +use openzeppelin_test_common::erc20::ERC20SpyHelpers; +use openzeppelin_testing::constants::{NAME, SYMBOL, OTHER, RECIPIENT, ZERO}; +use snforge_std::{start_cheat_caller_address, cheat_caller_address, CheatSpan, spy_events, EventSpy}; use starknet::{ContractAddress, contract_address_const}; fn ASSET_ADDRESS() -> ContractAddress { contract_address_const::<'ASSET_ADDRESS'>() } +fn HOLDER() -> ContractAddress { + contract_address_const::<'HOLDER'>() +} + +fn VAULT_NAME() -> ByteArray { + "VAULT" +} + +fn VAULT_SYMBOL() -> ByteArray { + "V" +} + +const DEFAULT_DECIMALS: u8 = 18; +const OFFSET_DECIMALS: u8 = 1; + +// +// Helpers +// + +fn parse_token(token: u256) -> u256 { + token * math::power(10, DEFAULT_DECIMALS.into()) +} + +fn parse_share(share: u256) -> u256 { + share * math::power(10, DEFAULT_DECIMALS.into() + OFFSET_DECIMALS.into()) +} + // // Setup // -type ComponentState = ERC4626Component::ComponentState; +fn deploy_asset() -> IERC20ReentrantDispatcher { + let mut asset_calldata: Array = array![]; + asset_calldata.append_serde(NAME()); + asset_calldata.append_serde(SYMBOL()); + + let contract_address = utils::declare_and_deploy("ERC20ReentrantMock", asset_calldata); + IERC20ReentrantDispatcher { contract_address } +} + +fn deploy_vault(asset_address: ContractAddress) -> ERC4626ABIDispatcher { + let mut vault_calldata: Array = array![]; + vault_calldata.append_serde(VAULT_NAME()); + vault_calldata.append_serde(VAULT_SYMBOL()); + vault_calldata.append_serde(asset_address); + + let contract_address = utils::declare_and_deploy("ERC4626Mock", vault_calldata); + ERC4626ABIDispatcher { contract_address } +} + +fn setup() -> (IERC20ReentrantDispatcher, ERC4626ABIDispatcher) { + let mut asset = deploy_asset(); + let mut vault = deploy_vault(asset.contract_address); + (asset, vault) +} + +// Further testing required for decimals once design is finalized +#[test] +fn test_offset_decimals() { + let (_, vault) = setup(); + + let decimals = vault.decimals(); + assert_eq!(decimals, 19); +} + +// +// Reentrancy +// + +#[test] +#[ignore] +fn test_share_price_with_reentrancy_before() { + let (asset, vault) = setup(); + + let amount = 1_000_000_000_000_000_000; + let reenter_amt = 1_000_000_000; + + asset.unsafe_mint(HOLDER(), amount); + asset.unsafe_mint(OTHER(), amount); + + let approvers: Span = array![HOLDER(), OTHER(), asset.contract_address].span(); + + for approver in approvers { + cheat_caller_address(asset.contract_address, *approver, CheatSpan::TargetCalls(1)); + asset.approve(vault.contract_address, Bounded::MAX); + }; + //stop_cheat_caller_address(asset.contract_address); + + // Mint token for deposit + asset.unsafe_mint(asset.contract_address, reenter_amt); + + // Schedule reentrancy + let mut calldata: Array = array![]; + calldata.append_serde(reenter_amt); + calldata.append_serde(HOLDER()); + + asset.schedule_reenter( + Type::Before, + vault.contract_address, + selector!("deposit"), + calldata.span() + ); + + // Initial share price + start_cheat_caller_address(vault.contract_address, HOLDER()); + + let shares_for_deposit = vault.preview_deposit(amount); + let _shares_for_reenter = vault.preview_deposit(reenter_amt); + + // Do deposit normally, triggering the hook + vault.deposit(amount, HOLDER()); + + // Assert prices are kept + let shares_after = vault.preview_deposit(amount); + assert_eq!(shares_for_deposit, shares_after, "ahhh"); +} + +#[test] +fn test_metadata() { + let (asset, vault) = setup(); + let name = vault.name(); + let symbol = vault.symbol(); + let decimals = vault.decimals(); + let asset_address = vault.asset(); + + assert_eq!(name, VAULT_NAME()); + assert_eq!(symbol, VAULT_SYMBOL()); + assert_eq!(decimals, DEFAULT_DECIMALS + OFFSET_DECIMALS); + assert_eq!(asset_address, asset.contract_address); +} + +// +// Empty vault: no assets, no shares +// + +#[test] +fn test_init_vault_status() { + let (_, vault) = setup(); + let total_assets = vault.total_assets(); + + assert_eq!(total_assets, 0); +} + +#[test] +fn test_deposit() { + let (asset, vault) = setup(); + let amount = parse_token(1); + + // Setup + asset.unsafe_mint(HOLDER(), Bounded::MAX / 2); + cheat_caller_address(asset.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); + asset.approve(vault.contract_address, Bounded::MAX); + + // Check max deposit + let max_deposit = vault.max_deposit(HOLDER()); + assert_eq!(max_deposit, Bounded::MAX); + + // Check preview == expected shares + let preview_deposit = vault.preview_deposit(amount); + let exp_shares = parse_share(1); + assert_eq!(preview_deposit, exp_shares); + + let holder_balance_before = asset.balance_of(HOLDER()); + let mut spy = spy_events(); + + // Deposit + cheat_caller_address(vault.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); + let shares = vault.deposit(amount, RECIPIENT()); + + // Check events + spy.assert_event_transfer(asset.contract_address, HOLDER(), vault.contract_address, amount); + spy.assert_event_transfer(vault.contract_address, ZERO(), RECIPIENT(), shares); + spy.assert_only_event_deposit(vault.contract_address, HOLDER(), RECIPIENT(), amount, shares); -fn CONTRACT_STATE() -> ERC4626Mock::ContractState { - ERC4626Mock::contract_state_for_testing() + let holder_balance_after = asset.balance_of(HOLDER()); + assert_eq!(holder_balance_after, holder_balance_before - amount); } -fn COMPONENT_STATE() -> ComponentState { - ERC4626Component::component_state_for_testing() + +#[test] +fn test_mint() { + let (asset, vault) = setup(); + + // Setup + asset.unsafe_mint(HOLDER(), Bounded::MAX / 2); + cheat_caller_address(asset.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); + asset.approve(vault.contract_address, Bounded::MAX); + + // Check max mint + let max_mint = vault.max_mint(HOLDER()); + assert_eq!(max_mint, Bounded::MAX); + + // Check preview mint + let preview_mint = vault.preview_mint(parse_share(1)); + let exp_assets = parse_token(1); + assert_eq!(preview_mint, exp_assets); + + let mut spy = spy_events(); + + // Mint + cheat_caller_address(vault.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); + vault.mint(parse_share(1), RECIPIENT()); + + // Check events + spy.assert_event_transfer(asset.contract_address, HOLDER(), vault.contract_address, parse_token(1)); + spy.assert_event_transfer(vault.contract_address, ZERO(), RECIPIENT(), parse_share(1)); + spy.assert_only_event_deposit(vault.contract_address, HOLDER(), RECIPIENT(), parse_token(1), parse_share(1)); } -fn setup() -> ComponentState { - let mut state = COMPONENT_STATE(); - let mut mock_state = CONTRACT_STATE(); +#[test] +fn test_withdraw() { + let (asset, vault) = setup(); + + // Setup + asset.unsafe_mint(HOLDER(), Bounded::MAX / 2); + cheat_caller_address(asset.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); + asset.approve(vault.contract_address, Bounded::MAX); + + // Check max mint + let max_withdraw = vault.max_withdraw(HOLDER()); + assert_eq!(max_withdraw, 0); - mock_state.erc20.initializer(NAME(), SYMBOL()); - state.initializer(ASSET_ADDRESS()); - state + // Check preview mint + let preview_withdraw = vault.preview_withdraw(0); + assert_eq!(preview_withdraw, 0); + + let mut spy = spy_events(); + + // Withdraw + cheat_caller_address(vault.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); + vault.withdraw(0, RECIPIENT(), HOLDER()); + + // Check events + spy.assert_event_transfer(vault.contract_address, HOLDER(), ZERO(), 0); + spy.assert_event_transfer(asset.contract_address, vault.contract_address, RECIPIENT(), 0); + spy.assert_only_event_withdraw(vault.contract_address, HOLDER(), RECIPIENT(), HOLDER(), 0, 0); } #[test] -fn test_default_decimals() { - let state = setup(); +fn test_redeem() { + let (asset, vault) = setup(); + + // Setup + asset.unsafe_mint(HOLDER(), Bounded::MAX / 2); + cheat_caller_address(asset.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); + asset.approve(vault.contract_address, Bounded::MAX); + + // Check max redeem + let max_redeem = vault.max_redeem(HOLDER()); + assert_eq!(max_redeem, 0); + + // Check preview redeem + let preview_redeem = vault.preview_redeem(0); + assert_eq!(preview_redeem, 0); + + let mut spy = spy_events(); + + // Redeem + cheat_caller_address(vault.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); + vault.redeem(0, RECIPIENT(), HOLDER()); + + // Check events + spy.assert_event_transfer(vault.contract_address, HOLDER(), ZERO(), 0); + spy.assert_event_transfer(asset.contract_address, vault.contract_address, RECIPIENT(), 0); + spy.assert_only_event_withdraw(vault.contract_address, HOLDER(), RECIPIENT(), HOLDER(), 0, 0); +} + +// +// Helpers +// + +#[generate_trait] +pub impl ERC4626SpyHelpersImpl of ERC4626SpyHelpers { + fn assert_event_deposit( + ref self: EventSpy, + contract: ContractAddress, + sender: ContractAddress, + owner: ContractAddress, + assets: u256, + shares:u256 + ) { + let expected = ERC4626Component::Event::Deposit(Deposit { sender, owner, assets, shares }); + self.assert_emitted_single(contract, expected); + } + + fn assert_only_event_deposit( + ref self: EventSpy, + contract: ContractAddress, + sender: ContractAddress, + owner: ContractAddress, + assets: u256, + shares:u256 + ) { + self.assert_event_deposit(contract, sender, owner, assets, shares); + self.assert_no_events_left_from(contract); + } + + fn assert_event_withdraw( + ref self: EventSpy, + contract: ContractAddress, + sender: ContractAddress, + receiver: ContractAddress, + owner: ContractAddress, + assets: u256, + shares:u256 + ) { + let expected = ERC4626Component::Event::Withdraw(Withdraw { sender, receiver, owner, assets, shares }); + self.assert_emitted_single(contract, expected); + } - let decimals = state.decimals(); - assert_eq!(decimals, 18); + fn assert_only_event_withdraw( + ref self: EventSpy, + contract: ContractAddress, + sender: ContractAddress, + receiver: ContractAddress, + owner: ContractAddress, + assets: u256, + shares:u256 + ) { + self.assert_event_withdraw(contract, sender, receiver, owner, assets, shares); + self.assert_no_events_left_from(contract); + } } From 7d2a3c165bbe77128ef4874ae339d7039420308d Mon Sep 17 00:00:00 2001 From: andrew Date: Sat, 5 Oct 2024 23:22:45 -0500 Subject: [PATCH 22/90] fix fmt --- .../token/src/erc20/extensions/erc4626.cairo | 2 +- .../erc20/extensions/erc4626/erc4626.cairo | 17 ++++-- .../token/src/tests/erc20/test_erc4626.cairo | 60 +++++++++++-------- packages/token/src/tests/mocks.cairo | 1 + .../src/tests/mocks/erc20_reentrant.cairo | 20 +++---- .../token/src/tests/mocks/erc4626_mocks.cairo | 4 +- 6 files changed, 61 insertions(+), 43 deletions(-) diff --git a/packages/token/src/erc20/extensions/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626.cairo index 49a364a97..804a3c777 100644 --- a/packages/token/src/erc20/extensions/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626.cairo @@ -1,6 +1,6 @@ pub mod erc4626; pub mod interface; +pub use erc4626::DefaultConfig; pub use erc4626::ERC4626Component; -pub use erc4626::DefaultConfig; pub use interface::IERC4626; diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo index b109a60c9..c385ac0eb 100644 --- a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -12,9 +12,9 @@ pub mod ERC4626Component { use crate::erc20::extensions::erc4626::interface::IERC4626; use crate::erc20::interface::{IERC20, IERC20Metadata}; use crate::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; - use starknet::ContractAddress; - use openzeppelin_utils::math; use openzeppelin_utils::math::Rounding; + use openzeppelin_utils::math; + use starknet::ContractAddress; use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; // The defualt values are only used when the DefaultConfig @@ -75,7 +75,10 @@ pub mod ERC4626Component { const DECIMALS_OFFSET: u8; fn validate() { - assert(Bounded::MAX - Self::UNDERLYING_DECIMALS >= Self::DECIMALS_OFFSET, Errors::DECIMALS_OVERFLOW) + assert( + Bounded::MAX - Self::UNDERLYING_DECIMALS >= Self::DECIMALS_OFFSET, + Errors::DECIMALS_OVERFLOW + ) } } @@ -279,7 +282,9 @@ pub mod ERC4626Component { self.emit(Withdraw { sender: caller, receiver, owner, assets, shares }); } - fn _convert_to_shares(self: @ComponentState, assets: u256, rounding: Rounding) -> u256 { + fn _convert_to_shares( + self: @ComponentState, assets: u256, rounding: Rounding + ) -> u256 { let mut erc20_component = get_dep_component!(self, ERC20); let total_supply = erc20_component.total_supply(); @@ -291,7 +296,9 @@ pub mod ERC4626Component { ) } - fn _convert_to_assets(self: @ComponentState, shares: u256, rounding: Rounding) -> u256 { + fn _convert_to_assets( + self: @ComponentState, shares: u256, rounding: Rounding + ) -> u256 { let mut erc20_component = get_dep_component!(self, ERC20); let total_supply = erc20_component.total_supply(); diff --git a/packages/token/src/tests/erc20/test_erc4626.cairo b/packages/token/src/tests/erc20/test_erc4626.cairo index cef5fae40..e48096a71 100644 --- a/packages/token/src/tests/erc20/test_erc4626.cairo +++ b/packages/token/src/tests/erc20/test_erc4626.cairo @@ -1,22 +1,28 @@ use core::num::traits::Bounded; //use crate::erc20::ERC20Component; use crate::erc20::ERC20Component::InternalImpl as ERC20InternalImpl; -use crate::erc20::extensions::erc4626::interface::{ERC4626ABIDispatcher, ERC4626ABIDispatcherTrait}; -use crate::erc20::extensions::erc4626::ERC4626Component::{ERC4626Impl, ERC4626MetadataImpl, InternalImpl}; -use crate::erc20::extensions::erc4626::ERC4626Component; -use crate::erc20::extensions::erc4626::ERC4626Component::{Deposit, Withdraw}; use crate::erc20::extensions::erc4626::DefaultConfig; -//use crate::tests::mocks::erc4626_mocks::ERC4626Mock; -use openzeppelin_testing::events::EventSpyExt; +use crate::erc20::extensions::erc4626::ERC4626Component::{Deposit, Withdraw}; +use crate::erc20::extensions::erc4626::ERC4626Component::{ + ERC4626Impl, ERC4626MetadataImpl, InternalImpl +}; +use crate::erc20::extensions::erc4626::ERC4626Component; +use crate::erc20::extensions::erc4626::interface::{ERC4626ABIDispatcher, ERC4626ABIDispatcherTrait}; use crate::tests::mocks::erc20_reentrant::Type; -use crate::tests::mocks::erc20_reentrant::{IERC20ReentrantDispatcher, IERC20ReentrantDispatcherTrait}; +use crate::tests::mocks::erc20_reentrant::{ + IERC20ReentrantDispatcher, IERC20ReentrantDispatcherTrait +}; +use openzeppelin_test_common::erc20::ERC20SpyHelpers; //use crate::tests::mocks::erc20_reentrant::ERC20ReentrantMock; use openzeppelin_testing as utils; -use openzeppelin_utils::serde::SerializedAppend; -use openzeppelin_utils::math; -use openzeppelin_test_common::erc20::ERC20SpyHelpers; use openzeppelin_testing::constants::{NAME, SYMBOL, OTHER, RECIPIENT, ZERO}; -use snforge_std::{start_cheat_caller_address, cheat_caller_address, CheatSpan, spy_events, EventSpy}; +//use crate::tests::mocks::erc4626_mocks::ERC4626Mock; +use openzeppelin_testing::events::EventSpyExt; +use openzeppelin_utils::math; +use openzeppelin_utils::serde::SerializedAppend; +use snforge_std::{ + start_cheat_caller_address, cheat_caller_address, CheatSpan, spy_events, EventSpy +}; use starknet::{ContractAddress, contract_address_const}; fn ASSET_ADDRESS() -> ContractAddress { @@ -119,12 +125,10 @@ fn test_share_price_with_reentrancy_before() { calldata.append_serde(reenter_amt); calldata.append_serde(HOLDER()); - asset.schedule_reenter( - Type::Before, - vault.contract_address, - selector!("deposit"), - calldata.span() - ); + asset + .schedule_reenter( + Type::Before, vault.contract_address, selector!("deposit"), calldata.span() + ); // Initial share price start_cheat_caller_address(vault.contract_address, HOLDER()); @@ -226,9 +230,15 @@ fn test_mint() { vault.mint(parse_share(1), RECIPIENT()); // Check events - spy.assert_event_transfer(asset.contract_address, HOLDER(), vault.contract_address, parse_token(1)); + spy + .assert_event_transfer( + asset.contract_address, HOLDER(), vault.contract_address, parse_token(1) + ); spy.assert_event_transfer(vault.contract_address, ZERO(), RECIPIENT(), parse_share(1)); - spy.assert_only_event_deposit(vault.contract_address, HOLDER(), RECIPIENT(), parse_token(1), parse_share(1)); + spy + .assert_only_event_deposit( + vault.contract_address, HOLDER(), RECIPIENT(), parse_token(1), parse_share(1) + ); } #[test] @@ -301,7 +311,7 @@ pub impl ERC4626SpyHelpersImpl of ERC4626SpyHelpers { sender: ContractAddress, owner: ContractAddress, assets: u256, - shares:u256 + shares: u256 ) { let expected = ERC4626Component::Event::Deposit(Deposit { sender, owner, assets, shares }); self.assert_emitted_single(contract, expected); @@ -313,7 +323,7 @@ pub impl ERC4626SpyHelpersImpl of ERC4626SpyHelpers { sender: ContractAddress, owner: ContractAddress, assets: u256, - shares:u256 + shares: u256 ) { self.assert_event_deposit(contract, sender, owner, assets, shares); self.assert_no_events_left_from(contract); @@ -326,9 +336,11 @@ pub impl ERC4626SpyHelpersImpl of ERC4626SpyHelpers { receiver: ContractAddress, owner: ContractAddress, assets: u256, - shares:u256 + shares: u256 ) { - let expected = ERC4626Component::Event::Withdraw(Withdraw { sender, receiver, owner, assets, shares }); + let expected = ERC4626Component::Event::Withdraw( + Withdraw { sender, receiver, owner, assets, shares } + ); self.assert_emitted_single(contract, expected); } @@ -339,7 +351,7 @@ pub impl ERC4626SpyHelpersImpl of ERC4626SpyHelpers { receiver: ContractAddress, owner: ContractAddress, assets: u256, - shares:u256 + shares: u256 ) { self.assert_event_withdraw(contract, sender, receiver, owner, assets, shares); self.assert_no_events_left_from(contract); diff --git a/packages/token/src/tests/mocks.cairo b/packages/token/src/tests/mocks.cairo index 8e5e59d57..ffab2fd95 100644 --- a/packages/token/src/tests/mocks.cairo +++ b/packages/token/src/tests/mocks.cairo @@ -11,3 +11,4 @@ pub(crate) mod erc4626_mocks; //pub(crate) mod erc721_receiver_mocks; //pub(crate) mod non_implementing_mock; //pub(crate) mod src5_mocks; + diff --git a/packages/token/src/tests/mocks/erc20_reentrant.cairo b/packages/token/src/tests/mocks/erc20_reentrant.cairo index 734bd93ed..70709ea58 100644 --- a/packages/token/src/tests/mocks/erc20_reentrant.cairo +++ b/packages/token/src/tests/mocks/erc20_reentrant.cairo @@ -47,8 +47,8 @@ pub(crate) trait IERC20Reentrant { pub(crate) mod ERC20ReentrantMock { use crate::erc20::ERC20Component; use starknet::ContractAddress; - use starknet::storage::{Vec, MutableVecTrait}; use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + use starknet::storage::{Vec, MutableVecTrait}; use starknet::syscalls::call_contract_syscall; use super::Type; @@ -133,9 +133,12 @@ pub(crate) mod ERC20ReentrantMock { let target = self.reenter_target.read(); let selector = self.reenter_selector.read(); let mut calldata = array![]; - for i in 0..self.reenter_calldata.len() { - calldata.append(self.reenter_calldata.at(i).read()); - }; + for i in 0 + ..self + .reenter_calldata + .len() { + calldata.append(self.reenter_calldata.at(i).read()); + }; call_contract_syscall(target, selector, calldata.span()).unwrap(); } @@ -146,13 +149,8 @@ pub(crate) mod ERC20ReentrantMock { } #[constructor] - fn constructor( - ref self: ContractState, - name: ByteArray, - symbol: ByteArray - ) { + fn constructor(ref self: ContractState, name: ByteArray, symbol: ByteArray) { self.erc20.initializer(name, symbol); self.reenter_type.write(Type::No); } - -} \ No newline at end of file +} diff --git a/packages/token/src/tests/mocks/erc4626_mocks.cairo b/packages/token/src/tests/mocks/erc4626_mocks.cairo index 11b77a678..e50c2f9a9 100644 --- a/packages/token/src/tests/mocks/erc4626_mocks.cairo +++ b/packages/token/src/tests/mocks/erc4626_mocks.cairo @@ -1,8 +1,8 @@ #[starknet::contract] pub(crate) mod ERC4626Mock { - use crate::erc20::{ERC20Component, ERC20HooksEmptyImpl}; - use crate::erc20::extensions::erc4626::ERC4626Component; use crate::erc20::extensions::erc4626::ERC4626Component::InternalTrait as ERC4626InternalTrait; + use crate::erc20::extensions::erc4626::ERC4626Component; + use crate::erc20::{ERC20Component, ERC20HooksEmptyImpl}; use starknet::ContractAddress; component!(path: ERC4626Component, storage: erc4626, event: ERC4626Event); From a45c8dcbae2b59a86b16299890c20baa1d9e8ecc Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 6 Oct 2024 02:25:29 -0500 Subject: [PATCH 23/90] clean up power fn --- packages/utils/src/math.cairo | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/utils/src/math.cairo b/packages/utils/src/math.cairo index e54e2efc1..a64df996f 100644 --- a/packages/utils/src/math.cairo +++ b/packages/utils/src/math.cairo @@ -9,14 +9,12 @@ pub fn power, +PartialEq, +TryInto, +Into, +Int base: T, exp: T ) -> T { assert!(base != 0_u8.into(), "Math: base cannot be zero"); - let mut base: u256 = base.into(); - let mut exp: u256 = exp.into(); + let base: u256 = base.into(); + let exp: u256 = exp.into(); let mut result: u256 = 1; - let mut i: u256 = 0; - while (i < exp) { + for _ in 0..exp { result *= base; - i += 1; }; result.try_into().unwrap() From bb1cdc26522738bf7f5efdad5d093114996e5f80 Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 6 Oct 2024 02:36:15 -0500 Subject: [PATCH 24/90] simplify operation --- packages/utils/src/math.cairo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/utils/src/math.cairo b/packages/utils/src/math.cairo index a64df996f..1444f5b1f 100644 --- a/packages/utils/src/math.cairo +++ b/packages/utils/src/math.cairo @@ -66,7 +66,7 @@ pub fn u256_mul_div(x: u256, y: u256, denominator: u256, rounding: Rounding) -> let felt_is_round_up: felt252 = round_up(rounding).into(); let has_remainder: felt252 = (r > 0).into(); - q + BitAnd::bitand(felt_is_round_up.into(), has_remainder.into()) + q + (felt_is_round_up.into() & has_remainder.into()) } pub fn _raw_u256_mul_div(x: u256, y: u256, denominator: u256) -> (u256, u256) { From f2681e5652391eae61988768fbfda3d3437c0813 Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 6 Oct 2024 12:19:47 -0500 Subject: [PATCH 25/90] add comments, fix visibility --- packages/utils/src/math.cairo | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/utils/src/math.cairo b/packages/utils/src/math.cairo index 1444f5b1f..7b09e7db9 100644 --- a/packages/utils/src/math.cairo +++ b/packages/utils/src/math.cairo @@ -5,6 +5,7 @@ use core::integer::u512_safe_div_rem_by_u256; use core::num::traits::WideMul; use core::traits::{Into, BitAnd, BitXor}; +/// ADD MEE pub fn power, +PartialEq, +TryInto, +Into, +Into>( base: T, exp: T ) -> T { @@ -59,6 +60,7 @@ fn round_up(rounding: Rounding) -> bool { u8_rounding % 2 == 1 } +/// ADD MEEE pub fn u256_mul_div(x: u256, y: u256, denominator: u256, rounding: Rounding) -> u256 { let (q, r) = _raw_u256_mul_div(x, y, denominator); @@ -69,7 +71,7 @@ pub fn u256_mul_div(x: u256, y: u256, denominator: u256, rounding: Rounding) -> q + (felt_is_round_up.into() & has_remainder.into()) } -pub fn _raw_u256_mul_div(x: u256, y: u256, denominator: u256) -> (u256, u256) { +fn _raw_u256_mul_div(x: u256, y: u256, denominator: u256) -> (u256, u256) { assert(denominator != 0, 'Math: division by zero'); let p = x.wide_mul(y); let (mut q, r) = u512_safe_div_rem_by_u256(p, denominator.try_into().unwrap()); From f401df1b033bd9337c3f755fb2dd72b3ed0161dd Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 6 Oct 2024 12:56:38 -0500 Subject: [PATCH 26/90] move fn, remove tests --- packages/utils/src/math.cairo | 133 ++++------------------------------ 1 file changed, 16 insertions(+), 117 deletions(-) diff --git a/packages/utils/src/math.cairo b/packages/utils/src/math.cairo index 7b09e7db9..7542a6da5 100644 --- a/packages/utils/src/math.cairo +++ b/packages/utils/src/math.cairo @@ -5,22 +5,6 @@ use core::integer::u512_safe_div_rem_by_u256; use core::num::traits::WideMul; use core::traits::{Into, BitAnd, BitXor}; -/// ADD MEE -pub fn power, +PartialEq, +TryInto, +Into, +Into>( - base: T, exp: T -) -> T { - assert!(base != 0_u8.into(), "Math: base cannot be zero"); - let base: u256 = base.into(); - let exp: u256 = exp.into(); - let mut result: u256 = 1; - - for _ in 0..exp { - result *= base; - }; - - result.try_into().unwrap() -} - /// Returns the average of two numbers. The result is rounded down. pub fn average< T, @@ -38,6 +22,22 @@ pub fn average< (a & b) + (a ^ b) / 2_u8.into() } +/// ADD MEE +pub fn power, +PartialEq, +TryInto, +Into, +Into>( + base: T, exp: T +) -> T { + assert!(base != 0_u8.into(), "Math: base cannot be zero"); + let base: u256 = base.into(); + let exp: u256 = exp.into(); + let mut result: u256 = 1; + + for _ in 0..exp { + result *= base; + }; + + result.try_into().unwrap() +} + #[derive(Drop, Copy, Debug)] pub enum Rounding { Floor, // Toward negative infinity @@ -78,104 +78,3 @@ fn _raw_u256_mul_div(x: u256, y: u256, denominator: u256) -> (u256, u256) { let q = q.try_into().expect('Math: quotient > u256'); (q, r) } - -#[cfg(test)] -mod Test { - use core::num::traits::Bounded; - use super::Rounding; - use super::u256_mul_div; - - #[test] - #[should_panic(expected: 'Math: division by zero')] - fn test_mul_div_divide_by_zero() { - let x = 1; - let y = 1; - let denominator = 0; - - u256_mul_div(x, y, denominator, Rounding::Floor); - } - - #[test] - #[should_panic(expected: 'Math: quotient > u256')] - fn test_mul_div_result_gt_u256() { - let x = 5; - let y = Bounded::MAX; - let denominator = 2; - - u256_mul_div(x, y, denominator, Rounding::Floor); - } - - #[test] - fn test_mul_div_round_down_small_values() { - let round_down = array![Rounding::Floor, Rounding::Trunc]; - let args_list = array![ // (x, y, denominator, expected result) - (3, 4, 5, 2), (3, 5, 5, 3)] - .span(); - - for round in round_down { - for args in args_list { - let (x, y, denominator, expected) = args; - assert_eq!(u256_mul_div(*x, *y, *denominator, round), *expected); - } - } - } - - #[test] - fn test_mul_div_round_down_large_values() { - let round_down = array![Rounding::Floor, Rounding::Trunc]; - let u256_max: u256 = Bounded::MAX; - let args_list = array![ - // (x, y, denominator, expected result) - (42, u256_max - 1, u256_max, 41), - (17, u256_max, u256_max, 17), - (u256_max - 1, u256_max - 1, u256_max, u256_max - 2), - (u256_max, u256_max - 1, u256_max, u256_max - 1), - (u256_max, u256_max, u256_max, u256_max) - ] - .span(); - - for round in round_down { - for args in args_list { - let (x, y, denominator, expected) = args; - assert_eq!(u256_mul_div(*x, *y, *denominator, round), *expected); - }; - }; - } - - #[test] - fn test_mul_div_round_up_small_values() { - let round_up = array![Rounding::Ceil, Rounding::Expand]; - let args_list = array![ // (x, y, denominator, expected result) - (3, 4, 5, 3), (3, 5, 5, 3)] - .span(); - - for round in round_up { - for args in args_list { - let (x, y, denominator, expected) = args; - assert_eq!(u256_mul_div(*x, *y, *denominator, round), *expected); - } - } - } - - #[test] - fn test_mul_div_round_up_large_values() { - let round_up = array![Rounding::Ceil, Rounding::Expand]; - let u256_max: u256 = Bounded::MAX; - let args_list = array![ - // (x, y, denominator, expected result) - (42, u256_max - 1, u256_max, 42), - (17, u256_max, u256_max, 17), - (u256_max - 1, u256_max - 1, u256_max, u256_max - 1), - (u256_max, u256_max - 1, u256_max, u256_max - 1), - (u256_max, u256_max, u256_max, u256_max) - ] - .span(); - - for round in round_up { - for args in args_list { - let (x, y, denominator, expected) = args; - assert_eq!(u256_mul_div(*x, *y, *denominator, round), *expected); - }; - }; - } -} From 4bdde8d501b996c8c4b7cae967170dce9a19d06c Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 6 Oct 2024 12:56:50 -0500 Subject: [PATCH 27/90] add test_math mod --- packages/utils/src/tests.cairo | 1 + packages/utils/src/tests/test_math.cairo | 97 ++++++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 packages/utils/src/tests/test_math.cairo diff --git a/packages/utils/src/tests.cairo b/packages/utils/src/tests.cairo index 37cf414d3..ffa8f265d 100644 --- a/packages/utils/src/tests.cairo +++ b/packages/utils/src/tests.cairo @@ -1,4 +1,5 @@ pub(crate) mod mocks; +mod test_math; mod test_nonces; mod test_snip12; diff --git a/packages/utils/src/tests/test_math.cairo b/packages/utils/src/tests/test_math.cairo new file mode 100644 index 000000000..dd70c7fde --- /dev/null +++ b/packages/utils/src/tests/test_math.cairo @@ -0,0 +1,97 @@ +use core::num::traits::Bounded; +use crate::math::Rounding; +use crate::math::{u256_mul_div, power}; + +#[test] +#[should_panic(expected: 'Math: division by zero')] +fn test_mul_div_divide_by_zero() { + let x = 1; + let y = 1; + let denominator = 0; + + u256_mul_div(x, y, denominator, Rounding::Floor); +} + +#[test] +#[should_panic(expected: 'Math: quotient > u256')] +fn test_mul_div_result_gt_u256() { + let x = 5; + let y = Bounded::MAX; + let denominator = 2; + + u256_mul_div(x, y, denominator, Rounding::Floor); +} + +#[test] +fn test_mul_div_round_down_small_values() { + let round_down = array![Rounding::Floor, Rounding::Trunc]; + let args_list = array![ // (x, y, denominator, expected result) + (3, 4, 5, 2), (3, 5, 5, 3)] + .span(); + + for round in round_down { + for args in args_list { + let (x, y, denominator, expected) = args; + assert_eq!(u256_mul_div(*x, *y, *denominator, round), *expected); + } + } +} + +#[test] +fn test_mul_div_round_down_large_values() { + let round_down = array![Rounding::Floor, Rounding::Trunc]; + let u256_max: u256 = Bounded::MAX; + let args_list = array![ + // (x, y, denominator, expected result) + (42, u256_max - 1, u256_max, 41), + (17, u256_max, u256_max, 17), + (u256_max - 1, u256_max - 1, u256_max, u256_max - 2), + (u256_max, u256_max - 1, u256_max, u256_max - 1), + (u256_max, u256_max, u256_max, u256_max) + ] + .span(); + + for round in round_down { + for args in args_list { + let (x, y, denominator, expected) = args; + assert_eq!(u256_mul_div(*x, *y, *denominator, round), *expected); + }; + }; +} + +#[test] +fn test_mul_div_round_up_small_values() { + let round_up = array![Rounding::Ceil, Rounding::Expand]; + let args_list = array![ // (x, y, denominator, expected result) + (3, 4, 5, 3), (3, 5, 5, 3)] + .span(); + + for round in round_up { + for args in args_list { + let (x, y, denominator, expected) = args; + assert_eq!(u256_mul_div(*x, *y, *denominator, round), *expected); + } + } +} + +#[test] +fn test_mul_div_round_up_large_values() { + let round_up = array![Rounding::Ceil, Rounding::Expand]; + let u256_max: u256 = Bounded::MAX; + let args_list = array![ + // (x, y, denominator, expected result) + (42, u256_max - 1, u256_max, 42), + (17, u256_max, u256_max, 17), + (u256_max - 1, u256_max - 1, u256_max, u256_max - 1), + (u256_max, u256_max - 1, u256_max, u256_max - 1), + (u256_max, u256_max, u256_max, u256_max) + ] + .span(); + + for round in round_up { + for args in args_list { + let (x, y, denominator, expected) = args; + assert_eq!(u256_mul_div(*x, *y, *denominator, round), *expected); + }; + }; +} From ccc87842fbe9ad33c1df419196de343521f689cd Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 7 Oct 2024 20:05:51 -0500 Subject: [PATCH 28/90] add mint to mock vault construction --- .../token/src/tests/erc20/test_erc4626.cairo | 31 ++++++++++++------- packages/token/src/tests/mocks.cairo | 1 + .../token/src/tests/mocks/erc4626_mocks.cairo | 5 ++- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/packages/token/src/tests/erc20/test_erc4626.cairo b/packages/token/src/tests/erc20/test_erc4626.cairo index e48096a71..6105e0a89 100644 --- a/packages/token/src/tests/erc20/test_erc4626.cairo +++ b/packages/token/src/tests/erc20/test_erc4626.cairo @@ -2,10 +2,10 @@ use core::num::traits::Bounded; //use crate::erc20::ERC20Component; use crate::erc20::ERC20Component::InternalImpl as ERC20InternalImpl; use crate::erc20::extensions::erc4626::DefaultConfig; -use crate::erc20::extensions::erc4626::ERC4626Component::{Deposit, Withdraw}; use crate::erc20::extensions::erc4626::ERC4626Component::{ ERC4626Impl, ERC4626MetadataImpl, InternalImpl }; +use crate::erc20::extensions::erc4626::ERC4626Component::{Deposit, Withdraw}; use crate::erc20::extensions::erc4626::ERC4626Component; use crate::erc20::extensions::erc4626::interface::{ERC4626ABIDispatcher, ERC4626ABIDispatcherTrait}; use crate::tests::mocks::erc20_reentrant::Type; @@ -69,26 +69,33 @@ fn deploy_asset() -> IERC20ReentrantDispatcher { IERC20ReentrantDispatcher { contract_address } } -fn deploy_vault(asset_address: ContractAddress) -> ERC4626ABIDispatcher { +fn deploy_vault( + asset_address: ContractAddress, initial_supply: u256, recipient: ContractAddress +) -> ERC4626ABIDispatcher { let mut vault_calldata: Array = array![]; vault_calldata.append_serde(VAULT_NAME()); vault_calldata.append_serde(VAULT_SYMBOL()); vault_calldata.append_serde(asset_address); + vault_calldata.append_serde(initial_supply); + vault_calldata.append_serde(recipient); let contract_address = utils::declare_and_deploy("ERC4626Mock", vault_calldata); ERC4626ABIDispatcher { contract_address } } -fn setup() -> (IERC20ReentrantDispatcher, ERC4626ABIDispatcher) { +fn setup_empty() -> (IERC20ReentrantDispatcher, ERC4626ABIDispatcher) { let mut asset = deploy_asset(); - let mut vault = deploy_vault(asset.contract_address); + + let no_amount = 0; + let recipient = HOLDER(); + let mut vault = deploy_vault(asset.contract_address, no_amount, recipient); (asset, vault) } // Further testing required for decimals once design is finalized #[test] fn test_offset_decimals() { - let (_, vault) = setup(); + let (_, vault) = setup_empty(); let decimals = vault.decimals(); assert_eq!(decimals, 19); @@ -101,7 +108,7 @@ fn test_offset_decimals() { #[test] #[ignore] fn test_share_price_with_reentrancy_before() { - let (asset, vault) = setup(); + let (asset, vault) = setup_empty(); let amount = 1_000_000_000_000_000_000; let reenter_amt = 1_000_000_000; @@ -146,7 +153,7 @@ fn test_share_price_with_reentrancy_before() { #[test] fn test_metadata() { - let (asset, vault) = setup(); + let (asset, vault) = setup_empty(); let name = vault.name(); let symbol = vault.symbol(); let decimals = vault.decimals(); @@ -164,7 +171,7 @@ fn test_metadata() { #[test] fn test_init_vault_status() { - let (_, vault) = setup(); + let (_, vault) = setup_empty(); let total_assets = vault.total_assets(); assert_eq!(total_assets, 0); @@ -172,7 +179,7 @@ fn test_init_vault_status() { #[test] fn test_deposit() { - let (asset, vault) = setup(); + let (asset, vault) = setup_empty(); let amount = parse_token(1); // Setup @@ -207,7 +214,7 @@ fn test_deposit() { #[test] fn test_mint() { - let (asset, vault) = setup(); + let (asset, vault) = setup_empty(); // Setup asset.unsafe_mint(HOLDER(), Bounded::MAX / 2); @@ -243,7 +250,7 @@ fn test_mint() { #[test] fn test_withdraw() { - let (asset, vault) = setup(); + let (asset, vault) = setup_empty(); // Setup asset.unsafe_mint(HOLDER(), Bounded::MAX / 2); @@ -272,7 +279,7 @@ fn test_withdraw() { #[test] fn test_redeem() { - let (asset, vault) = setup(); + let (asset, vault) = setup_empty(); // Setup asset.unsafe_mint(HOLDER(), Bounded::MAX / 2); diff --git a/packages/token/src/tests/mocks.cairo b/packages/token/src/tests/mocks.cairo index ffab2fd95..563ee3fd2 100644 --- a/packages/token/src/tests/mocks.cairo +++ b/packages/token/src/tests/mocks.cairo @@ -12,3 +12,4 @@ pub(crate) mod erc4626_mocks; //pub(crate) mod non_implementing_mock; //pub(crate) mod src5_mocks; + diff --git a/packages/token/src/tests/mocks/erc4626_mocks.cairo b/packages/token/src/tests/mocks/erc4626_mocks.cairo index e50c2f9a9..490a2fc42 100644 --- a/packages/token/src/tests/mocks/erc4626_mocks.cairo +++ b/packages/token/src/tests/mocks/erc4626_mocks.cairo @@ -51,9 +51,12 @@ pub(crate) mod ERC4626Mock { ref self: ContractState, name: ByteArray, symbol: ByteArray, - underlying_asset: ContractAddress + underlying_asset: ContractAddress, + initial_supply: u256, + recipient: ContractAddress ) { self.erc20.initializer(name, symbol); + self.erc20.mint(recipient, initial_supply); self.erc4626.initializer(underlying_asset); } } From 87a84abb3c9eadc304a3e19e362f5a297ef8a8df Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 7 Oct 2024 22:53:32 -0500 Subject: [PATCH 29/90] add full vault tests --- .../token/src/tests/erc20/test_erc4626.cairo | 465 ++++++++++++++++-- 1 file changed, 426 insertions(+), 39 deletions(-) diff --git a/packages/token/src/tests/erc20/test_erc4626.cairo b/packages/token/src/tests/erc20/test_erc4626.cairo index 6105e0a89..1f319f307 100644 --- a/packages/token/src/tests/erc20/test_erc4626.cairo +++ b/packages/token/src/tests/erc20/test_erc4626.cairo @@ -1,11 +1,10 @@ use core::num::traits::Bounded; -//use crate::erc20::ERC20Component; use crate::erc20::ERC20Component::InternalImpl as ERC20InternalImpl; use crate::erc20::extensions::erc4626::DefaultConfig; +use crate::erc20::extensions::erc4626::ERC4626Component::{Deposit, Withdraw}; use crate::erc20::extensions::erc4626::ERC4626Component::{ ERC4626Impl, ERC4626MetadataImpl, InternalImpl }; -use crate::erc20::extensions::erc4626::ERC4626Component::{Deposit, Withdraw}; use crate::erc20::extensions::erc4626::ERC4626Component; use crate::erc20::extensions::erc4626::interface::{ERC4626ABIDispatcher, ERC4626ABIDispatcherTrait}; use crate::tests::mocks::erc20_reentrant::Type; @@ -13,10 +12,8 @@ use crate::tests::mocks::erc20_reentrant::{ IERC20ReentrantDispatcher, IERC20ReentrantDispatcherTrait }; use openzeppelin_test_common::erc20::ERC20SpyHelpers; -//use crate::tests::mocks::erc20_reentrant::ERC20ReentrantMock; use openzeppelin_testing as utils; -use openzeppelin_testing::constants::{NAME, SYMBOL, OTHER, RECIPIENT, ZERO}; -//use crate::tests::mocks::erc4626_mocks::ERC4626Mock; +use openzeppelin_testing::constants::{NAME, SYMBOL, OTHER, RECIPIENT, ZERO, SPENDER}; use openzeppelin_testing::events::EventSpyExt; use openzeppelin_utils::math; use openzeppelin_utils::serde::SerializedAppend; @@ -25,10 +22,6 @@ use snforge_std::{ }; use starknet::{ContractAddress, contract_address_const}; -fn ASSET_ADDRESS() -> ContractAddress { - contract_address_const::<'ASSET_ADDRESS'>() -} - fn HOLDER() -> ContractAddress { contract_address_const::<'HOLDER'>() } @@ -44,10 +37,6 @@ fn VAULT_SYMBOL() -> ByteArray { const DEFAULT_DECIMALS: u8 = 18; const OFFSET_DECIMALS: u8 = 1; -// -// Helpers -// - fn parse_token(token: u256) -> u256 { token * math::power(10, DEFAULT_DECIMALS.into()) } @@ -83,7 +72,7 @@ fn deploy_vault( ERC4626ABIDispatcher { contract_address } } -fn setup_empty() -> (IERC20ReentrantDispatcher, ERC4626ABIDispatcher) { +fn setup_initial_state() -> (IERC20ReentrantDispatcher, ERC4626ABIDispatcher) { let mut asset = deploy_asset(); let no_amount = 0; @@ -95,7 +84,7 @@ fn setup_empty() -> (IERC20ReentrantDispatcher, ERC4626ABIDispatcher) { // Further testing required for decimals once design is finalized #[test] fn test_offset_decimals() { - let (_, vault) = setup_empty(); + let (_, vault) = setup_initial_state(); let decimals = vault.decimals(); assert_eq!(decimals, 19); @@ -153,7 +142,7 @@ fn test_share_price_with_reentrancy_before() { #[test] fn test_metadata() { - let (asset, vault) = setup_empty(); + let (asset, vault) = setup_initial_state(); let name = vault.name(); let symbol = vault.symbol(); let decimals = vault.decimals(); @@ -169,6 +158,21 @@ fn test_metadata() { // Empty vault: no assets, no shares // +fn setup_empty() -> (IERC20ReentrantDispatcher, ERC4626ABIDispatcher) { + let mut asset = deploy_asset(); + + let no_amount = 0; + let recipient = HOLDER(); + let mut vault = deploy_vault(asset.contract_address, no_amount, recipient); + + // Mint assets to HOLDER and approve vault + asset.unsafe_mint(HOLDER(), Bounded::MAX / 2); // 50% of max + cheat_caller_address(asset.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); + asset.approve(vault.contract_address, Bounded::MAX); + + (asset, vault) +} + #[test] fn test_init_vault_status() { let (_, vault) = setup_empty(); @@ -182,11 +186,6 @@ fn test_deposit() { let (asset, vault) = setup_empty(); let amount = parse_token(1); - // Setup - asset.unsafe_mint(HOLDER(), Bounded::MAX / 2); - cheat_caller_address(asset.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); - asset.approve(vault.contract_address, Bounded::MAX); - // Check max deposit let max_deposit = vault.max_deposit(HOLDER()); assert_eq!(max_deposit, Bounded::MAX); @@ -203,24 +202,23 @@ fn test_deposit() { cheat_caller_address(vault.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); let shares = vault.deposit(amount, RECIPIENT()); + // Check balances + let holder_balance_after = asset.balance_of(HOLDER()); + assert_eq!(holder_balance_after, holder_balance_before - amount); + + let recipient_shares = vault.balance_of(RECIPIENT()); + assert_eq!(recipient_shares, exp_shares); + // Check events spy.assert_event_transfer(asset.contract_address, HOLDER(), vault.contract_address, amount); spy.assert_event_transfer(vault.contract_address, ZERO(), RECIPIENT(), shares); spy.assert_only_event_deposit(vault.contract_address, HOLDER(), RECIPIENT(), amount, shares); - - let holder_balance_after = asset.balance_of(HOLDER()); - assert_eq!(holder_balance_after, holder_balance_before - amount); } #[test] fn test_mint() { let (asset, vault) = setup_empty(); - // Setup - asset.unsafe_mint(HOLDER(), Bounded::MAX / 2); - cheat_caller_address(asset.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); - asset.approve(vault.contract_address, Bounded::MAX); - // Check max mint let max_mint = vault.max_mint(HOLDER()); assert_eq!(max_mint, Bounded::MAX); @@ -231,11 +229,19 @@ fn test_mint() { assert_eq!(preview_mint, exp_assets); let mut spy = spy_events(); + let holder_balance_before = asset.balance_of(HOLDER()); // Mint cheat_caller_address(vault.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); vault.mint(parse_share(1), RECIPIENT()); + // Check balances + let holder_balance_after = asset.balance_of(HOLDER()); + assert_eq!(holder_balance_after, holder_balance_before - parse_token(1)); + + let recipient_shares = vault.balance_of(RECIPIENT()); + assert_eq!(recipient_shares, parse_share(1)); + // Check events spy .assert_event_transfer( @@ -252,11 +258,6 @@ fn test_mint() { fn test_withdraw() { let (asset, vault) = setup_empty(); - // Setup - asset.unsafe_mint(HOLDER(), Bounded::MAX / 2); - cheat_caller_address(asset.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); - asset.approve(vault.contract_address, Bounded::MAX); - // Check max mint let max_withdraw = vault.max_withdraw(HOLDER()); assert_eq!(max_withdraw, 0); @@ -281,11 +282,183 @@ fn test_withdraw() { fn test_redeem() { let (asset, vault) = setup_empty(); - // Setup - asset.unsafe_mint(HOLDER(), Bounded::MAX / 2); + // Check max redeem + let max_redeem = vault.max_redeem(HOLDER()); + assert_eq!(max_redeem, 0); + + // Check preview redeem + let preview_redeem = vault.preview_redeem(0); + assert_eq!(preview_redeem, 0); + + let mut spy = spy_events(); + + // Redeem + cheat_caller_address(vault.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); + vault.redeem(0, RECIPIENT(), HOLDER()); + + // Check events + spy.assert_event_transfer(vault.contract_address, HOLDER(), ZERO(), 0); + spy.assert_event_transfer(asset.contract_address, vault.contract_address, RECIPIENT(), 0); + spy.assert_only_event_withdraw(vault.contract_address, HOLDER(), RECIPIENT(), HOLDER(), 0, 0); +} + +// +// Inflation attack: Offset price by direct deposit of assets +// + +fn setup_inflation_attack() -> (IERC20ReentrantDispatcher, ERC4626ABIDispatcher) { + let mut asset = deploy_asset(); + + let no_amount = 0; + let recipient = HOLDER(); + let mut vault = deploy_vault(asset.contract_address, no_amount, recipient); + + // Mint assets to HOLDER and approve vault + asset.unsafe_mint(HOLDER(), Bounded::MAX / 2); // 50% of max cheat_caller_address(asset.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); asset.approve(vault.contract_address, Bounded::MAX); + // Donate 1 token to the vault to offset the price + asset.unsafe_mint(vault.contract_address, parse_token(1)); + + (asset, vault) +} + +#[test] +fn test_inflation_attack_status() { + let (_, vault) = setup_inflation_attack(); + + let total_supply = vault.total_supply(); + assert_eq!(total_supply, 0); + + let total_assets = vault.total_assets(); + assert_eq!(total_assets, parse_token(1)); +} + +#[test] +fn test_inflation_attack_deposit() { + let (asset, vault) = setup_inflation_attack(); + let virtual_assets = 1; + let offset = 1; + let virtual_shares = math::power(10, offset); + + let effective_assets = vault.total_assets() + virtual_assets; + let effective_shares = vault.total_supply() + virtual_shares; + + let deposit_assets = parse_token(1); + let expected_shares = (deposit_assets * effective_shares) / effective_assets; + + // Check max deposit + let max_deposit = vault.max_deposit(HOLDER()); + assert_eq!(max_deposit, Bounded::MAX); + + // Check preview deposit + let preview_deposit = vault.preview_deposit(deposit_assets); + assert_eq!(preview_deposit, expected_shares); + + // Before deposit + let holder_balance_before = asset.balance_of(HOLDER()); + let mut spy = spy_events(); + + // Deposit + cheat_caller_address(vault.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); + let shares = vault.deposit(deposit_assets, RECIPIENT()); + + // After deposit + let holder_balance_after = asset.balance_of(HOLDER()); + assert_eq!(holder_balance_after, holder_balance_before - deposit_assets); + + // Check recipient shares + let recipient_balance = vault.balance_of(RECIPIENT()); + assert_eq!(recipient_balance, expected_shares); + + // Check events + spy.assert_event_transfer(asset.contract_address, HOLDER(), vault.contract_address, deposit_assets); + spy.assert_event_transfer(vault.contract_address, ZERO(), RECIPIENT(), shares); + spy.assert_only_event_deposit(vault.contract_address, HOLDER(), RECIPIENT(), deposit_assets, expected_shares); +} + +#[test] +fn test_inflation_attack_mint() { + let (asset, vault) = setup_inflation_attack(); + let virtual_assets = 1; + let offset = 1; + let virtual_shares = math::power(10, offset); + + let effective_assets = vault.total_assets() + virtual_assets; + let effective_shares = vault.total_supply() + virtual_shares; + + let mint_shares = parse_share(1); + let expected_assets = (mint_shares * effective_assets) / effective_shares; + + // Check max mint + let max_mint = vault.max_mint(HOLDER()); + assert_eq!(max_mint, Bounded::MAX); + + // Check preview mint + let preview_mint = vault.preview_mint(mint_shares); + assert_eq!(preview_mint, expected_assets); + + // Capture initial balances + let holder_balance_before = asset.balance_of(HOLDER()); + let vault_balance_before = asset.balance_of(vault.contract_address); + + // Mint + let mut spy = spy_events(); + cheat_caller_address(vault.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); + vault.mint(mint_shares, RECIPIENT()); + + // Check balances + assert_assets_of(asset, HOLDER(), holder_balance_before - expected_assets); + assert_assets_of(asset, vault.contract_address, vault_balance_before + expected_assets); + assert_shares_of(vault, RECIPIENT(), parse_share(1)); + + // Check events + spy + .assert_event_transfer( + asset.contract_address, HOLDER(), vault.contract_address, expected_assets + ); + spy.assert_event_transfer(vault.contract_address, ZERO(), RECIPIENT(), mint_shares); + spy + .assert_only_event_deposit( + vault.contract_address, HOLDER(), RECIPIENT(), expected_assets, mint_shares + ); +} + +#[test] +fn test_inflation_attack_withdraw() { + let (asset, vault) = setup_inflation_attack(); + + // Check max withdraw + let max_withdraw = vault.max_withdraw(HOLDER()); + assert_eq!(max_withdraw, 0); + + // Check preview withdraw + let preview_withdraw = vault.preview_withdraw(0); + assert_eq!(preview_withdraw, 0); + + // Capture initial balances + let holder_balance_before = asset.balance_of(HOLDER()); + let vault_balance_before = asset.balance_of(vault.contract_address); + + // Withdraw + let mut spy = spy_events(); + cheat_caller_address(vault.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); + vault.withdraw(0, RECIPIENT(), HOLDER()); + + // Check balances and events + assert_assets_of(asset, HOLDER(), holder_balance_before); + assert_assets_of(asset, vault.contract_address, vault_balance_before); + + spy.assert_event_transfer(vault.contract_address, HOLDER(), ZERO(), 0); + spy.assert_event_transfer(asset.contract_address, vault.contract_address, RECIPIENT(), 0); + spy.assert_only_event_withdraw(vault.contract_address, HOLDER(), RECIPIENT(), HOLDER(), 0, 0); +} + +#[test] +fn test_inflation_attack_redeem() { + let (asset, vault) = setup_inflation_attack(); + // Check max redeem let max_redeem = vault.max_redeem(HOLDER()); assert_eq!(max_redeem, 0); @@ -294,9 +467,8 @@ fn test_redeem() { let preview_redeem = vault.preview_redeem(0); assert_eq!(preview_redeem, 0); - let mut spy = spy_events(); - // Redeem + let mut spy = spy_events(); cheat_caller_address(vault.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); vault.redeem(0, RECIPIENT(), HOLDER()); @@ -307,9 +479,224 @@ fn test_redeem() { } // -// Helpers +// Full vault: Assets and shares +// + +fn setup_full_vault() -> (IERC20ReentrantDispatcher, ERC4626ABIDispatcher) { + let mut asset = deploy_asset(); + + let shares = parse_share(100); + let recipient = HOLDER(); + + // Add 1 token of underlying asset and 100 shares to the vault + let mut vault = deploy_vault(asset.contract_address, shares, recipient); + asset.unsafe_mint(vault.contract_address, parse_token(1)); + + // Approve SPENDER + cheat_caller_address(vault.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); + vault.approve(SPENDER(), Bounded::MAX); + + // Mint assets to HOLDER, approve vault + asset.unsafe_mint(HOLDER(), Bounded::MAX / 2); // 50% of max + cheat_caller_address(asset.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); + asset.approve(vault.contract_address, Bounded::MAX); + + (asset, vault) +} + +#[test] +fn test_full_vault_status() { + let (_, vault) = setup_full_vault(); + + let total_supply = vault.total_supply(); + assert_eq!(total_supply, parse_share(100)); + + let total_assets = vault.total_assets(); + assert_eq!(total_assets, parse_token(1)); +} + +#[test] +fn test_full_vault_deposit() { + let (asset, vault) = setup_full_vault(); + + let virtual_assets = 1; + let offset = 1; + let virtual_shares = math::power(10, offset); + + let effective_assets = vault.total_assets() + virtual_assets; + let effective_shares = vault.total_supply() + virtual_shares; + + let deposit_assets = parse_token(1); + let expected_shares = (deposit_assets * effective_shares) / effective_assets; + + // Check max deposit + let max_deposit = vault.max_deposit(HOLDER()); + assert_eq!(max_deposit, Bounded::MAX); + + // Check preview deposit + let preview_deposit = vault.preview_deposit(deposit_assets); + assert_eq!(preview_deposit, expected_shares); + + // Before deposit + let holder_balance_before = asset.balance_of(HOLDER()); + + // Deposit + let mut spy = spy_events(); + cheat_caller_address(vault.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); + let shares = vault.deposit(deposit_assets, RECIPIENT()); + + // After deposit + let holder_balance_after = asset.balance_of(HOLDER()); + assert_eq!(holder_balance_after, holder_balance_before - deposit_assets); + + // Check recipient shares + let recipient_balance = vault.balance_of(RECIPIENT()); + assert_eq!(recipient_balance, expected_shares); + + // Check events + spy.assert_event_transfer(asset.contract_address, HOLDER(), vault.contract_address, deposit_assets); + spy.assert_event_transfer(vault.contract_address, ZERO(), RECIPIENT(), shares); + spy.assert_only_event_deposit(vault.contract_address, HOLDER(), RECIPIENT(), deposit_assets, expected_shares); +} + +#[test] +fn test_full_vault_mint() { + let (asset, vault) = setup_full_vault(); + + let virtual_assets = 1; + let offset = 1; + let virtual_shares = math::power(10, offset); + + let effective_assets = vault.total_assets() + virtual_assets; + let effective_shares = vault.total_supply() + virtual_shares; + + let mint_shares = parse_share(1); + let expected_assets = (mint_shares * effective_assets) / effective_shares + 1; // add `1` for the rounding + + // Check max mint + let max_mint = vault.max_mint(HOLDER()); + assert_eq!(max_mint, Bounded::MAX); + + // Check preview mint + let preview_mint = vault.preview_mint(mint_shares); + assert_eq!(preview_mint, expected_assets); + + // Capture initial balances + let holder_balance_before = asset.balance_of(HOLDER()); + let vault_balance_before = asset.balance_of(vault.contract_address); + + // Mint + let mut spy = spy_events(); + cheat_caller_address(vault.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); + vault.mint(mint_shares, RECIPIENT()); + + // Check balances + assert_assets_of(asset, HOLDER(), holder_balance_before - expected_assets); + assert_assets_of(asset, vault.contract_address, vault_balance_before + expected_assets); + assert_shares_of(vault, RECIPIENT(), parse_share(1)); + + // Check events + spy + .assert_event_transfer( + asset.contract_address, HOLDER(), vault.contract_address, expected_assets + ); + spy.assert_event_transfer(vault.contract_address, ZERO(), RECIPIENT(), mint_shares); + spy + .assert_only_event_deposit( + vault.contract_address, HOLDER(), RECIPIENT(), expected_assets, mint_shares + ); +} + +#[test] +fn test_full_vault_withdraw() { + let (asset, vault) = setup_full_vault(); + + let virtual_assets = 1; + let offset = 1; + let virtual_shares = math::power(10, offset); + + let effective_assets = vault.total_assets() + virtual_assets; + let effective_shares = vault.total_supply() + virtual_shares; + + let withdraw_assets = parse_token(1); + let expected_shares = (withdraw_assets * effective_shares) / effective_assets + 1; // add `1` for the rounding + + // Check max withdraw + let max_withdraw = vault.max_withdraw(HOLDER()); + assert_eq!(max_withdraw, withdraw_assets); + + // Check preview withdraw + let preview_withdraw = vault.preview_withdraw(withdraw_assets); + assert_eq!(preview_withdraw, expected_shares); + + // Capture initial balances + let holder_balance_before = asset.balance_of(HOLDER()); + let vault_balance_before = asset.balance_of(vault.contract_address); + + // Withdraw + let mut spy = spy_events(); + cheat_caller_address(vault.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); + vault.withdraw(withdraw_assets, RECIPIENT(), HOLDER()); + + // Check balances and events + assert_assets_of(asset, HOLDER(), holder_balance_before); + assert_assets_of(asset, RECIPIENT(), withdraw_assets); + assert_assets_of(asset, vault.contract_address, vault_balance_before - withdraw_assets); + + spy.assert_event_transfer(vault.contract_address, HOLDER(), ZERO(), expected_shares); + spy.assert_event_transfer(asset.contract_address, vault.contract_address, RECIPIENT(), withdraw_assets); + spy.assert_only_event_withdraw(vault.contract_address, HOLDER(), RECIPIENT(), HOLDER(), withdraw_assets, expected_shares); +} + +#[test] +fn test_full_vault_withdraw_with_approval() { + let (asset, vault) = setup_full_vault(); + + let virtual_assets = 1; + let offset = 1; + let virtual_shares = math::power(10, offset); + + let effective_assets = vault.total_assets() + virtual_assets; + let effective_shares = vault.total_supply() + virtual_shares; + + let withdraw_assets = parse_token(1); + let expected_shares = (withdraw_assets * effective_shares) / effective_assets + 1; // add `1` for the rounding + + // Withdraw + let mut spy = spy_events(); + cheat_caller_address(vault.contract_address, SPENDER(), CheatSpan::TargetCalls(1)); + vault.withdraw(withdraw_assets, RECIPIENT(), HOLDER()); + + // Check events + spy.assert_event_transfer(vault.contract_address, HOLDER(), ZERO(), expected_shares); + spy.assert_event_transfer(asset.contract_address, vault.contract_address, RECIPIENT(), withdraw_assets); + spy.assert_only_event_withdraw(vault.contract_address, SPENDER(), RECIPIENT(), HOLDER(), withdraw_assets, expected_shares); +} + +#[test] +#[should_panic(expected: 'ERC20: insufficient allowance')] +fn test_full_vault_withdraw_unauthorized() { + let (_, vault) = setup_full_vault(); + let withdraw_assets = parse_token(1); + + cheat_caller_address(vault.contract_address, OTHER(), CheatSpan::TargetCalls(1)); + vault.withdraw(withdraw_assets, RECIPIENT(), HOLDER()); +} + +// +// Assertions/Helpers // +fn assert_shares_of(vault: ERC4626ABIDispatcher, account: ContractAddress, expected_shares: u256) { + let actual_shares = vault.balance_of(account); + assert_eq!(actual_shares, expected_shares); +} + +fn assert_assets_of(asset: IERC20ReentrantDispatcher, account: ContractAddress, expected_assets: u256) { + let actual_assets = asset.balance_of(account); + assert_eq!(actual_assets, expected_assets); +} + #[generate_trait] pub impl ERC4626SpyHelpersImpl of ERC4626SpyHelpers { fn assert_event_deposit( From 501d7b903b444600d3717e935244c2baa155f63c Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 7 Oct 2024 22:54:01 -0500 Subject: [PATCH 30/90] fix fmt --- .../token/src/tests/erc20/test_erc4626.cairo | 65 +++++++++++++++---- 1 file changed, 52 insertions(+), 13 deletions(-) diff --git a/packages/token/src/tests/erc20/test_erc4626.cairo b/packages/token/src/tests/erc20/test_erc4626.cairo index 1f319f307..35a25cfbe 100644 --- a/packages/token/src/tests/erc20/test_erc4626.cairo +++ b/packages/token/src/tests/erc20/test_erc4626.cairo @@ -1,10 +1,10 @@ use core::num::traits::Bounded; use crate::erc20::ERC20Component::InternalImpl as ERC20InternalImpl; use crate::erc20::extensions::erc4626::DefaultConfig; -use crate::erc20::extensions::erc4626::ERC4626Component::{Deposit, Withdraw}; use crate::erc20::extensions::erc4626::ERC4626Component::{ ERC4626Impl, ERC4626MetadataImpl, InternalImpl }; +use crate::erc20::extensions::erc4626::ERC4626Component::{Deposit, Withdraw}; use crate::erc20::extensions::erc4626::ERC4626Component; use crate::erc20::extensions::erc4626::interface::{ERC4626ABIDispatcher, ERC4626ABIDispatcherTrait}; use crate::tests::mocks::erc20_reentrant::Type; @@ -373,9 +373,15 @@ fn test_inflation_attack_deposit() { assert_eq!(recipient_balance, expected_shares); // Check events - spy.assert_event_transfer(asset.contract_address, HOLDER(), vault.contract_address, deposit_assets); + spy + .assert_event_transfer( + asset.contract_address, HOLDER(), vault.contract_address, deposit_assets + ); spy.assert_event_transfer(vault.contract_address, ZERO(), RECIPIENT(), shares); - spy.assert_only_event_deposit(vault.contract_address, HOLDER(), RECIPIENT(), deposit_assets, expected_shares); + spy + .assert_only_event_deposit( + vault.contract_address, HOLDER(), RECIPIENT(), deposit_assets, expected_shares + ); } #[test] @@ -554,9 +560,15 @@ fn test_full_vault_deposit() { assert_eq!(recipient_balance, expected_shares); // Check events - spy.assert_event_transfer(asset.contract_address, HOLDER(), vault.contract_address, deposit_assets); + spy + .assert_event_transfer( + asset.contract_address, HOLDER(), vault.contract_address, deposit_assets + ); spy.assert_event_transfer(vault.contract_address, ZERO(), RECIPIENT(), shares); - spy.assert_only_event_deposit(vault.contract_address, HOLDER(), RECIPIENT(), deposit_assets, expected_shares); + spy + .assert_only_event_deposit( + vault.contract_address, HOLDER(), RECIPIENT(), deposit_assets, expected_shares + ); } #[test] @@ -571,7 +583,8 @@ fn test_full_vault_mint() { let effective_shares = vault.total_supply() + virtual_shares; let mint_shares = parse_share(1); - let expected_assets = (mint_shares * effective_assets) / effective_shares + 1; // add `1` for the rounding + let expected_assets = (mint_shares * effective_assets) / effective_shares + + 1; // add `1` for the rounding // Check max mint let max_mint = vault.max_mint(HOLDER()); @@ -619,7 +632,8 @@ fn test_full_vault_withdraw() { let effective_shares = vault.total_supply() + virtual_shares; let withdraw_assets = parse_token(1); - let expected_shares = (withdraw_assets * effective_shares) / effective_assets + 1; // add `1` for the rounding + let expected_shares = (withdraw_assets * effective_shares) / effective_assets + + 1; // add `1` for the rounding // Check max withdraw let max_withdraw = vault.max_withdraw(HOLDER()); @@ -644,8 +658,19 @@ fn test_full_vault_withdraw() { assert_assets_of(asset, vault.contract_address, vault_balance_before - withdraw_assets); spy.assert_event_transfer(vault.contract_address, HOLDER(), ZERO(), expected_shares); - spy.assert_event_transfer(asset.contract_address, vault.contract_address, RECIPIENT(), withdraw_assets); - spy.assert_only_event_withdraw(vault.contract_address, HOLDER(), RECIPIENT(), HOLDER(), withdraw_assets, expected_shares); + spy + .assert_event_transfer( + asset.contract_address, vault.contract_address, RECIPIENT(), withdraw_assets + ); + spy + .assert_only_event_withdraw( + vault.contract_address, + HOLDER(), + RECIPIENT(), + HOLDER(), + withdraw_assets, + expected_shares + ); } #[test] @@ -660,7 +685,8 @@ fn test_full_vault_withdraw_with_approval() { let effective_shares = vault.total_supply() + virtual_shares; let withdraw_assets = parse_token(1); - let expected_shares = (withdraw_assets * effective_shares) / effective_assets + 1; // add `1` for the rounding + let expected_shares = (withdraw_assets * effective_shares) / effective_assets + + 1; // add `1` for the rounding // Withdraw let mut spy = spy_events(); @@ -669,8 +695,19 @@ fn test_full_vault_withdraw_with_approval() { // Check events spy.assert_event_transfer(vault.contract_address, HOLDER(), ZERO(), expected_shares); - spy.assert_event_transfer(asset.contract_address, vault.contract_address, RECIPIENT(), withdraw_assets); - spy.assert_only_event_withdraw(vault.contract_address, SPENDER(), RECIPIENT(), HOLDER(), withdraw_assets, expected_shares); + spy + .assert_event_transfer( + asset.contract_address, vault.contract_address, RECIPIENT(), withdraw_assets + ); + spy + .assert_only_event_withdraw( + vault.contract_address, + SPENDER(), + RECIPIENT(), + HOLDER(), + withdraw_assets, + expected_shares + ); } #[test] @@ -692,7 +729,9 @@ fn assert_shares_of(vault: ERC4626ABIDispatcher, account: ContractAddress, expec assert_eq!(actual_shares, expected_shares); } -fn assert_assets_of(asset: IERC20ReentrantDispatcher, account: ContractAddress, expected_assets: u256) { +fn assert_assets_of( + asset: IERC20ReentrantDispatcher, account: ContractAddress, expected_assets: u256 +) { let actual_assets = asset.balance_of(account); assert_eq!(actual_assets, expected_assets); } From ee222f5a11f5fe860c299437a2943b7c9e73d097 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 8 Oct 2024 03:13:56 -0500 Subject: [PATCH 31/90] fix assertions --- packages/token/src/erc20/extensions/erc4626/erc4626.cairo | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo index c385ac0eb..9bd4d014b 100644 --- a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -116,11 +116,12 @@ pub mod ERC4626Component { self._convert_to_shares(assets, Rounding::Floor) } + fn deposit( ref self: ComponentState, assets: u256, receiver: ContractAddress ) -> u256 { let max_assets = self.max_deposit(receiver); - assert(assets < max_assets, Errors::EXCEEDED_MAX_DEPOSIT); + assert(assets <= max_assets, Errors::EXCEEDED_MAX_DEPOSIT); let shares = self.preview_deposit(assets); let caller = starknet::get_caller_address(); @@ -140,7 +141,7 @@ pub mod ERC4626Component { ref self: ComponentState, shares: u256, receiver: ContractAddress ) -> u256 { let max_shares = self.max_mint(receiver); - assert(shares < max_shares, Errors::EXCEEDED_MAX_MINT); + assert(shares <= max_shares, Errors::EXCEEDED_MAX_MINT); let assets = self.preview_mint(shares); let caller = starknet::get_caller_address(); From 970cb75adb49861829d16b8ba640ede7316a079d Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 8 Oct 2024 03:14:29 -0500 Subject: [PATCH 32/90] add full vault redeem tests --- .../token/src/tests/erc20/test_erc4626.cairo | 143 ++++++++++++++++-- 1 file changed, 130 insertions(+), 13 deletions(-) diff --git a/packages/token/src/tests/erc20/test_erc4626.cairo b/packages/token/src/tests/erc20/test_erc4626.cairo index 35a25cfbe..1378bea39 100644 --- a/packages/token/src/tests/erc20/test_erc4626.cairo +++ b/packages/token/src/tests/erc20/test_erc4626.cairo @@ -415,9 +415,9 @@ fn test_inflation_attack_mint() { vault.mint(mint_shares, RECIPIENT()); // Check balances - assert_assets_of(asset, HOLDER(), holder_balance_before - expected_assets); - assert_assets_of(asset, vault.contract_address, vault_balance_before + expected_assets); - assert_shares_of(vault, RECIPIENT(), parse_share(1)); + assert_expected_assets(asset, HOLDER(), holder_balance_before - expected_assets); + assert_expected_assets(asset, vault.contract_address, vault_balance_before + expected_assets); + assert_expected_shares(vault, RECIPIENT(), parse_share(1)); // Check events spy @@ -453,8 +453,8 @@ fn test_inflation_attack_withdraw() { vault.withdraw(0, RECIPIENT(), HOLDER()); // Check balances and events - assert_assets_of(asset, HOLDER(), holder_balance_before); - assert_assets_of(asset, vault.contract_address, vault_balance_before); + assert_expected_assets(asset, HOLDER(), holder_balance_before); + assert_expected_assets(asset, vault.contract_address, vault_balance_before); spy.assert_event_transfer(vault.contract_address, HOLDER(), ZERO(), 0); spy.assert_event_transfer(asset.contract_address, vault.contract_address, RECIPIENT(), 0); @@ -604,9 +604,9 @@ fn test_full_vault_mint() { vault.mint(mint_shares, RECIPIENT()); // Check balances - assert_assets_of(asset, HOLDER(), holder_balance_before - expected_assets); - assert_assets_of(asset, vault.contract_address, vault_balance_before + expected_assets); - assert_shares_of(vault, RECIPIENT(), parse_share(1)); + assert_expected_assets(asset, HOLDER(), holder_balance_before - expected_assets); + assert_expected_assets(asset, vault.contract_address, vault_balance_before + expected_assets); + assert_expected_shares(vault, RECIPIENT(), parse_share(1)); // Check events spy @@ -653,9 +653,9 @@ fn test_full_vault_withdraw() { vault.withdraw(withdraw_assets, RECIPIENT(), HOLDER()); // Check balances and events - assert_assets_of(asset, HOLDER(), holder_balance_before); - assert_assets_of(asset, RECIPIENT(), withdraw_assets); - assert_assets_of(asset, vault.contract_address, vault_balance_before - withdraw_assets); + assert_expected_assets(asset, HOLDER(), holder_balance_before); + assert_expected_assets(asset, RECIPIENT(), withdraw_assets); + assert_expected_assets(asset, vault.contract_address, vault_balance_before - withdraw_assets); spy.assert_event_transfer(vault.contract_address, HOLDER(), ZERO(), expected_shares); spy @@ -720,16 +720,133 @@ fn test_full_vault_withdraw_unauthorized() { vault.withdraw(withdraw_assets, RECIPIENT(), HOLDER()); } +#[test] +fn test_full_vault_redeem() { + let (asset, vault) = setup_full_vault(); + + let virtual_assets = 1; + let offset = 1; + let virtual_shares = math::power(10, offset); + + let effective_assets = vault.total_assets() + virtual_assets; + let effective_shares = vault.total_supply() + virtual_shares; + + let redeem_shares = parse_share(100); + let expected_assets = (redeem_shares * effective_assets) / effective_shares; + + // Check max redeem + let max_redeem = vault.max_redeem(HOLDER()); + assert_eq!(max_redeem, redeem_shares); + + // Check preview redeem + let preview_redeem = vault.preview_redeem(redeem_shares); + assert_eq!(preview_redeem, expected_assets); + + // Capture initial balances + let holder_balance_before = asset.balance_of(HOLDER()); + let vault_balance_before = asset.balance_of(vault.contract_address); + let holder_shares_before = vault.balance_of(HOLDER()); + + // Redeem + let mut spy = spy_events(); + cheat_caller_address(vault.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); + vault.redeem(redeem_shares, RECIPIENT(), HOLDER()); + + // Check balances and events + assert_expected_assets(asset, RECIPIENT(), expected_assets); + assert_expected_assets(asset, vault.contract_address, vault_balance_before - expected_assets); + assert_expected_shares(vault, HOLDER(), holder_shares_before - redeem_shares); + + spy.assert_event_transfer(vault.contract_address, HOLDER(), ZERO(), redeem_shares); + spy + .assert_event_transfer( + asset.contract_address, vault.contract_address, RECIPIENT(), expected_assets + ); + spy + .assert_only_event_withdraw( + vault.contract_address, + HOLDER(), + RECIPIENT(), + HOLDER(), + expected_assets, + redeem_shares + ); +} + +#[test] +fn test_full_vault_redeem_with_approval() { + let (asset, vault) = setup_full_vault(); + + let virtual_assets = 1; + let offset = 1; + let virtual_shares = math::power(10, offset); + + let effective_assets = vault.total_assets() + virtual_assets; + let effective_shares = vault.total_supply() + virtual_shares; + + let redeem_shares = parse_share(100); + let expected_assets = (redeem_shares * effective_assets) / effective_shares; + + // Check max redeem + let max_redeem = vault.max_redeem(HOLDER()); + assert_eq!(max_redeem, redeem_shares); + + // Check preview redeem + let preview_redeem = vault.preview_redeem(redeem_shares); + assert_eq!(preview_redeem, expected_assets); + + // Capture initial balances + let holder_balance_before = asset.balance_of(HOLDER()); + let vault_balance_before = asset.balance_of(vault.contract_address); + let holder_shares_before = vault.balance_of(HOLDER()); + + // Redeem from SPENDER + let mut spy = spy_events(); + cheat_caller_address(vault.contract_address, SPENDER(), CheatSpan::TargetCalls(1)); + vault.redeem(redeem_shares, RECIPIENT(), HOLDER()); + + // Check balances and events + assert_expected_assets(asset, RECIPIENT(), expected_assets); + assert_expected_assets(asset, vault.contract_address, vault_balance_before - expected_assets); + assert_expected_shares(vault, HOLDER(), holder_shares_before - redeem_shares); + + spy.assert_event_transfer(vault.contract_address, HOLDER(), ZERO(), redeem_shares); + spy + .assert_event_transfer( + asset.contract_address, vault.contract_address, RECIPIENT(), expected_assets + ); + spy + .assert_only_event_withdraw( + vault.contract_address, + SPENDER(), + RECIPIENT(), + HOLDER(), + expected_assets, + redeem_shares + ); +} + +#[test] +#[should_panic(expected: 'ERC20: insufficient allowance')] +fn test_full_vault_redeem_unauthorized() { + let (asset, vault) = setup_full_vault(); + let redeem_shares = parse_share(100); + + // Unauthorized redeem + cheat_caller_address(vault.contract_address, OTHER(), CheatSpan::TargetCalls(1)); + vault.redeem(redeem_shares, RECIPIENT(), HOLDER()); +} + // // Assertions/Helpers // -fn assert_shares_of(vault: ERC4626ABIDispatcher, account: ContractAddress, expected_shares: u256) { +fn assert_expected_shares(vault: ERC4626ABIDispatcher, account: ContractAddress, expected_shares: u256) { let actual_shares = vault.balance_of(account); assert_eq!(actual_shares, expected_shares); } -fn assert_assets_of( +fn assert_expected_assets( asset: IERC20ReentrantDispatcher, account: ContractAddress, expected_assets: u256 ) { let actual_assets = asset.balance_of(account); From 39b205ca1119f42a003e9ad5345020e9efc3cb2f Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 8 Oct 2024 03:15:04 -0500 Subject: [PATCH 33/90] fix fmt --- .../src/erc20/extensions/erc4626/erc4626.cairo | 1 - .../token/src/tests/erc20/test_erc4626.cairo | 18 +++++------------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo index 9bd4d014b..bf6c794d2 100644 --- a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -116,7 +116,6 @@ pub mod ERC4626Component { self._convert_to_shares(assets, Rounding::Floor) } - fn deposit( ref self: ComponentState, assets: u256, receiver: ContractAddress ) -> u256 { diff --git a/packages/token/src/tests/erc20/test_erc4626.cairo b/packages/token/src/tests/erc20/test_erc4626.cairo index 1378bea39..77fc6a5fe 100644 --- a/packages/token/src/tests/erc20/test_erc4626.cairo +++ b/packages/token/src/tests/erc20/test_erc4626.cairo @@ -764,12 +764,7 @@ fn test_full_vault_redeem() { ); spy .assert_only_event_withdraw( - vault.contract_address, - HOLDER(), - RECIPIENT(), - HOLDER(), - expected_assets, - redeem_shares + vault.contract_address, HOLDER(), RECIPIENT(), HOLDER(), expected_assets, redeem_shares ); } @@ -817,12 +812,7 @@ fn test_full_vault_redeem_with_approval() { ); spy .assert_only_event_withdraw( - vault.contract_address, - SPENDER(), - RECIPIENT(), - HOLDER(), - expected_assets, - redeem_shares + vault.contract_address, SPENDER(), RECIPIENT(), HOLDER(), expected_assets, redeem_shares ); } @@ -841,7 +831,9 @@ fn test_full_vault_redeem_unauthorized() { // Assertions/Helpers // -fn assert_expected_shares(vault: ERC4626ABIDispatcher, account: ContractAddress, expected_shares: u256) { +fn assert_expected_shares( + vault: ERC4626ABIDispatcher, account: ContractAddress, expected_shares: u256 +) { let actual_shares = vault.balance_of(account); assert_eq!(actual_shares, expected_shares); } From 5af5b8b44b6e2723ce5bc1ac34149cfb28f5a372 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 8 Oct 2024 15:54:06 -0500 Subject: [PATCH 34/90] fix fmt --- packages/test_common/src/lib.cairo | 4 ++-- packages/token/src/lib.cairo | 6 +++--- packages/token/src/tests.cairo | 12 ++++++------ packages/token/src/tests/mocks.cairo | 18 ++++++++---------- 4 files changed, 19 insertions(+), 21 deletions(-) diff --git a/packages/test_common/src/lib.cairo b/packages/test_common/src/lib.cairo index dd2bb2209..8013cedc4 100644 --- a/packages/test_common/src/lib.cairo +++ b/packages/test_common/src/lib.cairo @@ -1,7 +1,7 @@ pub mod account; -//pub mod erc1155; +pub mod erc1155; pub mod erc20; -//pub mod erc721; +pub mod erc721; pub mod eth_account; pub mod ownable; pub mod upgrades; diff --git a/packages/token/src/lib.cairo b/packages/token/src/lib.cairo index 71f45bc01..e158d0e7d 100644 --- a/packages/token/src/lib.cairo +++ b/packages/token/src/lib.cairo @@ -1,6 +1,6 @@ -//pub mod common; -//pub mod erc1155; +pub mod common; +pub mod erc1155; pub mod erc20; -//pub mod erc721; +pub mod erc721; pub mod tests; diff --git a/packages/token/src/tests.cairo b/packages/token/src/tests.cairo index 9b29ef1fa..d37791a03 100644 --- a/packages/token/src/tests.cairo +++ b/packages/token/src/tests.cairo @@ -1,10 +1,10 @@ -//#[cfg(test)] -//pub mod erc1155; +#[cfg(test)] +pub mod erc1155; #[cfg(test)] pub mod erc20; -//#[cfg(test)] -//pub mod erc2981; -//#[cfg(test)] -//pub mod erc721; +#[cfg(test)] +pub mod erc2981; +#[cfg(test)] +pub mod erc721; pub(crate) mod mocks; diff --git a/packages/token/src/tests/mocks.cairo b/packages/token/src/tests/mocks.cairo index 563ee3fd2..3197f91bd 100644 --- a/packages/token/src/tests/mocks.cairo +++ b/packages/token/src/tests/mocks.cairo @@ -1,15 +1,13 @@ pub(crate) mod account_mocks; -//pub(crate) mod erc1155_mocks; -//pub(crate) mod erc1155_receiver_mocks; +pub(crate) mod erc1155_mocks; +pub(crate) mod erc1155_receiver_mocks; pub(crate) mod erc20_mocks; pub(crate) mod erc20_reentrant; pub(crate) mod erc20_votes_mocks; +pub(crate) mod erc2981_mocks; pub(crate) mod erc4626_mocks; -//pub(crate) mod erc2981_mocks; -//pub(crate) mod erc721_enumerable_mocks; -//pub(crate) mod erc721_mocks; -//pub(crate) mod erc721_receiver_mocks; -//pub(crate) mod non_implementing_mock; -//pub(crate) mod src5_mocks; - - +pub(crate) mod erc721_enumerable_mocks; +pub(crate) mod erc721_mocks; +pub(crate) mod erc721_receiver_mocks; +pub(crate) mod non_implementing_mock; +pub(crate) mod src5_mocks; From b735b5da519bb78ff13b63c60f91a8da69f9e5f1 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 8 Oct 2024 17:00:08 -0500 Subject: [PATCH 35/90] fix fmt --- packages/test_common/src/mocks.cairo | 2 +- packages/token/src/tests/erc20/test_erc4626.cairo | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/test_common/src/mocks.cairo b/packages/test_common/src/mocks.cairo index ead770b4c..91609cf6c 100644 --- a/packages/test_common/src/mocks.cairo +++ b/packages/test_common/src/mocks.cairo @@ -2,8 +2,8 @@ pub mod access; pub mod account; pub mod erc1155; pub mod erc20; -pub mod erc4626; pub mod erc2981; +pub mod erc4626; pub mod erc721; pub mod non_implementing; pub mod nonces; diff --git a/packages/token/src/tests/erc20/test_erc4626.cairo b/packages/token/src/tests/erc20/test_erc4626.cairo index 253001876..d79b5d8a0 100644 --- a/packages/token/src/tests/erc20/test_erc4626.cairo +++ b/packages/token/src/tests/erc20/test_erc4626.cairo @@ -7,11 +7,11 @@ use crate::erc20::extensions::erc4626::ERC4626Component::{ use crate::erc20::extensions::erc4626::ERC4626Component::{Deposit, Withdraw}; use crate::erc20::extensions::erc4626::ERC4626Component; use crate::erc20::extensions::erc4626::interface::{ERC4626ABIDispatcher, ERC4626ABIDispatcherTrait}; +use openzeppelin_test_common::erc20::ERC20SpyHelpers; use openzeppelin_test_common::mocks::erc20::Type; use openzeppelin_test_common::mocks::erc20::{ IERC20ReentrantDispatcher, IERC20ReentrantDispatcherTrait }; -use openzeppelin_test_common::erc20::ERC20SpyHelpers; use openzeppelin_testing as utils; use openzeppelin_testing::constants::{NAME, SYMBOL, OTHER, RECIPIENT, ZERO, SPENDER}; use openzeppelin_testing::events::EventSpyExt; From 43440d0807ccb4986e6d7896bc742e6b2b97bcb4 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 8 Oct 2024 17:31:32 -0500 Subject: [PATCH 36/90] move erc4626 tests to erc4626 dir --- packages/token/src/tests.cairo | 1 + packages/token/src/tests/erc20.cairo | 1 - packages/token/src/tests/erc4626.cairo | 1 + .../token/src/tests/{erc20 => erc4626}/test_erc4626.cairo | 4 +--- 4 files changed, 3 insertions(+), 4 deletions(-) create mode 100644 packages/token/src/tests/erc4626.cairo rename packages/token/src/tests/{erc20 => erc4626}/test_erc4626.cairo (99%) diff --git a/packages/token/src/tests.cairo b/packages/token/src/tests.cairo index fdb72f5ef..a9654141f 100644 --- a/packages/token/src/tests.cairo +++ b/packages/token/src/tests.cairo @@ -1,4 +1,5 @@ pub mod erc1155; pub mod erc20; pub mod erc2981; +pub mod erc4626; pub mod erc721; diff --git a/packages/token/src/tests/erc20.cairo b/packages/token/src/tests/erc20.cairo index f66c21461..4e62efeae 100644 --- a/packages/token/src/tests/erc20.cairo +++ b/packages/token/src/tests/erc20.cairo @@ -1,3 +1,2 @@ mod test_erc20; mod test_erc20_votes; -mod test_erc4626; diff --git a/packages/token/src/tests/erc4626.cairo b/packages/token/src/tests/erc4626.cairo new file mode 100644 index 000000000..d7bf4e38b --- /dev/null +++ b/packages/token/src/tests/erc4626.cairo @@ -0,0 +1 @@ +mod test_erc4626; diff --git a/packages/token/src/tests/erc20/test_erc4626.cairo b/packages/token/src/tests/erc4626/test_erc4626.cairo similarity index 99% rename from packages/token/src/tests/erc20/test_erc4626.cairo rename to packages/token/src/tests/erc4626/test_erc4626.cairo index d79b5d8a0..293819cec 100644 --- a/packages/token/src/tests/erc20/test_erc4626.cairo +++ b/packages/token/src/tests/erc4626/test_erc4626.cairo @@ -743,7 +743,6 @@ fn test_full_vault_redeem() { assert_eq!(preview_redeem, expected_assets); // Capture initial balances - let holder_balance_before = asset.balance_of(HOLDER()); let vault_balance_before = asset.balance_of(vault.contract_address); let holder_shares_before = vault.balance_of(HOLDER()); @@ -791,7 +790,6 @@ fn test_full_vault_redeem_with_approval() { assert_eq!(preview_redeem, expected_assets); // Capture initial balances - let holder_balance_before = asset.balance_of(HOLDER()); let vault_balance_before = asset.balance_of(vault.contract_address); let holder_shares_before = vault.balance_of(HOLDER()); @@ -819,7 +817,7 @@ fn test_full_vault_redeem_with_approval() { #[test] #[should_panic(expected: 'ERC20: insufficient allowance')] fn test_full_vault_redeem_unauthorized() { - let (asset, vault) = setup_full_vault(); + let (_, vault) = setup_full_vault(); let redeem_shares = parse_share(100); // Unauthorized redeem From 56cea65bcfd2157aaeff31977d30dae68eaaba23 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 9 Oct 2024 03:41:53 -0500 Subject: [PATCH 37/90] add transfer assertion --- packages/token/src/erc20/extensions/erc4626/erc4626.cairo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo index 80a94b7d7..7054c8477 100644 --- a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -277,7 +277,7 @@ pub mod ERC4626Component { // Transfer assets after burn let asset_dispatcher = IERC20Dispatcher { contract_address: self.ERC4626_asset.read() }; - asset_dispatcher.transfer(receiver, assets); + assert(asset_dispatcher.transfer(receiver, assets), Errors::TOKEN_TRANSFER_FAILED); self.emit(Withdraw { sender: caller, receiver, owner, assets, shares }); } From 087093025ab0601b1d05e9c5e468b611739a6f2c Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 10 Oct 2024 19:48:35 -0500 Subject: [PATCH 38/90] add reentrancy tests --- .../src/tests/erc4626/test_erc4626.cairo | 234 ++++++++++++++---- 1 file changed, 186 insertions(+), 48 deletions(-) diff --git a/packages/token/src/tests/erc4626/test_erc4626.cairo b/packages/token/src/tests/erc4626/test_erc4626.cairo index 293819cec..049943d83 100644 --- a/packages/token/src/tests/erc4626/test_erc4626.cairo +++ b/packages/token/src/tests/erc4626/test_erc4626.cairo @@ -18,7 +18,7 @@ use openzeppelin_testing::events::EventSpyExt; use openzeppelin_utils::math; use openzeppelin_utils::serde::SerializedAppend; use snforge_std::{ - start_cheat_caller_address, cheat_caller_address, CheatSpan, spy_events, EventSpy + cheat_caller_address, CheatSpan, spy_events, EventSpy }; use starknet::{ContractAddress, contract_address_const}; @@ -91,55 +91,9 @@ fn test_offset_decimals() { } // -// Reentrancy +// Initial state // -#[test] -#[ignore] -fn test_share_price_with_reentrancy_before() { - let (asset, vault) = setup_empty(); - - let amount = 1_000_000_000_000_000_000; - let reenter_amt = 1_000_000_000; - - asset.unsafe_mint(HOLDER(), amount); - asset.unsafe_mint(OTHER(), amount); - - let approvers: Span = array![HOLDER(), OTHER(), asset.contract_address].span(); - - for approver in approvers { - cheat_caller_address(asset.contract_address, *approver, CheatSpan::TargetCalls(1)); - asset.approve(vault.contract_address, Bounded::MAX); - }; - //stop_cheat_caller_address(asset.contract_address); - - // Mint token for deposit - asset.unsafe_mint(asset.contract_address, reenter_amt); - - // Schedule reentrancy - let mut calldata: Array = array![]; - calldata.append_serde(reenter_amt); - calldata.append_serde(HOLDER()); - - asset - .schedule_reenter( - Type::Before, vault.contract_address, selector!("deposit"), calldata.span() - ); - - // Initial share price - start_cheat_caller_address(vault.contract_address, HOLDER()); - - let shares_for_deposit = vault.preview_deposit(amount); - let _shares_for_reenter = vault.preview_deposit(reenter_amt); - - // Do deposit normally, triggering the hook - vault.deposit(amount, HOLDER()); - - // Assert prices are kept - let shares_after = vault.preview_deposit(amount); - assert_eq!(shares_for_deposit, shares_after, "ahhh"); -} - #[test] fn test_metadata() { let (asset, vault) = setup_initial_state(); @@ -825,6 +779,190 @@ fn test_full_vault_redeem_unauthorized() { vault.redeem(redeem_shares, RECIPIENT(), HOLDER()); } +// +// Reentrancy +// + +fn setup_reentrancy() -> (IERC20ReentrantDispatcher, ERC4626ABIDispatcher) { + let mut asset = deploy_asset(); + + let no_shares = 0; + let recipient = HOLDER(); + let mut vault = deploy_vault(asset.contract_address, no_shares, recipient); + + let value: u256 = 1_000_000_000_000_000_000; + asset.unsafe_mint(HOLDER(), value); + asset.unsafe_mint(OTHER(), value); + + // Set infinite approvals from HOLDER, OTHER, and asset to vault + let approvers: Span = array![HOLDER(), OTHER(), asset.contract_address].span(); + for addr in approvers { + cheat_caller_address(asset.contract_address, *addr, CheatSpan::TargetCalls(1)); + asset.approve(vault.contract_address, Bounded::MAX); + }; + + (asset, vault) +} + +#[test] +fn test_share_price_with_reentrancy_before_deposit() { + let (asset, vault) = setup_reentrancy(); + + let value = 1_000_000_000_000_000_000; + let reenter_value = 1_000_000_000; + + asset.unsafe_mint(asset.contract_address, reenter_value); + + // Schedule reentrancy + let mut calldata: Array = array![]; + calldata.append_serde(reenter_value); + calldata.append_serde(HOLDER()); + asset + .schedule_reenter( + Type::Before, vault.contract_address, selector!("deposit"), calldata.span() + ); + + let shares_for_deposit = vault.preview_deposit(value); + let shares_for_reenter = vault.preview_deposit(reenter_value); + + // Deposit + let mut spy = spy_events(); + cheat_caller_address(vault.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); + vault.deposit(value, HOLDER()); + + // Check price is kept + let after_deposit = vault.preview_deposit(value); + assert_eq!(shares_for_deposit, after_deposit); + + // Check events + // Reentered events come first because they're called in mock ERC20 `before_update` hook + spy.assert_event_transfer(asset.contract_address, asset.contract_address, vault.contract_address, reenter_value); + spy.assert_event_transfer(vault.contract_address, ZERO(), HOLDER(), shares_for_reenter); + spy.assert_event_deposit(vault.contract_address, asset.contract_address, HOLDER(), reenter_value, shares_for_reenter); + + spy.assert_event_transfer(asset.contract_address, HOLDER(), vault.contract_address, value); + spy.assert_event_transfer(vault.contract_address, ZERO(), HOLDER(), shares_for_deposit); + spy.assert_only_event_deposit(vault.contract_address, HOLDER(), HOLDER(), value, shares_for_deposit); +} + +#[test] +fn test_share_price_with_reentrancy_after_withdraw() { + let (asset, vault) = setup_reentrancy(); + + let value = 1_000_000_000_000_000_000; + let reenter_value = 1_000_000_000; + + // Deposit from HOLDER and OTHER + cheat_caller_address(vault.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); + vault.deposit(value, HOLDER()); + + cheat_caller_address(vault.contract_address, OTHER(), CheatSpan::TargetCalls(1)); + vault.deposit(reenter_value, asset.contract_address); + + // Schedule reentrancy + let mut calldata: Array = array![]; + calldata.append_serde(reenter_value); + calldata.append_serde(HOLDER()); + calldata.append_serde(asset.contract_address); + asset + .schedule_reenter( + Type::After, vault.contract_address, selector!("withdraw"), calldata.span() + ); + + let shares_for_withdraw = vault.preview_withdraw(value); + let shares_for_reenter = vault.preview_withdraw(reenter_value); + + // Withdraw + let mut spy = spy_events(); + cheat_caller_address(vault.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); + vault.withdraw(value, HOLDER(), HOLDER()); + + // Check price is kept + let after_withdraw = vault.preview_withdraw(value); + assert_eq!(shares_for_withdraw, after_withdraw); + + // Main withdraw event + spy + .assert_event_withdraw( + vault.contract_address, HOLDER(), HOLDER(), HOLDER(), value, shares_for_withdraw + ); + // Reentrant withdraw event → uses same price + spy + .assert_event_withdraw( + vault.contract_address, asset.contract_address, HOLDER(), asset.contract_address, reenter_value, shares_for_reenter + ); +} + +#[test] +fn test_price_change_during_reentrancy_doesnt_affect_deposit() { + let (asset, vault) = setup_reentrancy(); + + let value: u256 = 1_000_000_000_000_000_000; + let reenter_value: u256 = 1_000_000_000; + + // Schedules a reentrancy from the token contract that messes up the share price + let mut calldata: Array = array![]; + calldata.append_serde(vault.contract_address); + calldata.append_serde(reenter_value); + asset + .schedule_reenter( + Type::Before, asset.contract_address, selector!("unsafe_mint"), calldata.span() + ); + + let shares_before = vault.preview_deposit(value); + + // Deposit + let mut spy = spy_events(); + cheat_caller_address(vault.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); + vault.deposit(value, HOLDER()); + + // Check main event to ensure price is as previewed + spy.assert_event_deposit(vault.contract_address, HOLDER(), HOLDER(), value, shares_before); + + // Check that price is modified after reentrant tx + let shares_after = vault.preview_deposit(value); + assert(shares_after < shares_before, 'Mint should change share price'); +} + +#[test] +fn test_price_change_during_reentrancy_doesnt_affect_withdraw() { + let (asset, vault) = setup_reentrancy(); + + let value: u256 = 1_000_000_000_000_000_000; + let reenter_value: u256 = 1_000_000_000; + + cheat_caller_address(vault.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); + vault.deposit(value, HOLDER()); + cheat_caller_address(vault.contract_address, OTHER(), CheatSpan::TargetCalls(1)); + vault.deposit(value, OTHER()); + + // Schedules a reentrancy from the token contract that messes up the share price + let mut calldata: Array = array![]; + calldata.append_serde(vault.contract_address); + calldata.append_serde(reenter_value); + asset + .schedule_reenter( + Type::After, asset.contract_address, selector!("unsafe_burn"), calldata.span() + ); + + let shares_before = vault.preview_withdraw(value); + + // Withdraw, triggering ERC20 `after_update` hook + let mut spy = spy_events(); + cheat_caller_address(vault.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); + vault.withdraw(value, HOLDER(), HOLDER()); + + // Check main event to ensure price is as previewed + spy + .assert_event_withdraw( + vault.contract_address, HOLDER(), HOLDER(), HOLDER(), value, shares_before + ); + + // Check that price is modified after reentrant tx + let shares_after = vault.preview_withdraw(value); + assert(shares_after > shares_before, 'Burn should change share price'); +} + // // Assertions/Helpers // From 65dddfc45c18e5bf260447f4e98e1844903ecc56 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 10 Oct 2024 19:48:52 -0500 Subject: [PATCH 39/90] expose burn in mock --- packages/test_common/src/mocks/erc20.cairo | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/test_common/src/mocks/erc20.cairo b/packages/test_common/src/mocks/erc20.cairo index 0054bbd09..dd3f7af87 100644 --- a/packages/test_common/src/mocks/erc20.cairo +++ b/packages/test_common/src/mocks/erc20.cairo @@ -194,6 +194,7 @@ pub trait IERC20ReentrantHelpers { ); fn function_call(ref self: TState); fn unsafe_mint(ref self: TState, recipient: ContractAddress, amount: u256); + fn unsafe_burn(ref self: TState, account: ContractAddress, amount: u256); } #[starknet::interface] @@ -207,6 +208,7 @@ pub trait IERC20Reentrant { ); fn function_call(ref self: TState); fn unsafe_mint(ref self: TState, recipient: ContractAddress, amount: u256); + fn unsafe_burn(ref self: TState, account: ContractAddress, amount: u256); // IERC20 fn total_supply(self: @TState) -> u256; @@ -226,6 +228,7 @@ pub mod ERC20ReentrantMock { use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; use starknet::storage::{Vec, MutableVecTrait}; use starknet::syscalls::call_contract_syscall; + use starknet::SyscallResultTrait; use super::Type; component!(path: ERC20Component, storage: erc20, event: ERC20Event); @@ -259,7 +262,7 @@ pub mod ERC20ReentrantMock { // Hooks // - impl ERC20VotesHooksImpl of ERC20Component::ERC20HooksTrait { + impl ERC20ReentrantImpl of ERC20Component::ERC20HooksTrait { fn before_update( ref self: ERC20Component::ComponentState, from: ContractAddress, @@ -298,6 +301,7 @@ pub mod ERC20ReentrantMock { selector: felt252, calldata: Span ) { + self.reenter_type.write(when); self.reenter_target.write(target); self.reenter_selector.write(selector); for elem in calldata { @@ -315,13 +319,16 @@ pub mod ERC20ReentrantMock { .len() { calldata.append(self.reenter_calldata.at(i).read()); }; - - call_contract_syscall(target, selector, calldata.span()).unwrap(); + call_contract_syscall(target, selector, calldata.span()).unwrap_syscall(); } fn unsafe_mint(ref self: ContractState, recipient: ContractAddress, amount: u256) { self.erc20.mint(recipient, amount); } + + fn unsafe_burn(ref self: ContractState, account: ContractAddress, amount: u256) { + self.erc20.burn(account, amount); + } } #[constructor] From bded58ffdbddde5dd2384e51c1377544cbef9d1c Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 11 Oct 2024 02:06:10 -0500 Subject: [PATCH 40/90] tidy up tests --- .../src/tests/erc4626/test_erc4626.cairo | 77 ++++++++++--------- 1 file changed, 41 insertions(+), 36 deletions(-) diff --git a/packages/token/src/tests/erc4626/test_erc4626.cairo b/packages/token/src/tests/erc4626/test_erc4626.cairo index 049943d83..deb6a3553 100644 --- a/packages/token/src/tests/erc4626/test_erc4626.cairo +++ b/packages/token/src/tests/erc4626/test_erc4626.cairo @@ -35,6 +35,7 @@ fn VAULT_SYMBOL() -> ByteArray { } const DEFAULT_DECIMALS: u8 = 18; +const NO_OFFSET_DECIMALS: u8 = 0; const OFFSET_DECIMALS: u8 = 1; fn parse_token(token: u256) -> u256 { @@ -58,54 +59,67 @@ fn deploy_asset() -> IERC20ReentrantDispatcher { IERC20ReentrantDispatcher { contract_address } } -fn deploy_vault( - asset_address: ContractAddress, initial_supply: u256, recipient: ContractAddress -) -> ERC4626ABIDispatcher { +fn deploy_vault_no_offset(asset_address: ContractAddress) -> ERC4626ABIDispatcher { + let no_shares = 0_u256; + let mut vault_calldata: Array = array![]; vault_calldata.append_serde(VAULT_NAME()); vault_calldata.append_serde(VAULT_SYMBOL()); vault_calldata.append_serde(asset_address); - vault_calldata.append_serde(initial_supply); - vault_calldata.append_serde(recipient); + vault_calldata.append_serde(no_shares); + vault_calldata.append_serde(HOLDER()); let contract_address = utils::declare_and_deploy("ERC4626Mock", vault_calldata); ERC4626ABIDispatcher { contract_address } } -fn setup_initial_state() -> (IERC20ReentrantDispatcher, ERC4626ABIDispatcher) { - let mut asset = deploy_asset(); +fn deploy_vault_minted_shares( + asset_address: ContractAddress, shares: u256, recipient: ContractAddress +) -> ERC4626ABIDispatcher { + let mut vault_calldata: Array = array![]; + vault_calldata.append_serde(VAULT_NAME()); + vault_calldata.append_serde(VAULT_SYMBOL()); + vault_calldata.append_serde(asset_address); + vault_calldata.append_serde(shares); + vault_calldata.append_serde(recipient); - let no_amount = 0; - let recipient = HOLDER(); - let mut vault = deploy_vault(asset.contract_address, no_amount, recipient); - (asset, vault) + let contract_address = utils::declare_and_deploy("ERC4626OffsetMock", vault_calldata); + ERC4626ABIDispatcher { contract_address } } -// Further testing required for decimals once design is finalized -#[test] -fn test_offset_decimals() { - let (_, vault) = setup_initial_state(); - - let decimals = vault.decimals(); - assert_eq!(decimals, 19); +fn deploy_vault(asset_address: ContractAddress) -> ERC4626ABIDispatcher { + deploy_vault_minted_shares(asset_address, 0, HOLDER()) } // -// Initial state +// Metadata // #[test] fn test_metadata() { - let (asset, vault) = setup_initial_state(); + let asset = deploy_asset(); + let vault = deploy_vault_no_offset(asset.contract_address); + let name = vault.name(); + assert_eq!(name, VAULT_NAME()); + let symbol = vault.symbol(); + assert_eq!(symbol, VAULT_SYMBOL()); + let decimals = vault.decimals(); + assert_eq!(decimals, DEFAULT_DECIMALS + NO_OFFSET_DECIMALS); + let asset_address = vault.asset(); + assert_eq!(asset_address, asset.contract_address); +} - assert_eq!(name, VAULT_NAME()); - assert_eq!(symbol, VAULT_SYMBOL()); +#[test] +fn test_decimals_offset() { + let asset = deploy_asset(); + let vault = deploy_vault(asset.contract_address); + + let decimals = vault.decimals(); assert_eq!(decimals, DEFAULT_DECIMALS + OFFSET_DECIMALS); - assert_eq!(asset_address, asset.contract_address); } // @@ -114,10 +128,7 @@ fn test_metadata() { fn setup_empty() -> (IERC20ReentrantDispatcher, ERC4626ABIDispatcher) { let mut asset = deploy_asset(); - - let no_amount = 0; - let recipient = HOLDER(); - let mut vault = deploy_vault(asset.contract_address, no_amount, recipient); + let mut vault = deploy_vault(asset.contract_address); // Mint assets to HOLDER and approve vault asset.unsafe_mint(HOLDER(), Bounded::MAX / 2); // 50% of max @@ -262,10 +273,7 @@ fn test_redeem() { fn setup_inflation_attack() -> (IERC20ReentrantDispatcher, ERC4626ABIDispatcher) { let mut asset = deploy_asset(); - - let no_amount = 0; - let recipient = HOLDER(); - let mut vault = deploy_vault(asset.contract_address, no_amount, recipient); + let mut vault = deploy_vault(asset.contract_address); // Mint assets to HOLDER and approve vault asset.unsafe_mint(HOLDER(), Bounded::MAX / 2); // 50% of max @@ -449,7 +457,7 @@ fn setup_full_vault() -> (IERC20ReentrantDispatcher, ERC4626ABIDispatcher) { let recipient = HOLDER(); // Add 1 token of underlying asset and 100 shares to the vault - let mut vault = deploy_vault(asset.contract_address, shares, recipient); + let mut vault = deploy_vault_minted_shares(asset.contract_address, shares, recipient); asset.unsafe_mint(vault.contract_address, parse_token(1)); // Approve SPENDER @@ -785,10 +793,7 @@ fn test_full_vault_redeem_unauthorized() { fn setup_reentrancy() -> (IERC20ReentrantDispatcher, ERC4626ABIDispatcher) { let mut asset = deploy_asset(); - - let no_shares = 0; - let recipient = HOLDER(); - let mut vault = deploy_vault(asset.contract_address, no_shares, recipient); + let mut vault = deploy_vault(asset.contract_address); let value: u256 = 1_000_000_000_000_000_000; asset.unsafe_mint(HOLDER(), value); From fd4d1465a8435d9e0c486651efeb70a995d0142f Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 11 Oct 2024 02:06:35 -0500 Subject: [PATCH 41/90] add default decimals mock --- packages/test_common/src/mocks/erc4626.cairo | 59 ++++++++++++++++++++ packages/token/Scarb.toml | 1 + 2 files changed, 60 insertions(+) diff --git a/packages/test_common/src/mocks/erc4626.cairo b/packages/test_common/src/mocks/erc4626.cairo index 3a19ccc0e..c7c9d253c 100644 --- a/packages/test_common/src/mocks/erc4626.cairo +++ b/packages/test_common/src/mocks/erc4626.cairo @@ -1,5 +1,64 @@ #[starknet::contract] pub mod ERC4626Mock { + use openzeppelin_token::erc20::extensions::erc4626::ERC4626Component::InternalTrait as ERC4626InternalTrait; + use openzeppelin_token::erc20::extensions::erc4626::ERC4626Component; + use openzeppelin_token::erc20::extensions::erc4626::DefaultConfig; + use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use starknet::ContractAddress; + + component!(path: ERC4626Component, storage: erc4626, event: ERC4626Event); + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + + // ERC4626 + #[abi(embed_v0)] + impl ERC4626ComponentImpl = ERC4626Component::ERC4626Impl; + // ERC4626MetadataImpl is a custom impl of IERC20Metadata + #[abi(embed_v0)] + impl ERC4626MetadataImpl = ERC4626Component::ERC4626MetadataImpl; + + // ERC20 + #[abi(embed_v0)] + impl ERC20Impl = ERC20Component::ERC20Impl; + #[abi(embed_v0)] + impl ERC20CamelOnlyImpl = ERC20Component::ERC20CamelOnlyImpl; + + impl ERC4626InternalImpl = ERC4626Component::InternalImpl; + impl ERC20InternalImpl = ERC20Component::InternalImpl; + + #[storage] + pub struct Storage { + #[substorage(v0)] + pub erc4626: ERC4626Component::Storage, + #[substorage(v0)] + pub erc20: ERC20Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC4626Event: ERC4626Component::Event, + #[flat] + ERC20Event: ERC20Component::Event + } + + #[constructor] + fn constructor( + ref self: ContractState, + name: ByteArray, + symbol: ByteArray, + underlying_asset: ContractAddress, + initial_supply: u256, + recipient: ContractAddress + ) { + self.erc20.initializer(name, symbol); + self.erc20.mint(recipient, initial_supply); + self.erc4626.initializer(underlying_asset); + } +} + +#[starknet::contract] +pub mod ERC4626OffsetMock { use openzeppelin_token::erc20::extensions::erc4626::ERC4626Component::InternalTrait as ERC4626InternalTrait; use openzeppelin_token::erc20::extensions::erc4626::ERC4626Component; use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; diff --git a/packages/token/Scarb.toml b/packages/token/Scarb.toml index 6c7633be8..531c930b2 100644 --- a/packages/token/Scarb.toml +++ b/packages/token/Scarb.toml @@ -48,6 +48,7 @@ build-external-contracts = [ "openzeppelin_test_common::mocks::account::DualCaseAccountMock", "openzeppelin_test_common::mocks::erc20::ERC20ReentrantMock", "openzeppelin_test_common::mocks::erc4626::ERC4626Mock", + "openzeppelin_test_common::mocks::erc4626::ERC4626OffsetMock", "openzeppelin_test_common::mocks::erc721::DualCaseERC721ReceiverMock", "openzeppelin_test_common::mocks::erc1155::DualCaseERC1155ReceiverMock", "openzeppelin_test_common::mocks::non_implementing::NonImplementingMock", From 2bf3aac528e2df094898739b7a27d4df4569d5de Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 11 Oct 2024 02:09:36 -0500 Subject: [PATCH 42/90] fix deploy fn names --- .../src/tests/erc4626/test_erc4626.cairo | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/token/src/tests/erc4626/test_erc4626.cairo b/packages/token/src/tests/erc4626/test_erc4626.cairo index deb6a3553..f8f837f58 100644 --- a/packages/token/src/tests/erc4626/test_erc4626.cairo +++ b/packages/token/src/tests/erc4626/test_erc4626.cairo @@ -59,7 +59,7 @@ fn deploy_asset() -> IERC20ReentrantDispatcher { IERC20ReentrantDispatcher { contract_address } } -fn deploy_vault_no_offset(asset_address: ContractAddress) -> ERC4626ABIDispatcher { +fn deploy_vault(asset_address: ContractAddress) -> ERC4626ABIDispatcher { let no_shares = 0_u256; let mut vault_calldata: Array = array![]; @@ -73,7 +73,7 @@ fn deploy_vault_no_offset(asset_address: ContractAddress) -> ERC4626ABIDispatche ERC4626ABIDispatcher { contract_address } } -fn deploy_vault_minted_shares( +fn deploy_vault_offset_minted_shares( asset_address: ContractAddress, shares: u256, recipient: ContractAddress ) -> ERC4626ABIDispatcher { let mut vault_calldata: Array = array![]; @@ -87,8 +87,8 @@ fn deploy_vault_minted_shares( ERC4626ABIDispatcher { contract_address } } -fn deploy_vault(asset_address: ContractAddress) -> ERC4626ABIDispatcher { - deploy_vault_minted_shares(asset_address, 0, HOLDER()) +fn deploy_vault_offset(asset_address: ContractAddress) -> ERC4626ABIDispatcher { + deploy_vault_offset_minted_shares(asset_address, 0, HOLDER()) } // @@ -98,7 +98,7 @@ fn deploy_vault(asset_address: ContractAddress) -> ERC4626ABIDispatcher { #[test] fn test_metadata() { let asset = deploy_asset(); - let vault = deploy_vault_no_offset(asset.contract_address); + let vault = deploy_vault(asset.contract_address); let name = vault.name(); assert_eq!(name, VAULT_NAME()); @@ -116,7 +116,7 @@ fn test_metadata() { #[test] fn test_decimals_offset() { let asset = deploy_asset(); - let vault = deploy_vault(asset.contract_address); + let vault = deploy_vault_offset(asset.contract_address); let decimals = vault.decimals(); assert_eq!(decimals, DEFAULT_DECIMALS + OFFSET_DECIMALS); @@ -128,7 +128,7 @@ fn test_decimals_offset() { fn setup_empty() -> (IERC20ReentrantDispatcher, ERC4626ABIDispatcher) { let mut asset = deploy_asset(); - let mut vault = deploy_vault(asset.contract_address); + let mut vault = deploy_vault_offset(asset.contract_address); // Mint assets to HOLDER and approve vault asset.unsafe_mint(HOLDER(), Bounded::MAX / 2); // 50% of max @@ -273,7 +273,7 @@ fn test_redeem() { fn setup_inflation_attack() -> (IERC20ReentrantDispatcher, ERC4626ABIDispatcher) { let mut asset = deploy_asset(); - let mut vault = deploy_vault(asset.contract_address); + let mut vault = deploy_vault_offset(asset.contract_address); // Mint assets to HOLDER and approve vault asset.unsafe_mint(HOLDER(), Bounded::MAX / 2); // 50% of max @@ -457,7 +457,7 @@ fn setup_full_vault() -> (IERC20ReentrantDispatcher, ERC4626ABIDispatcher) { let recipient = HOLDER(); // Add 1 token of underlying asset and 100 shares to the vault - let mut vault = deploy_vault_minted_shares(asset.contract_address, shares, recipient); + let mut vault = deploy_vault_offset_minted_shares(asset.contract_address, shares, recipient); asset.unsafe_mint(vault.contract_address, parse_token(1)); // Approve SPENDER @@ -793,7 +793,7 @@ fn test_full_vault_redeem_unauthorized() { fn setup_reentrancy() -> (IERC20ReentrantDispatcher, ERC4626ABIDispatcher) { let mut asset = deploy_asset(); - let mut vault = deploy_vault(asset.contract_address); + let mut vault = deploy_vault_offset(asset.contract_address); let value: u256 = 1_000_000_000_000_000_000; asset.unsafe_mint(HOLDER(), value); From 9c7243f816cab72dfa71069a2454b88dbd529b49 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 11 Oct 2024 02:15:08 -0500 Subject: [PATCH 43/90] improve helper fn name --- .../src/tests/erc4626/test_erc4626.cairo | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/token/src/tests/erc4626/test_erc4626.cairo b/packages/token/src/tests/erc4626/test_erc4626.cairo index f8f837f58..c78ff3459 100644 --- a/packages/token/src/tests/erc4626/test_erc4626.cairo +++ b/packages/token/src/tests/erc4626/test_erc4626.cairo @@ -42,7 +42,7 @@ fn parse_token(token: u256) -> u256 { token * math::power(10, DEFAULT_DECIMALS.into()) } -fn parse_share(share: u256) -> u256 { +fn parse_share_offset(share: u256) -> u256 { share * math::power(10, DEFAULT_DECIMALS.into() + OFFSET_DECIMALS.into()) } @@ -157,7 +157,7 @@ fn test_deposit() { // Check preview == expected shares let preview_deposit = vault.preview_deposit(amount); - let exp_shares = parse_share(1); + let exp_shares = parse_share_offset(1); assert_eq!(preview_deposit, exp_shares); let holder_balance_before = asset.balance_of(HOLDER()); @@ -189,7 +189,7 @@ fn test_mint() { assert_eq!(max_mint, Bounded::MAX); // Check preview mint - let preview_mint = vault.preview_mint(parse_share(1)); + let preview_mint = vault.preview_mint(parse_share_offset(1)); let exp_assets = parse_token(1); assert_eq!(preview_mint, exp_assets); @@ -198,24 +198,24 @@ fn test_mint() { // Mint cheat_caller_address(vault.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); - vault.mint(parse_share(1), RECIPIENT()); + vault.mint(parse_share_offset(1), RECIPIENT()); // Check balances let holder_balance_after = asset.balance_of(HOLDER()); assert_eq!(holder_balance_after, holder_balance_before - parse_token(1)); let recipient_shares = vault.balance_of(RECIPIENT()); - assert_eq!(recipient_shares, parse_share(1)); + assert_eq!(recipient_shares, parse_share_offset(1)); // Check events spy .assert_event_transfer( asset.contract_address, HOLDER(), vault.contract_address, parse_token(1) ); - spy.assert_event_transfer(vault.contract_address, ZERO(), RECIPIENT(), parse_share(1)); + spy.assert_event_transfer(vault.contract_address, ZERO(), RECIPIENT(), parse_share_offset(1)); spy .assert_only_event_deposit( - vault.contract_address, HOLDER(), RECIPIENT(), parse_token(1), parse_share(1) + vault.contract_address, HOLDER(), RECIPIENT(), parse_token(1), parse_share_offset(1) ); } @@ -356,7 +356,7 @@ fn test_inflation_attack_mint() { let effective_assets = vault.total_assets() + virtual_assets; let effective_shares = vault.total_supply() + virtual_shares; - let mint_shares = parse_share(1); + let mint_shares = parse_share_offset(1); let expected_assets = (mint_shares * effective_assets) / effective_shares; // Check max mint @@ -379,7 +379,7 @@ fn test_inflation_attack_mint() { // Check balances assert_expected_assets(asset, HOLDER(), holder_balance_before - expected_assets); assert_expected_assets(asset, vault.contract_address, vault_balance_before + expected_assets); - assert_expected_shares(vault, RECIPIENT(), parse_share(1)); + assert_expected_shares(vault, RECIPIENT(), parse_share_offset(1)); // Check events spy @@ -453,7 +453,7 @@ fn test_inflation_attack_redeem() { fn setup_full_vault() -> (IERC20ReentrantDispatcher, ERC4626ABIDispatcher) { let mut asset = deploy_asset(); - let shares = parse_share(100); + let shares = parse_share_offset(100); let recipient = HOLDER(); // Add 1 token of underlying asset and 100 shares to the vault @@ -477,7 +477,7 @@ fn test_full_vault_status() { let (_, vault) = setup_full_vault(); let total_supply = vault.total_supply(); - assert_eq!(total_supply, parse_share(100)); + assert_eq!(total_supply, parse_share_offset(100)); let total_assets = vault.total_assets(); assert_eq!(total_assets, parse_token(1)); @@ -544,7 +544,7 @@ fn test_full_vault_mint() { let effective_assets = vault.total_assets() + virtual_assets; let effective_shares = vault.total_supply() + virtual_shares; - let mint_shares = parse_share(1); + let mint_shares = parse_share_offset(1); let expected_assets = (mint_shares * effective_assets) / effective_shares + 1; // add `1` for the rounding @@ -568,7 +568,7 @@ fn test_full_vault_mint() { // Check balances assert_expected_assets(asset, HOLDER(), holder_balance_before - expected_assets); assert_expected_assets(asset, vault.contract_address, vault_balance_before + expected_assets); - assert_expected_shares(vault, RECIPIENT(), parse_share(1)); + assert_expected_shares(vault, RECIPIENT(), parse_share_offset(1)); // Check events spy @@ -693,7 +693,7 @@ fn test_full_vault_redeem() { let effective_assets = vault.total_assets() + virtual_assets; let effective_shares = vault.total_supply() + virtual_shares; - let redeem_shares = parse_share(100); + let redeem_shares = parse_share_offset(100); let expected_assets = (redeem_shares * effective_assets) / effective_shares; // Check max redeem @@ -740,7 +740,7 @@ fn test_full_vault_redeem_with_approval() { let effective_assets = vault.total_assets() + virtual_assets; let effective_shares = vault.total_supply() + virtual_shares; - let redeem_shares = parse_share(100); + let redeem_shares = parse_share_offset(100); let expected_assets = (redeem_shares * effective_assets) / effective_shares; // Check max redeem @@ -780,7 +780,7 @@ fn test_full_vault_redeem_with_approval() { #[should_panic(expected: 'ERC20: insufficient allowance')] fn test_full_vault_redeem_unauthorized() { let (_, vault) = setup_full_vault(); - let redeem_shares = parse_share(100); + let redeem_shares = parse_share_offset(100); // Unauthorized redeem cheat_caller_address(vault.contract_address, OTHER(), CheatSpan::TargetCalls(1)); From bb3a6b04c66ae633cf25a21a7e8df248883208f7 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 11 Oct 2024 02:16:00 -0500 Subject: [PATCH 44/90] fix fmt --- packages/test_common/src/mocks/erc20.cairo | 2 +- packages/test_common/src/mocks/erc4626.cairo | 2 +- .../src/tests/erc4626/test_erc4626.cairo | 32 ++++++++++++++----- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/packages/test_common/src/mocks/erc20.cairo b/packages/test_common/src/mocks/erc20.cairo index dd3f7af87..6702269c3 100644 --- a/packages/test_common/src/mocks/erc20.cairo +++ b/packages/test_common/src/mocks/erc20.cairo @@ -225,10 +225,10 @@ pub trait IERC20Reentrant { pub mod ERC20ReentrantMock { use openzeppelin_token::erc20::ERC20Component; use starknet::ContractAddress; + use starknet::SyscallResultTrait; use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; use starknet::storage::{Vec, MutableVecTrait}; use starknet::syscalls::call_contract_syscall; - use starknet::SyscallResultTrait; use super::Type; component!(path: ERC20Component, storage: erc20, event: ERC20Event); diff --git a/packages/test_common/src/mocks/erc4626.cairo b/packages/test_common/src/mocks/erc4626.cairo index c7c9d253c..726e6bbc5 100644 --- a/packages/test_common/src/mocks/erc4626.cairo +++ b/packages/test_common/src/mocks/erc4626.cairo @@ -1,8 +1,8 @@ #[starknet::contract] pub mod ERC4626Mock { + use openzeppelin_token::erc20::extensions::erc4626::DefaultConfig; use openzeppelin_token::erc20::extensions::erc4626::ERC4626Component::InternalTrait as ERC4626InternalTrait; use openzeppelin_token::erc20::extensions::erc4626::ERC4626Component; - use openzeppelin_token::erc20::extensions::erc4626::DefaultConfig; use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; use starknet::ContractAddress; diff --git a/packages/token/src/tests/erc4626/test_erc4626.cairo b/packages/token/src/tests/erc4626/test_erc4626.cairo index c78ff3459..487a7d229 100644 --- a/packages/token/src/tests/erc4626/test_erc4626.cairo +++ b/packages/token/src/tests/erc4626/test_erc4626.cairo @@ -17,9 +17,7 @@ use openzeppelin_testing::constants::{NAME, SYMBOL, OTHER, RECIPIENT, ZERO, SPEN use openzeppelin_testing::events::EventSpyExt; use openzeppelin_utils::math; use openzeppelin_utils::serde::SerializedAppend; -use snforge_std::{ - cheat_caller_address, CheatSpan, spy_events, EventSpy -}; +use snforge_std::{cheat_caller_address, CheatSpan, spy_events, EventSpy}; use starknet::{ContractAddress, contract_address_const}; fn HOLDER() -> ContractAddress { @@ -841,13 +839,26 @@ fn test_share_price_with_reentrancy_before_deposit() { // Check events // Reentered events come first because they're called in mock ERC20 `before_update` hook - spy.assert_event_transfer(asset.contract_address, asset.contract_address, vault.contract_address, reenter_value); + spy + .assert_event_transfer( + asset.contract_address, asset.contract_address, vault.contract_address, reenter_value + ); spy.assert_event_transfer(vault.contract_address, ZERO(), HOLDER(), shares_for_reenter); - spy.assert_event_deposit(vault.contract_address, asset.contract_address, HOLDER(), reenter_value, shares_for_reenter); + spy + .assert_event_deposit( + vault.contract_address, + asset.contract_address, + HOLDER(), + reenter_value, + shares_for_reenter + ); spy.assert_event_transfer(asset.contract_address, HOLDER(), vault.contract_address, value); spy.assert_event_transfer(vault.contract_address, ZERO(), HOLDER(), shares_for_deposit); - spy.assert_only_event_deposit(vault.contract_address, HOLDER(), HOLDER(), value, shares_for_deposit); + spy + .assert_only_event_deposit( + vault.contract_address, HOLDER(), HOLDER(), value, shares_for_deposit + ); } #[test] @@ -894,7 +905,12 @@ fn test_share_price_with_reentrancy_after_withdraw() { // Reentrant withdraw event → uses same price spy .assert_event_withdraw( - vault.contract_address, asset.contract_address, HOLDER(), asset.contract_address, reenter_value, shares_for_reenter + vault.contract_address, + asset.contract_address, + HOLDER(), + asset.contract_address, + reenter_value, + shares_for_reenter ); } @@ -961,7 +977,7 @@ fn test_price_change_during_reentrancy_doesnt_affect_withdraw() { spy .assert_event_withdraw( vault.contract_address, HOLDER(), HOLDER(), HOLDER(), value, shares_before - ); + ); // Check that price is modified after reentrant tx let shares_after = vault.preview_withdraw(value); From c8a9dfaf63a7d70fd151f7520f4383caf17d698a Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 18 Oct 2024 20:28:01 -0500 Subject: [PATCH 45/90] add comments, remove unused dep --- packages/utils/src/math.cairo | 9 +++++---- packages/utils/src/tests/test_math.cairo | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/utils/src/math.cairo b/packages/utils/src/math.cairo index 7542a6da5..6f63e6e89 100644 --- a/packages/utils/src/math.cairo +++ b/packages/utils/src/math.cairo @@ -60,15 +60,16 @@ fn round_up(rounding: Rounding) -> bool { u8_rounding % 2 == 1 } -/// ADD MEEE +/// Returns the quotient of x * y / denominator and rounds up or down depending on `rounding`. +/// Uses `u512_safe_div_rem_by_u256` for precision. pub fn u256_mul_div(x: u256, y: u256, denominator: u256, rounding: Rounding) -> u256 { let (q, r) = _raw_u256_mul_div(x, y, denominator); - // Prepare vars for bitwise op - let felt_is_round_up: felt252 = round_up(rounding).into(); + // Cast to felts for bitwise op + let is_rounded_up: felt252 = round_up(rounding).into(); let has_remainder: felt252 = (r > 0).into(); - q + (felt_is_round_up.into() & has_remainder.into()) + q + (is_rounded_up.into() & has_remainder.into()) } fn _raw_u256_mul_div(x: u256, y: u256, denominator: u256) -> (u256, u256) { diff --git a/packages/utils/src/tests/test_math.cairo b/packages/utils/src/tests/test_math.cairo index dd70c7fde..d78a1b41d 100644 --- a/packages/utils/src/tests/test_math.cairo +++ b/packages/utils/src/tests/test_math.cairo @@ -1,6 +1,6 @@ use core::num::traits::Bounded; use crate::math::Rounding; -use crate::math::{u256_mul_div, power}; +use crate::math::u256_mul_div; #[test] #[should_panic(expected: 'Math: division by zero')] From 385118ed89ecf0ef39c5ccde61745606f287193d Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 18 Oct 2024 20:51:50 -0500 Subject: [PATCH 46/90] update branch --- packages/test_common/src/mocks/erc4626.cairo | 288 +++++++++++++++ packages/token/Scarb.toml | 2 + .../token/src/erc20/extensions/erc4626.cairo | 3 + .../erc20/extensions/erc4626/erc4626.cairo | 231 +++++++++++- .../erc20/extensions/erc4626/interface.cairo | 168 +++++++++ .../src/tests/erc4626/test_erc4626.cairo | 330 ++++++++++++++++++ 6 files changed, 1003 insertions(+), 19 deletions(-) diff --git a/packages/test_common/src/mocks/erc4626.cairo b/packages/test_common/src/mocks/erc4626.cairo index 726e6bbc5..e434f3adf 100644 --- a/packages/test_common/src/mocks/erc4626.cairo +++ b/packages/test_common/src/mocks/erc4626.cairo @@ -3,6 +3,9 @@ pub mod ERC4626Mock { use openzeppelin_token::erc20::extensions::erc4626::DefaultConfig; use openzeppelin_token::erc20::extensions::erc4626::ERC4626Component::InternalTrait as ERC4626InternalTrait; use openzeppelin_token::erc20::extensions::erc4626::ERC4626Component; + use openzeppelin_token::erc20::extensions::erc4626::ERC4626DefaultLimits; + use openzeppelin_token::erc20::extensions::erc4626::ERC4626DefaultNoFees; + use openzeppelin_token::erc20::extensions::erc4626::ERC4626HooksEmptyImpl; use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; use starknet::ContractAddress; @@ -61,6 +64,9 @@ pub mod ERC4626Mock { pub mod ERC4626OffsetMock { use openzeppelin_token::erc20::extensions::erc4626::ERC4626Component::InternalTrait as ERC4626InternalTrait; use openzeppelin_token::erc20::extensions::erc4626::ERC4626Component; + use openzeppelin_token::erc20::extensions::erc4626::ERC4626DefaultLimits; + use openzeppelin_token::erc20::extensions::erc4626::ERC4626DefaultNoFees; + use openzeppelin_token::erc20::extensions::erc4626::ERC4626HooksEmptyImpl; use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; use starknet::ContractAddress; @@ -119,3 +125,285 @@ pub mod ERC4626OffsetMock { self.erc4626.initializer(underlying_asset); } } + +#[starknet::contract] +pub mod ERC4626LimitsMock { + use openzeppelin_token::erc20::extensions::erc4626::ERC4626Component::InternalTrait as ERC4626InternalTrait; + use openzeppelin_token::erc20::extensions::erc4626::ERC4626Component; + use openzeppelin_token::erc20::extensions::erc4626::ERC4626DefaultNoFees; + use openzeppelin_token::erc20::extensions::erc4626::ERC4626HooksEmptyImpl; + use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use starknet::ContractAddress; + + component!(path: ERC4626Component, storage: erc4626, event: ERC4626Event); + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + + // ERC4626 + #[abi(embed_v0)] + impl ERC4626ComponentImpl = ERC4626Component::ERC4626Impl; + // ERC4626MetadataImpl is a custom impl of IERC20Metadata + #[abi(embed_v0)] + impl ERC4626MetadataImpl = ERC4626Component::ERC4626MetadataImpl; + + // ERC20 + #[abi(embed_v0)] + impl ERC20Impl = ERC20Component::ERC20Impl; + #[abi(embed_v0)] + impl ERC20CamelOnlyImpl = ERC20Component::ERC20CamelOnlyImpl; + + impl ERC4626InternalImpl = ERC4626Component::InternalImpl; + impl ERC20InternalImpl = ERC20Component::InternalImpl; + + #[storage] + pub struct Storage { + #[substorage(v0)] + pub erc4626: ERC4626Component::Storage, + #[substorage(v0)] + pub erc20: ERC20Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC4626Event: ERC4626Component::Event, + #[flat] + ERC20Event: ERC20Component::Event + } + + pub impl OffsetConfig of ERC4626Component::ImmutableConfig { + const UNDERLYING_DECIMALS: u8 = ERC4626Component::DEFAULT_UNDERLYING_DECIMALS; + const DECIMALS_OFFSET: u8 = 1; + } + + const MAX_DEPOSIT: u256 = 100_000_000_000_000_000_000; + const MAX_MINT: u256 = 100_000_000_000_000_000_000; + + impl ERC4626LimitsImpl of ERC4626Component::LimitConfigTrait { + fn deposit_limit( + self: @ERC4626Component::ComponentState, receiver: ContractAddress + ) -> Option:: { + Option::Some(MAX_DEPOSIT) + } + + fn mint_limit( + self: @ERC4626Component::ComponentState, receiver: ContractAddress + ) -> Option:: { + Option::Some(MAX_MINT) + } + } + + #[constructor] + fn constructor( + ref self: ContractState, + name: ByteArray, + symbol: ByteArray, + underlying_asset: ContractAddress, + initial_supply: u256, + recipient: ContractAddress + ) { + self.erc20.initializer(name, symbol); + self.erc20.mint(recipient, initial_supply); + self.erc4626.initializer(underlying_asset); + } +} + +#[starknet::contract] +pub mod ERC4626FeesMock { + use openzeppelin_token::erc20::extensions::erc4626::ERC4626Component::FeeConfigTrait; + use openzeppelin_token::erc20::extensions::erc4626::ERC4626Component::InternalTrait as ERC4626InternalTrait; + use openzeppelin_token::erc20::extensions::erc4626::ERC4626Component; + use openzeppelin_token::erc20::extensions::erc4626::ERC4626DefaultLimits; + use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use openzeppelin_utils::math::Rounding; + use openzeppelin_utils::math; + use openzeppelin_utils::serde::SerializedAppend; + use starknet::ContractAddress; + use starknet::SyscallResultTrait; + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + + component!(path: ERC4626Component, storage: erc4626, event: ERC4626Event); + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + + // ERC4626 + #[abi(embed_v0)] + impl ERC4626ComponentImpl = ERC4626Component::ERC4626Impl; + // ERC4626MetadataImpl is a custom impl of IERC20Metadata + #[abi(embed_v0)] + impl ERC4626MetadataImpl = ERC4626Component::ERC4626MetadataImpl; + + // ERC20 + #[abi(embed_v0)] + impl ERC20Impl = ERC20Component::ERC20Impl; + #[abi(embed_v0)] + impl ERC20CamelOnlyImpl = ERC20Component::ERC20CamelOnlyImpl; + + impl ERC4626InternalImpl = ERC4626Component::InternalImpl; + impl ERC20InternalImpl = ERC20Component::InternalImpl; + + #[storage] + pub struct Storage { + #[substorage(v0)] + pub erc4626: ERC4626Component::Storage, + #[substorage(v0)] + pub erc20: ERC20Component::Storage, + pub entry_fee_basis_point_value: u256, + pub entry_fee_recipient: ContractAddress, + pub exit_fee_basis_point_value: u256, + pub exit_fee_recipient: ContractAddress + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC4626Event: ERC4626Component::Event, + #[flat] + ERC20Event: ERC20Component::Event + } + + const _BASIS_POINT_SCALE: u256 = 10_000; + + /// Immutable config + impl OffsetConfig of ERC4626Component::ImmutableConfig { + const UNDERLYING_DECIMALS: u8 = ERC4626Component::DEFAULT_UNDERLYING_DECIMALS; + const DECIMALS_OFFSET: u8 = 0; + } + + /// Hooks + impl ERC4626HooksEmptyImpl of ERC4626Component::ERC4626HooksTrait { + fn after_deposit( + ref self: ERC4626Component::ComponentState, assets: u256, shares: u256 + ) { + let mut contract_state = ERC4626Component::HasComponent::get_contract_mut(ref self); + let entry_basis_points = contract_state.entry_fee_basis_point_value.read(); + let fee = contract_state.fee_on_total(assets, entry_basis_points); + let recipient = contract_state.entry_fee_recipient.read(); + + if (fee > 0 && recipient != starknet::get_contract_address()) { + contract_state.transfer_fees(recipient, fee); + } + } + + fn before_withdraw( + ref self: ERC4626Component::ComponentState, assets: u256, shares: u256 + ) { + let mut contract_state = ERC4626Component::HasComponent::get_contract_mut(ref self); + let exit_basis_points = contract_state.exit_fee_basis_point_value.read(); + let fee = contract_state.fee_on_raw(assets, exit_basis_points); + let recipient = contract_state.exit_fee_recipient.read(); + + if (fee > 0 && recipient != starknet::get_contract_address()) { + contract_state.transfer_fees(recipient, fee); + } + } + } + + /// Adjust fees + impl AdjustFeesImpl of FeeConfigTrait { + fn adjust_deposit( + self: @ERC4626Component::ComponentState, assets: u256 + ) -> u256 { + let contract_state = ERC4626Component::HasComponent::get_contract(self); + contract_state.remove_fee_from_deposit(assets) + } + + fn adjust_mint( + self: @ERC4626Component::ComponentState, shares: u256 + ) -> u256 { + let contract_state = ERC4626Component::HasComponent::get_contract(self); + contract_state.add_fee_to_mint(shares) + } + + fn adjust_withdraw( + self: @ERC4626Component::ComponentState, assets: u256 + ) -> u256 { + let contract_state = ERC4626Component::HasComponent::get_contract(self); + contract_state.add_fee_to_withdraw(assets) + } + + fn adjust_redeem( + self: @ERC4626Component::ComponentState, shares: u256 + ) -> u256 { + let contract_state = ERC4626Component::HasComponent::get_contract(self); + contract_state.remove_fee_from_redeem(shares) + } + } + + #[constructor] + fn constructor( + ref self: ContractState, + name: ByteArray, + symbol: ByteArray, + underlying_asset: ContractAddress, + initial_supply: u256, + recipient: ContractAddress, + entry_fee: u256, + entry_treasury: ContractAddress, + exit_fee: u256, + exit_treasury: ContractAddress + ) { + self.erc20.initializer(name, symbol); + self.erc20.mint(recipient, initial_supply); + self.erc4626.initializer(underlying_asset); + + self.entry_fee_basis_point_value.write(entry_fee); + self.entry_fee_recipient.write(entry_treasury); + self.exit_fee_basis_point_value.write(exit_fee); + self.exit_fee_recipient.write(exit_treasury); + } + + #[generate_trait] + pub impl InternalImpl of InternalTrait { + fn transfer_fees(ref self: ContractState, recipient: ContractAddress, fee: u256) { + let asset_addr = self.asset(); + let selector = selector!("transfer"); + let mut calldata: Array = array![]; + calldata.append_serde(recipient); + calldata.append_serde(fee); + + let ret = starknet::syscalls::call_contract_syscall( + asset_addr, selector, calldata.span() + ) + .unwrap_syscall(); + assert_eq!(*ret.at(0), 1); // true + } + + fn remove_fee_from_deposit(self: @ContractState, assets: u256) -> u256 { + let fee = self.fee_on_total(assets, self.entry_fee_basis_point_value.read()); + assets - fee + } + + fn add_fee_to_mint(self: @ContractState, assets: u256) -> u256 { + assets + self.fee_on_raw(assets, self.entry_fee_basis_point_value.read()) + } + + fn add_fee_to_withdraw(self: @ContractState, assets: u256) -> u256 { + let fee = self.fee_on_raw(assets, self.exit_fee_basis_point_value.read()); + assets + fee + } + + fn remove_fee_from_redeem(self: @ContractState, assets: u256) -> u256 { + assets - self.fee_on_total(assets, self.exit_fee_basis_point_value.read()) + } + + /// + /// Fee operations + /// + + /// Calculates the fees that should be added to an amount `assets` that does not already + /// include fees. + /// Used in IERC4626::mint and IERC4626::withdraw operations. + fn fee_on_raw(self: @ContractState, assets: u256, fee_basis_points: u256) -> u256 { + math::u256_mul_div(assets, fee_basis_points, _BASIS_POINT_SCALE, Rounding::Ceil) + } + + /// Calculates the fee part of an amount `assets` that already includes fees. + /// Used in IERC4626::deposit and IERC4626::redeem operations. + fn fee_on_total(self: @ContractState, assets: u256, fee_basis_points: u256) -> u256 { + math::u256_mul_div( + assets, fee_basis_points, fee_basis_points + _BASIS_POINT_SCALE, Rounding::Ceil + ) + } + } +} diff --git a/packages/token/Scarb.toml b/packages/token/Scarb.toml index 531c930b2..a0e85e537 100644 --- a/packages/token/Scarb.toml +++ b/packages/token/Scarb.toml @@ -49,6 +49,8 @@ build-external-contracts = [ "openzeppelin_test_common::mocks::erc20::ERC20ReentrantMock", "openzeppelin_test_common::mocks::erc4626::ERC4626Mock", "openzeppelin_test_common::mocks::erc4626::ERC4626OffsetMock", + "openzeppelin_test_common::mocks::erc4626::ERC4626FeesMock", + "openzeppelin_test_common::mocks::erc4626::ERC4626LimitsMock", "openzeppelin_test_common::mocks::erc721::DualCaseERC721ReceiverMock", "openzeppelin_test_common::mocks::erc1155::DualCaseERC1155ReceiverMock", "openzeppelin_test_common::mocks::non_implementing::NonImplementingMock", diff --git a/packages/token/src/erc20/extensions/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626.cairo index 804a3c777..c0b2bc208 100644 --- a/packages/token/src/erc20/extensions/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626.cairo @@ -3,4 +3,7 @@ pub mod interface; pub use erc4626::DefaultConfig; pub use erc4626::ERC4626Component; +pub use erc4626::ERC4626DefaultLimits; +pub use erc4626::ERC4626DefaultNoFees; +pub use erc4626::ERC4626HooksEmptyImpl; pub use interface::IERC4626; diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo index 7054c8477..a59819c03 100644 --- a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -3,7 +3,7 @@ /// # ERC4626 Component /// -/// ADD MEEEEEEEEEEEEEEEEE AHHHH +/// ADD ME #[starknet::component] pub mod ERC4626Component { use core::num::traits::{Bounded, Zero}; @@ -17,7 +17,7 @@ pub mod ERC4626Component { use starknet::ContractAddress; use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; - // The defualt values are only used when the DefaultConfig + // The default values are only used when the DefaultConfig // is in scope in the implementing contract. pub const DEFAULT_UNDERLYING_DECIMALS: u8 = 18; pub const DEFAULT_DECIMALS_OFFSET: u8 = 0; @@ -82,40 +82,123 @@ pub mod ERC4626Component { } } + /// Adjustments for fees expected to be defined on the contract level. + /// Defaults to no entry or exit fees. + /// To transfer fees, this trait needs to be coordinated with ERC4626Component::ERC4626Hooks. + pub trait FeeConfigTrait { + fn adjust_deposit(self: @ComponentState, assets: u256) -> u256 { + assets + } + + fn adjust_mint(self: @ComponentState, shares: u256) -> u256 { + shares + } + + fn adjust_withdraw(self: @ComponentState, assets: u256) -> u256 { + assets + } + + fn adjust_redeem(self: @ComponentState, shares: u256) -> u256 { + shares + } + } + + /// Sets custom limits to the target exchange type and is expected to be defined at the contract + /// level. + pub trait LimitConfigTrait { + fn deposit_limit( + self: @ComponentState, receiver: ContractAddress + ) -> Option:: { + Option::None + } + + fn mint_limit( + self: @ComponentState, receiver: ContractAddress + ) -> Option:: { + Option::None + } + + fn withdraw_limit( + self: @ComponentState, owner: ContractAddress + ) -> Option:: { + Option::None + } + + fn redeem_limit( + self: @ComponentState, owner: ContractAddress + ) -> Option:: { + Option::None + } + } + + /// Allows contracts to hook logic into deposit and withdraw transactions. + /// This is where contracts can transfer fees. + pub trait ERC4626HooksTrait { + fn before_withdraw(ref self: ComponentState, assets: u256, shares: u256) {} + fn after_deposit(ref self: ComponentState, assets: u256, shares: u256) {} + } + #[embeddable_as(ERC4626Impl)] impl ERC4626< TContractState, +HasComponent, + impl Fee: FeeConfigTrait, + impl Limit: LimitConfigTrait, + impl Hooks: ERC4626HooksTrait, impl Immutable: ImmutableConfig, impl ERC20: ERC20Component::HasComponent, +ERC20Component::ERC20HooksTrait, +Drop > of IERC4626> { + /// Returns the address of the underlying token used for the Vault for accounting, + /// depositing, and withdrawing. fn asset(self: @ComponentState) -> ContractAddress { self.ERC4626_asset.read() } + /// Returns the total amount of the underlying asset that is “managed” by Vault. fn total_assets(self: @ComponentState) -> u256 { let this = starknet::get_contract_address(); IERC20Dispatcher { contract_address: self.ERC4626_asset.read() }.balance_of(this) } + /// Returns the amount of shares that the Vault would exchange for the amount of assets + /// provided, in an ideal scenario where all the conditions are met. fn convert_to_shares(self: @ComponentState, assets: u256) -> u256 { self._convert_to_shares(assets, Rounding::Floor) } + /// Returns the amount of assets that the Vault would exchange for the amount of shares + /// provided, in an ideal scenario where all the conditions are met. fn convert_to_assets(self: @ComponentState, shares: u256) -> u256 { self._convert_to_assets(shares, Rounding::Floor) } + /// Returns the maximum amount of the underlying asset that can be deposited into the Vault + /// for the receiver, through a deposit call. + /// If the `LimitConfigTrait` is not defined for deposits, returns 2 ** 256 - 1. fn max_deposit(self: @ComponentState, receiver: ContractAddress) -> u256 { - Bounded::MAX + match Limit::deposit_limit(self, receiver) { + Option::Some(limit) => limit, + Option::None => Bounded::MAX + } } + /// Allows an on-chain or off-chain user to simulate the effects of their deposit at the + /// current block, given current on-chain conditions. + /// If the `FeeConfigTrait` is not defined for deposits, returns the full amount of shares. fn preview_deposit(self: @ComponentState, assets: u256) -> u256 { - self._convert_to_shares(assets, Rounding::Floor) + let adjusted_assets = Fee::adjust_deposit(self, assets); + self._convert_to_shares(adjusted_assets, Rounding::Floor) } + /// Mints Vault shares to `receiver` by depositing exactly `assets` of underlying tokens. + /// Returns the amount of newly-minted shares. + /// + /// Requirements: + /// - `assets` is less than or equal to the max deposit amount for `receiver`. + /// + /// Emits a `Deposit` event. fn deposit( ref self: ComponentState, assets: u256, receiver: ContractAddress ) -> u256 { @@ -125,17 +208,35 @@ pub mod ERC4626Component { let shares = self.preview_deposit(assets); let caller = starknet::get_caller_address(); self._deposit(caller, receiver, assets, shares); + shares } + /// Returns the maximum amount of the Vault shares that can be minted for `receiver` through + /// a `mint` call. + /// If the `LimitConfigTrait` is not defined for mints, returns 2 ** 256 - 1. fn max_mint(self: @ComponentState, receiver: ContractAddress) -> u256 { - Bounded::MAX + match Limit::mint_limit(self, receiver) { + Option::Some(limit) => limit, + Option::None => Bounded::MAX + } } + /// Allows an on-chain or off-chain user to simulate the effects of their mint at the + /// current block, given current on-chain conditions. + /// If the `FeeConfigTrait` is not defined for mints, returns the full amount of assets. fn preview_mint(self: @ComponentState, shares: u256) -> u256 { - self._convert_to_assets(shares, Rounding::Ceil) + let raw_amount = self._convert_to_assets(shares, Rounding::Ceil); + Fee::adjust_mint(self, raw_amount) } + /// Mints exactly Vault `shares` to `receiver` by depositing amount of underlying tokens. + /// Returns the amount deposited assets. + /// + /// Requirements: + /// - `shares` is less than or equal to the max shares amount for `receiver`. + /// + /// Emits a `Deposit` event. fn mint( ref self: ComponentState, shares: u256, receiver: ContractAddress ) -> u256 { @@ -145,19 +246,39 @@ pub mod ERC4626Component { let assets = self.preview_mint(shares); let caller = starknet::get_caller_address(); self._deposit(caller, receiver, assets, shares); + assets } + /// Returns the maximum amount of the underlying asset that can be withdrawn from the owner + /// balance in the Vault, through a `withdraw` call. + /// If the `LimitConfigTrait` is not defined for withdraws, returns the full balance of + /// assets for `owner` (converted to shares). fn max_withdraw(self: @ComponentState, owner: ContractAddress) -> u256 { - let erc20_component = get_dep_component!(self, ERC20); - let owner_bal = erc20_component.balance_of(owner); - self._convert_to_assets(owner_bal, Rounding::Floor) + match Limit::withdraw_limit(self, owner) { + Option::Some(limit) => limit, + Option::None => { + let erc20_component = get_dep_component!(self, ERC20); + let owner_bal = erc20_component.balance_of(owner); + self._convert_to_assets(owner_bal, Rounding::Floor) + } + } } + /// Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the + /// current block, given current on-chain conditions. + /// If the `FeeConfigTrait` is not defined for withdraws, returns the full amount of shares. fn preview_withdraw(self: @ComponentState, assets: u256) -> u256 { - self._convert_to_shares(assets, Rounding::Ceil) + let adjusted_assets = Fee::adjust_withdraw(self, assets); + self._convert_to_shares(adjusted_assets, Rounding::Ceil) } + /// Burns shares from `owner` and sends exactly `assets` of underlying tokens to `receiver`. + /// + /// Requirements: + /// - `assets` is less than or equal to the max withdraw amount of `owner`. + /// + /// Emits a `Withdraw` event. fn withdraw( ref self: ComponentState, assets: u256, @@ -174,15 +295,34 @@ pub mod ERC4626Component { shares } + /// Returns the maximum amount of Vault shares that can be redeemed from the owner balance + /// in the Vault, through a `redeem` call. + /// If the `LimitConfigTrait` is not defined for redeems, returns the full balance of assets + /// for `owner`. fn max_redeem(self: @ComponentState, owner: ContractAddress) -> u256 { - let erc20_component = get_dep_component!(self, ERC20); - erc20_component.balance_of(owner) + match Limit::redeem_limit(self, owner) { + Option::Some(limit) => limit, + Option::None => { + let erc20_component = get_dep_component!(self, ERC20); + erc20_component.balance_of(owner) + } + } } + /// Allows an on-chain or off-chain user to simulate the effects of their redeemption at the + /// current block, given current on-chain conditions. + /// If the `FeeConfigTrait` is not defined for redeems, returns the full amount of assets. fn preview_redeem(self: @ComponentState, shares: u256) -> u256 { - self._convert_to_assets(shares, Rounding::Floor) + let raw_amount = self._convert_to_assets(shares, Rounding::Floor); + Fee::adjust_redeem(self, raw_amount) } + /// Burns exactly `shares` from `owner` and sends assets of underlying tokens to `receiver`. + /// + /// Requirements: + /// - `shares` is less than or equal to the max redeem amount of `owner`. + /// + /// Emits a `Withdraw` event. fn redeem( ref self: ComponentState, shares: u256, @@ -219,7 +359,9 @@ pub mod ERC4626Component { erc20_component.ERC20_symbol.read() } - /// Returns the number of decimals used to get its user representation. + /// Returns the cumulative number of decimals which includes both the underlying and offset + /// decimals. + /// Both of which must be defined in the `ImmutableConfig` inside the implementing contract. fn decimals(self: @ComponentState) -> u8 { Immutable::UNDERLYING_DECIMALS + Immutable::DECIMALS_OFFSET } @@ -229,17 +371,36 @@ pub mod ERC4626Component { pub impl InternalImpl< TContractState, +HasComponent, + impl Hooks: ERC4626HooksTrait, impl Immutable: ImmutableConfig, impl ERC20: ERC20Component::HasComponent, + +FeeConfigTrait, + +LimitConfigTrait, +ERC20Component::ERC20HooksTrait, +Drop > of InternalTrait { + /// Validates the `ImmutableConfig` constants and sets the `asset_address` to the vault. + /// This should be set in the contract's constructor. + /// + /// Requirements: + /// - `asset_address` cannot be the zero address. fn initializer(ref self: ComponentState, asset_address: ContractAddress) { ImmutableConfig::validate(); assert(!asset_address.is_zero(), Errors::INVALID_ASSET_ADDRESS); self.ERC4626_asset.write(asset_address); } + /// Business logic for `deposit` and `mint`. + /// Transfers `assets` from `caller` to the Vault contract then mints `shares` to + /// `receiver`. + /// Fees can be transferred in the `ERC4626Hooks::after_deposit` hook which is executed + /// after the business logic. + /// + /// Requirements: + /// - `ERC20::transfer_from` must return true. + /// + /// Emits two `ERC20::Transfer` events (`ERC20::mint` and `ERC20::transfer_from`). + /// Emits a `Deposit` event. fn _deposit( ref self: ComponentState, caller: ContractAddress, @@ -258,8 +419,21 @@ pub mod ERC4626Component { let mut erc20_component = get_dep_component_mut!(ref self, ERC20); erc20_component.mint(receiver, shares); self.emit(Deposit { sender: caller, owner: receiver, assets, shares }); + + // After deposit hook + Hooks::after_deposit(ref self, assets, shares); } + /// Business logic for `withdraw` and `redeem`. + /// Burns `shares` from `owner` and then transfers `assets` to `receiver`. + /// Fees can be transferred in the `ERC4626Hooks::before_withdraw` hook which is executed + /// before the business logic. + /// + /// Requirements: + /// - `ERC20::transfer` must return true. + /// + /// Emits two `ERC20::Transfer` events (`ERC20::burn` and `ERC20::transfer`). + /// Emits a `Withdraw` event. fn _withdraw( ref self: ComponentState, caller: ContractAddress, @@ -268,6 +442,9 @@ pub mod ERC4626Component { assets: u256, shares: u256 ) { + // Before withdraw hook + Hooks::before_withdraw(ref self, assets, shares); + // Burn shares first let mut erc20_component = get_dep_component_mut!(ref self, ERC20); if (caller != owner) { @@ -282,6 +459,8 @@ pub mod ERC4626Component { self.emit(Withdraw { sender: caller, receiver, owner, assets, shares }); } + /// Internal conversion function (from assets to shares) with support for `rounding` + /// direction. fn _convert_to_shares( self: @ComponentState, assets: u256, rounding: Rounding ) -> u256 { @@ -296,6 +475,8 @@ pub mod ERC4626Component { ) } + /// Internal conversion function (from shares to assets) with support for `rounding` + /// direction. fn _convert_to_assets( self: @ComponentState, shares: u256, rounding: Rounding ) -> u256 { @@ -312,7 +493,19 @@ pub mod ERC4626Component { } } -/// Implementation of the default ERC2981Component ImmutableConfig. +/// +/// Default (empty) traits +/// + +pub impl ERC4626HooksEmptyImpl< + TContractState +> of ERC4626Component::ERC4626HooksTrait {} +pub impl ERC4626DefaultNoFees of ERC4626Component::FeeConfigTrait {} +pub impl ERC4626DefaultLimits< + TContractState +> of ERC4626Component::LimitConfigTrait {} + +/// Implementation of the default `ERC4626Component::ImmutableConfig`. /// /// See /// https://github.com/starknet-io/SNIPs/blob/963848f0752bde75c7087c2446d83b7da8118b25/SNIPS/snip-107.md#defaultconfig-implementation @@ -327,9 +520,10 @@ pub impl DefaultConfig of ERC4626Component::ImmutableConfig { #[cfg(test)] mod Test { use openzeppelin_test_common::mocks::erc4626::ERC4626Mock; - use starknet::contract_address_const; use super::ERC4626Component::InternalImpl; use super::ERC4626Component; + use super::ERC4626DefaultLimits; + use super::ERC4626DefaultNoFees; type ComponentState = ERC4626Component::ComponentState; @@ -337,7 +531,7 @@ mod Test { ERC4626Component::component_state_for_testing() } - // Invalid fee denominator + // Invalid decimals impl InvalidImmutableConfig of ERC4626Component::ImmutableConfig { const UNDERLYING_DECIMALS: u8 = 255; const DECIMALS_OFFSET: u8 = 1; @@ -347,9 +541,8 @@ mod Test { #[should_panic(expected: 'ERC4626: decimals overflow')] fn test_initializer_invalid_config_panics() { let mut state = COMPONENT_STATE(); - let asset = contract_address_const::<'ASSET'>(); + let asset = starknet::contract_address_const::<'ASSET'>(); state.initializer(asset); } } - diff --git a/packages/token/src/erc20/extensions/erc4626/interface.cairo b/packages/token/src/erc20/extensions/erc4626/interface.cairo index 3eb4adcc3..2eb9e3b0b 100644 --- a/packages/token/src/erc20/extensions/erc4626/interface.cairo +++ b/packages/token/src/erc20/extensions/erc4626/interface.cairo @@ -5,23 +5,191 @@ use starknet::ContractAddress; #[starknet::interface] pub trait IERC4626 { + /// Returns the address of the underlying token used for the Vault for accounting, depositing, + /// and withdrawing. + /// + /// MUST be an ERC20 token contract. + /// MUST NOT panic. fn asset(self: @TState) -> ContractAddress; + /// Returns the total amount of the underlying asset that is “managed” by Vault. + /// + /// SHOULD include any compounding that occurs from yield. + /// MUST be inclusive of any fees that are charged against assets in the Vault. + /// MUST NOT panic. fn total_assets(self: @TState) -> u256; + /// Returns the amount of shares that the Vault would exchange for the amount of assets + /// provided, in an ideal scenario where all the conditions are met. + /// + /// MUST NOT be inclusive of any fees that are charged against assets in the Vault. + /// MUST NOT show any variations depending on the caller. + /// MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange. + /// MUST NOT panic. fn convert_to_shares(self: @TState, assets: u256) -> u256; + /// Returns the amount of assets that the Vault would exchange for the amount of shares + /// provided, in an ideal scenario where all the conditions are met. + /// + /// MUST NOT be inclusive of any fees that are charged against assets in the Vault. + /// MUST NOT show any variations depending on the caller. + /// MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange. + /// MUST NOT panic. + /// + /// Note that this calculation MAY NOT reflect the “per-user” price-per-share, and instead + /// should reflect the “average-user’s” price-per-share, meaning what the average user + /// should expect to see when exchanging to and from. fn convert_to_assets(self: @TState, shares: u256) -> u256; + /// Returns the maximum amount of the underlying asset that can be deposited into the Vault for + /// `receiver`, through a deposit call. + /// + /// MUST return a limited value if receiver is subject to some deposit limit. + /// MUST return 2 ** 256 - 1 if there is no limit on the maximum amount of assets that may be + /// deposited. + /// MUST NOT panic. + /// + /// Note that this calculation MAY NOT reflect the “per-user” price-per-share, and instead + /// should reflect the “average-user’s” price-per-share, meaning what the average user + /// should expect to see when exchanging to and from. fn max_deposit(self: @TState, receiver: ContractAddress) -> u256; + /// Allows an on-chain or off-chain user to simulate the effects of their deposit at the current + /// block, given current on-chain conditions. + /// + /// MUST return as close to and no more than the exact amount of Vault shares that would be + /// minted in a deposit call in the same transaction i.e. deposit should return the same or more + /// shares as `preview_deposit` if called in the same transaction. + /// MUST NOT account for deposit limits like those returned from `max_deposit` and should always + /// act as though the deposit would be accepted, regardless if the user has enough tokens + /// approved, etc. + /// MUST be inclusive of deposit fees. Integrators should be aware of the existence of deposit + /// fees. + /// MUST NOT panic. + /// + /// Note that any unfavorable discrepancy between `convert_to_shares` and `preview_deposit` + /// SHOULD be considered slippage in share price or some other type of condition, meaning the + /// depositor will lose assets by depositing. fn preview_deposit(self: @TState, assets: u256) -> u256; + /// Mints Vault shares to `receiver` by depositing exactly amount of `assets`. + /// + /// MUST emit the Deposit event. + /// MAY support an additional flow in which the underlying tokens are owned by the Vault + /// contract before the deposit execution, and are accounted for during deposit. + /// MUST panic if all of assets cannot be deposited (due to deposit limit being reached, + /// slippage, the user not approving enough underlying tokens to the Vault contract, etc). + /// + /// Note that most implementations will require pre-approval of the Vault with the Vault’s + /// underlying asset token. fn deposit(ref self: TState, assets: u256, receiver: ContractAddress) -> u256; + /// Returns the maximum amount of the Vault shares that can be minted for the receiver, through + /// a mint call. + /// + /// MUST return a limited value if receiver is subject to some mint limit. + /// MUST return 2 ** 256 - 1 if there is no limit on the maximum amount of shares that may be + /// minted. + /// MUST NOT panic. fn max_mint(self: @TState, receiver: ContractAddress) -> u256; + /// Allows an on-chain or off-chain user to simulate the effects of their mint at the current + /// block, given current on-chain conditions. + /// + /// MUST return as close to and no fewer than the exact amount of assets that would be deposited + /// in a `mint` call in the same transaction. I.e. `mint` should return the same or fewer assets + /// as `preview_mint` if called in the same transaction. + /// MUST NOT account for mint limits like those returned from `max_mint` and should always act + /// as though the mint would be accepted, regardless if the user has enough tokens approved, + /// etc. + /// MUST be inclusive of deposit fees. Integrators should be aware of the existence of deposit + /// fees. + /// MUST NOT panic. + /// + /// NOTE: Any unfavorable discrepancy between convertToAssets and previewMint SHOULD be + /// considered slippage in share price or some other type of condition, meaning the depositor + /// will lose assets by minting. + /// + /// Note that any unfavorable discrepancy between `convert_to_assets` and `preview_mint` SHOULD + /// be considered slippage in share price or some other type of condition, meaning the depositor + /// will lose assets by minting. fn preview_mint(self: @TState, shares: u256) -> u256; + /// Mints exactly shares Vault shares to receiver by depositing amount of underlying tokens. + /// + /// MUST emit the `Deposit` event. + /// MAY support an additional flow in which the underlying tokens are owned by the Vault + /// contract before the mint execution, and are accounted for during mint. + /// MUST panic if all of shares cannot be minted (due to deposit limit being reached, slippage, + /// the user not approving enough underlying tokens to the Vault contract, etc). + /// + /// Note that most implementations will require pre-approval of the Vault with the Vault’s + /// underlying asset token. fn mint(ref self: TState, shares: u256, receiver: ContractAddress) -> u256; + /// Returns the maximum amount of the underlying asset that can be withdrawn from the owner + /// balance in the Vault, through a withdraw call. + /// + /// MUST return a limited value if owner is subject to some withdrawal limit or timelock. + /// MUST NOT panic. fn max_withdraw(self: @TState, owner: ContractAddress) -> u256; + /// Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the + /// current block, given current on-chain conditions. + /// + /// MUST return as close to and no fewer than the exact amount of Vault shares that would be + /// burned in a withdraw call in the same transaction i.e. withdraw should return the same or + /// fewer shares as preview_withdraw if called in the same transaction. + /// MUST NOT account for withdrawal limits like those returned from max_withdraw and should + /// always act as though the withdrawal would be accepted, regardless if the user has enough + /// shares, etc. + /// MUST be inclusive of withdrawal fees. Integrators should be aware of the existence of + /// withdrawal fees. + /// MUST not panic. + /// + /// Note that any unfavorable discrepancy between `convert_to_shares` and `preview_withdraw` + /// SHOULD be considered slippage in share price or some other type of condition, meaning the + /// depositor will lose assets by depositing. fn preview_withdraw(self: @TState, assets: u256) -> u256; + /// Burns shares from owner and sends exactly assets of underlying tokens to receiver. + /// + /// MUST emit the `Withdraw` event. + /// MAY support an additional flow in which the underlying tokens are owned by the Vault + /// contract before the withdraw execution, and are accounted for during withdraw. + /// MUST revert if all of assets cannot be withdrawn (due to withdrawal limit being reached, + /// slippage, the owner not having enough shares, etc). + /// + /// Note that some implementations will require pre-requesting to the Vault before a withdrawal + /// may be performed. + /// Those methods should be performed separately. fn withdraw( ref self: TState, assets: u256, receiver: ContractAddress, owner: ContractAddress ) -> u256; + /// Returns the maximum amount of Vault shares that can be redeemed from the owner balance in + /// the Vault, through a redeem call. + /// + /// MUST return a limited value if owner is subject to some withdrawal limit or timelock. + /// MUST return `ERC20::balance_of(owner)` if `owner` is not subject to any withdrawal limit or + /// timelock. + /// MUST NOT panic. fn max_redeem(self: @TState, owner: ContractAddress) -> u256; + /// Allows an on-chain or off-chain user to simulate the effects of their redeemption at the + /// current block, given current on-chain conditions. + /// + /// MUST return as close to and no more than the exact amount of assets that would be withdrawn + /// in a redeem call in the same transaction i.e. redeem should return the same or more assets + /// as preview_redeem if called in the same transaction. + /// MUST NOT account for redemption limits like those returned from max_redeem and should always + /// act as though the redemption would be accepted, regardless if the user has enough shares, + /// etc. + /// MUST be inclusive of withdrawal fees. Integrators should be aware of the existence of + /// withdrawal fees. + /// MUST NOT panic. + /// + /// Note any unfavorable discrepancy between `convert_to_assets` and `preview_redeem` SHOULD be + /// considered slippage in share price or some other type of condition, meaning the depositor + /// will lose assets by redeeming. fn preview_redeem(self: @TState, shares: u256) -> u256; + /// Burns exactly shares from owner and sends assets of underlying tokens to receiver. + /// + /// MUST emit the `Withdraw` event. + /// MAY support an additional flow in which the underlying tokens are owned by the Vault + /// contract before the redeem execution, and are accounted for during redeem. + /// MUST revert if all of shares cannot be redeemed (due to withdrawal limit being reached, + /// slippage, the owner not having enough shares, etc). + /// + /// Note some implementations will require pre-requesting to the Vault before a withdrawal may + /// be performed. + /// Those methods should be performed separately. fn redeem( ref self: TState, shares: u256, receiver: ContractAddress, owner: ContractAddress ) -> u256; diff --git a/packages/token/src/tests/erc4626/test_erc4626.cairo b/packages/token/src/tests/erc4626/test_erc4626.cairo index 487a7d229..44e127662 100644 --- a/packages/token/src/tests/erc4626/test_erc4626.cairo +++ b/packages/token/src/tests/erc4626/test_erc4626.cairo @@ -24,6 +24,10 @@ fn HOLDER() -> ContractAddress { contract_address_const::<'HOLDER'>() } +fn TREASURY() -> ContractAddress { + contract_address_const::<'TREASURY'>() +} + fn VAULT_NAME() -> ByteArray { "VAULT" } @@ -89,6 +93,77 @@ fn deploy_vault_offset(asset_address: ContractAddress) -> ERC4626ABIDispatcher { deploy_vault_offset_minted_shares(asset_address, 0, HOLDER()) } +fn deploy_vault_fees(asset_address: ContractAddress) -> ERC4626ABIDispatcher { + let no_shares = 0_u256; + deploy_vault_fees_with_shares(asset_address, no_shares, HOLDER()) +} + +fn deploy_vault_fees_with_shares( + asset_address: ContractAddress, shares: u256, recipient: ContractAddress +) -> ERC4626ABIDispatcher { + let fee_basis_points = 500_u256; // 5% + let _value_without_fees = 10_000_u256; + let _fees = (_value_without_fees * fee_basis_points) / 10_000_u256; + let _value_with_fees = _value_without_fees - _fees; + + let mut vault_calldata: Array = array![]; + vault_calldata.append_serde(VAULT_NAME()); + vault_calldata.append_serde(VAULT_SYMBOL()); + vault_calldata.append_serde(asset_address); + vault_calldata.append_serde(shares); + vault_calldata.append_serde(recipient); + + // Enter fees + vault_calldata.append_serde(fee_basis_points); + vault_calldata.append_serde(TREASURY()); + // No exit fees + vault_calldata.append_serde(0_u256); + vault_calldata.append_serde(ZERO()); + + let contract_address = utils::declare_and_deploy("ERC4626FeesMock", vault_calldata); + ERC4626ABIDispatcher { contract_address } +} + +fn deploy_vault_exit_fees_with_shares( + asset_address: ContractAddress, shares: u256, recipient: ContractAddress +) -> ERC4626ABIDispatcher { + let fee_basis_points = 500_u256; // 5% + let _value_without_fees = 10_000_u256; + let _fees = (_value_without_fees * fee_basis_points) / 10_000_u256; + let _value_with_fees = _value_without_fees - _fees; + + let mut vault_calldata: Array = array![]; + vault_calldata.append_serde(VAULT_NAME()); + vault_calldata.append_serde(VAULT_SYMBOL()); + vault_calldata.append_serde(asset_address); + vault_calldata.append_serde(shares); + vault_calldata.append_serde(recipient); + + // No enter fees + vault_calldata.append_serde(0_u256); + vault_calldata.append_serde(ZERO()); + // Exit fees + vault_calldata.append_serde(fee_basis_points); + vault_calldata.append_serde(TREASURY()); + + let contract_address = utils::declare_and_deploy("ERC4626FeesMock", vault_calldata); + ERC4626ABIDispatcher { contract_address } +} + +fn deploy_vault_limits(asset_address: ContractAddress) -> ERC4626ABIDispatcher { + let no_shares = 0_u256; + + let mut vault_calldata: Array = array![]; + vault_calldata.append_serde(VAULT_NAME()); + vault_calldata.append_serde(VAULT_SYMBOL()); + vault_calldata.append_serde(asset_address); + vault_calldata.append_serde(no_shares); + vault_calldata.append_serde(HOLDER()); + + let contract_address = utils::declare_and_deploy("ERC4626LimitsMock", vault_calldata); + ERC4626ABIDispatcher { contract_address } +} + // // Metadata // @@ -984,6 +1059,261 @@ fn test_price_change_during_reentrancy_doesnt_affect_withdraw() { assert(shares_after > shares_before, 'Burn should change share price'); } +// +// Limits +// + +fn setup_limits() -> (IERC20ReentrantDispatcher, ERC4626ABIDispatcher) { + let mut asset = deploy_asset(); + let mut vault = deploy_vault_limits(asset.contract_address); + + (asset, vault) +} + +#[test] +#[should_panic(expected: 'ERC4626: exceeds max deposit')] +fn test_max_limit_deposit() { + let (_, vault) = setup_limits(); + + let max_deposit = vault.max_deposit(HOLDER()); + cheat_caller_address(vault.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); + vault.deposit(max_deposit + 1, HOLDER()); +} + +#[test] +#[should_panic(expected: 'ERC4626: exceeds max mint')] +fn test_max_limit_mint() { + let (_, vault) = setup_limits(); + + let max_mint = vault.max_mint(HOLDER()); + cheat_caller_address(vault.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); + vault.mint(max_mint + 1, HOLDER()); +} + +#[test] +#[should_panic(expected: 'ERC4626: exceeds max withdraw')] +fn test_max_limit_withdraw() { + let (_, vault) = setup_limits(); + + let max_withdraw = vault.max_redeem(HOLDER()); + cheat_caller_address(vault.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); + vault.withdraw(max_withdraw + 1, HOLDER(), HOLDER()); +} + +#[test] +#[should_panic(expected: 'ERC4626: exceeds max redeem')] +fn test_max_limit_redeem() { + let (_, vault) = setup_limits(); + + let max_redeem = vault.max_redeem(HOLDER()); + cheat_caller_address(vault.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); + vault.redeem(max_redeem + 1, HOLDER(), HOLDER()); +} + +// +// Fees +// + +fn setup_input_fees() -> (IERC20ReentrantDispatcher, ERC4626ABIDispatcher) { + let mut asset = deploy_asset(); + let mut vault = deploy_vault_fees(asset.contract_address); + + let half_max: u256 = Bounded::MAX / 2; + asset.unsafe_mint(HOLDER(), half_max); + + cheat_caller_address(asset.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); + asset.approve(vault.contract_address, half_max); + + (asset, vault) +} + +fn setup_output_fees() -> (IERC20ReentrantDispatcher, ERC4626ABIDispatcher) { + let mut asset = deploy_asset(); + let half_max: u256 = Bounded::MAX / 2; + + // Mint shares to HOLDER + let mut vault = deploy_vault_exit_fees_with_shares(asset.contract_address, half_max, HOLDER()); + + // Mint assets to vault + asset.unsafe_mint(vault.contract_address, half_max); + + (asset, vault) +} + +#[test] +fn test_input_fees_deposit() { + let (asset, vault) = setup_input_fees(); + + let FEE_BASIS_POINTS: u256 = 500; // 5% + let VALUE_WITHOUT_FEES: u256 = 10_000; + let FEES = (VALUE_WITHOUT_FEES * FEE_BASIS_POINTS) / 10_000; + let VALUE_WITH_FEES = VALUE_WITHOUT_FEES + FEES; + + let actual_value = vault.preview_deposit(VALUE_WITH_FEES); + assert_eq!(actual_value, VALUE_WITHOUT_FEES); + + let holder_asset_bal = asset.balance_of(HOLDER()); + let vault_asset_bal = asset.balance_of(vault.contract_address); + + let mut spy = spy_events(); + cheat_caller_address(vault.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); + vault.deposit(VALUE_WITH_FEES, RECIPIENT()); + + // Check asset balances + assert_expected_assets(asset, HOLDER(), holder_asset_bal - VALUE_WITH_FEES); + assert_expected_assets(asset, vault.contract_address, vault_asset_bal + VALUE_WITHOUT_FEES); + assert_expected_assets(asset, TREASURY(), FEES); + + // Check shares + assert_expected_shares(vault, RECIPIENT(), VALUE_WITHOUT_FEES); + + // Check events + spy + .assert_event_transfer( + asset.contract_address, HOLDER(), vault.contract_address, VALUE_WITH_FEES + ); + spy.assert_event_transfer(vault.contract_address, ZERO(), RECIPIENT(), VALUE_WITHOUT_FEES); + spy + .assert_event_deposit( + vault.contract_address, HOLDER(), RECIPIENT(), VALUE_WITH_FEES, VALUE_WITHOUT_FEES + ); + spy.assert_event_transfer(asset.contract_address, vault.contract_address, TREASURY(), FEES); +} + +#[test] +fn test_input_fees_mint() { + let (asset, vault) = setup_input_fees(); + + let FEE_BASIS_POINTS: u256 = 500; // 5% + let VALUE_WITHOUT_FEES: u256 = 10_000; + let FEES = (VALUE_WITHOUT_FEES * FEE_BASIS_POINTS) / 10_000; + let VALUE_WITH_FEES = VALUE_WITHOUT_FEES + FEES; + + let actual_value = vault.preview_mint(VALUE_WITHOUT_FEES); + assert_eq!(actual_value, VALUE_WITH_FEES); + + let holder_asset_bal = asset.balance_of(HOLDER()); + let vault_asset_bal = asset.balance_of(vault.contract_address); + + let mut spy = spy_events(); + cheat_caller_address(vault.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); + vault.mint(VALUE_WITHOUT_FEES, RECIPIENT()); + + // Check asset balances + assert_expected_assets(asset, HOLDER(), holder_asset_bal - VALUE_WITH_FEES); + assert_expected_assets(asset, vault.contract_address, vault_asset_bal + VALUE_WITHOUT_FEES); + assert_expected_assets(asset, TREASURY(), FEES); + + // Check shares + assert_expected_shares(vault, RECIPIENT(), VALUE_WITHOUT_FEES); + + // Check events + spy + .assert_event_transfer( + asset.contract_address, HOLDER(), vault.contract_address, VALUE_WITH_FEES + ); + spy.assert_event_transfer(vault.contract_address, ZERO(), RECIPIENT(), VALUE_WITHOUT_FEES); + spy + .assert_event_deposit( + vault.contract_address, HOLDER(), RECIPIENT(), VALUE_WITH_FEES, VALUE_WITHOUT_FEES + ); + spy.assert_event_transfer(asset.contract_address, vault.contract_address, TREASURY(), FEES); +} + +#[test] +fn test_output_fees_redeem() { + let (asset, vault) = setup_output_fees(); + + let FEE_BASIS_POINTS: u256 = 500; // 5% + let VALUE_WITHOUT_FEES: u256 = 10_000; + let FEES = (VALUE_WITHOUT_FEES * FEE_BASIS_POINTS) / 10_000; + let VALUE_WITH_FEES = VALUE_WITHOUT_FEES + FEES; + + let preview_redeem = vault.preview_redeem(VALUE_WITH_FEES); + assert_eq!(preview_redeem, VALUE_WITHOUT_FEES); + + let vault_asset_bal = asset.balance_of(vault.contract_address); + let recipient_asset_bal = asset.balance_of(RECIPIENT()); + let treasury_asset_bal = asset.balance_of(TREASURY()); + let holder_shares = vault.balance_of(HOLDER()); + + let mut spy = spy_events(); + cheat_caller_address(vault.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); + vault.redeem(VALUE_WITH_FEES, RECIPIENT(), HOLDER()); + + // Check asset balances + assert_expected_assets(asset, vault.contract_address, vault_asset_bal - VALUE_WITH_FEES); + assert_expected_assets(asset, RECIPIENT(), recipient_asset_bal + VALUE_WITHOUT_FEES); + assert_expected_assets(asset, TREASURY(), treasury_asset_bal + FEES); + + // Check shares + assert_expected_shares(vault, HOLDER(), holder_shares - VALUE_WITH_FEES); + + // Check events + spy.assert_event_transfer(asset.contract_address, vault.contract_address, TREASURY(), FEES); + spy.assert_event_transfer(vault.contract_address, HOLDER(), ZERO(), VALUE_WITH_FEES); + spy + .assert_event_transfer( + asset.contract_address, vault.contract_address, RECIPIENT(), VALUE_WITHOUT_FEES + ); + spy + .assert_only_event_withdraw( + vault.contract_address, + HOLDER(), + RECIPIENT(), + HOLDER(), + VALUE_WITHOUT_FEES, + VALUE_WITH_FEES + ); +} + +#[test] +fn test_output_fees_withdraw() { + let (asset, vault) = setup_output_fees(); + + let FEE_BASIS_POINTS: u256 = 500; // 5% + let VALUE_WITHOUT_FEES: u256 = 10_000; + let FEES = (VALUE_WITHOUT_FEES * FEE_BASIS_POINTS) / 10_000; + let VALUE_WITH_FEES = VALUE_WITHOUT_FEES + FEES; + + let preview_withdraw = vault.preview_withdraw(VALUE_WITHOUT_FEES); + assert_eq!(preview_withdraw, VALUE_WITH_FEES); + + let vault_asset_bal = asset.balance_of(vault.contract_address); + let recipient_asset_bal = asset.balance_of(RECIPIENT()); + let treasury_asset_bal = asset.balance_of(TREASURY()); + let holder_shares = vault.balance_of(HOLDER()); + + let mut spy = spy_events(); + cheat_caller_address(vault.contract_address, HOLDER(), CheatSpan::TargetCalls(1)); + vault.withdraw(VALUE_WITHOUT_FEES, RECIPIENT(), HOLDER()); + + // Check asset balances + assert_expected_assets(asset, vault.contract_address, vault_asset_bal - VALUE_WITH_FEES); + assert_expected_assets(asset, RECIPIENT(), recipient_asset_bal + VALUE_WITHOUT_FEES); + assert_expected_assets(asset, TREASURY(), treasury_asset_bal + FEES); + + // Check shares + assert_expected_shares(vault, HOLDER(), holder_shares - VALUE_WITH_FEES); + + // Check events + spy.assert_event_transfer(asset.contract_address, vault.contract_address, TREASURY(), FEES); + spy.assert_event_transfer(vault.contract_address, HOLDER(), ZERO(), VALUE_WITH_FEES); + spy + .assert_event_transfer( + asset.contract_address, vault.contract_address, RECIPIENT(), VALUE_WITHOUT_FEES + ); + spy + .assert_only_event_withdraw( + vault.contract_address, + HOLDER(), + RECIPIENT(), + HOLDER(), + VALUE_WITHOUT_FEES, + VALUE_WITH_FEES + ); +} + // // Assertions/Helpers // From 1d14afcf2bd8b5d5dd80273cda756968683bb543 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 18 Oct 2024 21:05:12 -0500 Subject: [PATCH 47/90] fix conflicts --- packages/token/src/erc20.cairo | 1 + packages/token/src/erc20/extensions.cairo | 3 - .../token/src/erc20/extensions/erc4626.cairo | 9 + .../erc20/extensions/erc4626/erc4626.cairo | 548 ++++++++++++++++++ .../erc20/extensions/erc4626/interface.cairo | 243 ++++++++ 5 files changed, 801 insertions(+), 3 deletions(-) create mode 100644 packages/token/src/erc20/extensions/erc4626.cairo create mode 100644 packages/token/src/erc20/extensions/erc4626/erc4626.cairo create mode 100644 packages/token/src/erc20/extensions/erc4626/interface.cairo diff --git a/packages/token/src/erc20.cairo b/packages/token/src/erc20.cairo index e3a8368f7..2f9f94b58 100644 --- a/packages/token/src/erc20.cairo +++ b/packages/token/src/erc20.cairo @@ -1,4 +1,5 @@ pub mod erc20; +pub mod extensions; pub mod interface; pub mod snip12_utils; diff --git a/packages/token/src/erc20/extensions.cairo b/packages/token/src/erc20/extensions.cairo index 20aaef469..52a7e03eb 100644 --- a/packages/token/src/erc20/extensions.cairo +++ b/packages/token/src/erc20/extensions.cairo @@ -1,4 +1 @@ -pub mod erc20_votes; pub mod erc4626; - -pub use erc20_votes::ERC20VotesComponent; diff --git a/packages/token/src/erc20/extensions/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626.cairo new file mode 100644 index 000000000..c0b2bc208 --- /dev/null +++ b/packages/token/src/erc20/extensions/erc4626.cairo @@ -0,0 +1,9 @@ +pub mod erc4626; +pub mod interface; +pub use erc4626::DefaultConfig; + +pub use erc4626::ERC4626Component; +pub use erc4626::ERC4626DefaultLimits; +pub use erc4626::ERC4626DefaultNoFees; +pub use erc4626::ERC4626HooksEmptyImpl; +pub use interface::IERC4626; diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo new file mode 100644 index 000000000..4ef16a38b --- /dev/null +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -0,0 +1,548 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.17.0 (token/erc20/extensions/erc4626/erc4626.cairo) + +/// # ERC4626 Component +/// +/// ADD MEEEEEEEEEEEEEEEEE AHHHH +#[starknet::component] +pub mod ERC4626Component { + use core::num::traits::{Bounded, Zero}; + use crate::erc20::ERC20Component::InternalImpl as ERC20InternalImpl; + use crate::erc20::ERC20Component; + use crate::erc20::extensions::erc4626::interface::IERC4626; + use crate::erc20::interface::{IERC20, IERC20Metadata}; + use crate::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; + use openzeppelin_utils::math::Rounding; + use openzeppelin_utils::math; + use starknet::ContractAddress; + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + + // The default values are only used when the DefaultConfig + // is in scope in the implementing contract. + pub const DEFAULT_UNDERLYING_DECIMALS: u8 = 18; + pub const DEFAULT_DECIMALS_OFFSET: u8 = 0; + + #[storage] + pub struct Storage { + ERC4626_asset: ContractAddress + } + + #[event] + #[derive(Drop, PartialEq, starknet::Event)] + pub enum Event { + Deposit: Deposit, + Withdraw: Withdraw, + } + + #[derive(Drop, PartialEq, starknet::Event)] + pub struct Deposit { + #[key] + pub sender: ContractAddress, + #[key] + pub owner: ContractAddress, + pub assets: u256, + pub shares: u256 + } + + #[derive(Drop, PartialEq, starknet::Event)] + pub struct Withdraw { + #[key] + pub sender: ContractAddress, + #[key] + pub receiver: ContractAddress, + #[key] + pub owner: ContractAddress, + pub assets: u256, + pub shares: u256 + } + + pub mod Errors { + pub const EXCEEDED_MAX_DEPOSIT: felt252 = 'ERC4626: exceeds max deposit'; + pub const EXCEEDED_MAX_MINT: felt252 = 'ERC4626: exceeds max mint'; + pub const EXCEEDED_MAX_WITHDRAW: felt252 = 'ERC4626: exceeds max withdraw'; + pub const EXCEEDED_MAX_REDEEM: felt252 = 'ERC4626: exceeds max redeem'; + pub const TOKEN_TRANSFER_FAILED: felt252 = 'ERC4626: token transfer failed'; + pub const INVALID_ASSET_ADDRESS: felt252 = 'ERC4626: asset address set to 0'; + pub const DECIMALS_OVERFLOW: felt252 = 'ERC4626: decimals overflow'; + } + + /// Constants expected to be defined at the contract level used to configure the component + /// behaviour. + /// + /// ADD ME... + pub trait ImmutableConfig { + const UNDERLYING_DECIMALS: u8; + const DECIMALS_OFFSET: u8; + + fn validate() { + assert( + Bounded::MAX - Self::UNDERLYING_DECIMALS >= Self::DECIMALS_OFFSET, + Errors::DECIMALS_OVERFLOW + ) + } + } + + /// Adjustments for fees expected to be defined on the contract level. + /// Defaults to no entry or exit fees. + /// To transfer fees, this trait needs to be coordinated with ERC4626Component::ERC4626Hooks. + pub trait FeeConfigTrait { + fn adjust_deposit(self: @ComponentState, assets: u256) -> u256 { + assets + } + + fn adjust_mint(self: @ComponentState, shares: u256) -> u256 { + shares + } + + fn adjust_withdraw(self: @ComponentState, assets: u256) -> u256 { + assets + } + + fn adjust_redeem(self: @ComponentState, shares: u256) -> u256 { + shares + } + } + + /// Sets custom limits to the target exchange type and is expected to be defined at the contract + /// level. + pub trait LimitConfigTrait { + fn deposit_limit( + self: @ComponentState, receiver: ContractAddress + ) -> Option:: { + Option::None + } + + fn mint_limit( + self: @ComponentState, receiver: ContractAddress + ) -> Option:: { + Option::None + } + + fn withdraw_limit( + self: @ComponentState, owner: ContractAddress + ) -> Option:: { + Option::None + } + + fn redeem_limit( + self: @ComponentState, owner: ContractAddress + ) -> Option:: { + Option::None + } + } + + /// Allows contracts to hook logic into deposit and withdraw transactions. + /// This is where contracts can transfer fees. + pub trait ERC4626HooksTrait { + fn before_withdraw(ref self: ComponentState, assets: u256, shares: u256) {} + fn after_deposit(ref self: ComponentState, assets: u256, shares: u256) {} + } + + #[embeddable_as(ERC4626Impl)] + impl ERC4626< + TContractState, + +HasComponent, + impl Fee: FeeConfigTrait, + impl Limit: LimitConfigTrait, + impl Hooks: ERC4626HooksTrait, + impl Immutable: ImmutableConfig, + impl ERC20: ERC20Component::HasComponent, + +ERC20Component::ERC20HooksTrait, + +Drop + > of IERC4626> { + /// Returns the address of the underlying token used for the Vault for accounting, + /// depositing, and withdrawing. + fn asset(self: @ComponentState) -> ContractAddress { + self.ERC4626_asset.read() + } + + /// Returns the total amount of the underlying asset that is “managed” by Vault. + fn total_assets(self: @ComponentState) -> u256 { + let this = starknet::get_contract_address(); + IERC20Dispatcher { contract_address: self.ERC4626_asset.read() }.balance_of(this) + } + + /// Returns the amount of shares that the Vault would exchange for the amount of assets + /// provided, in an ideal scenario where all the conditions are met. + fn convert_to_shares(self: @ComponentState, assets: u256) -> u256 { + self._convert_to_shares(assets, Rounding::Floor) + } + + /// Returns the amount of assets that the Vault would exchange for the amount of shares + /// provided, in an ideal scenario where all the conditions are met. + fn convert_to_assets(self: @ComponentState, shares: u256) -> u256 { + self._convert_to_assets(shares, Rounding::Floor) + } + + /// Returns the maximum amount of the underlying asset that can be deposited into the Vault + /// for the receiver, through a deposit call. + /// If the `LimitConfigTrait` is not defined for deposits, returns 2 ** 256 - 1. + fn max_deposit(self: @ComponentState, receiver: ContractAddress) -> u256 { + match Limit::deposit_limit(self, receiver) { + Option::Some(limit) => limit, + Option::None => Bounded::MAX + } + } + + /// Allows an on-chain or off-chain user to simulate the effects of their deposit at the + /// current block, given current on-chain conditions. + /// If the `FeeConfigTrait` is not defined for deposits, returns the full amount of shares. + fn preview_deposit(self: @ComponentState, assets: u256) -> u256 { + let adjusted_assets = Fee::adjust_deposit(self, assets); + self._convert_to_shares(adjusted_assets, Rounding::Floor) + } + + /// Mints Vault shares to `receiver` by depositing exactly `assets` of underlying tokens. + /// Returns the amount of newly-minted shares. + /// + /// Requirements: + /// - `assets` is less than or equal to the max deposit amount for `receiver`. + /// + /// Emits a `Deposit` event. + fn deposit( + ref self: ComponentState, assets: u256, receiver: ContractAddress + ) -> u256 { + let max_assets = self.max_deposit(receiver); + assert(assets <= max_assets, Errors::EXCEEDED_MAX_DEPOSIT); + + let shares = self.preview_deposit(assets); + let caller = starknet::get_caller_address(); + self._deposit(caller, receiver, assets, shares); + + shares + } + + /// Returns the maximum amount of the Vault shares that can be minted for `receiver` through + /// a `mint` call. + /// If the `LimitConfigTrait` is not defined for mints, returns 2 ** 256 - 1. + fn max_mint(self: @ComponentState, receiver: ContractAddress) -> u256 { + match Limit::mint_limit(self, receiver) { + Option::Some(limit) => limit, + Option::None => Bounded::MAX + } + } + + /// Allows an on-chain or off-chain user to simulate the effects of their mint at the + /// current block, given current on-chain conditions. + /// If the `FeeConfigTrait` is not defined for mints, returns the full amount of assets. + fn preview_mint(self: @ComponentState, shares: u256) -> u256 { + let raw_amount = self._convert_to_assets(shares, Rounding::Ceil); + Fee::adjust_mint(self, raw_amount) + } + + /// Mints exactly Vault `shares` to `receiver` by depositing amount of underlying tokens. + /// Returns the amount deposited assets. + /// + /// Requirements: + /// - `shares` is less than or equal to the max shares amount for `receiver`. + /// + /// Emits a `Deposit` event. + fn mint( + ref self: ComponentState, shares: u256, receiver: ContractAddress + ) -> u256 { + let max_shares = self.max_mint(receiver); + assert(shares <= max_shares, Errors::EXCEEDED_MAX_MINT); + + let assets = self.preview_mint(shares); + let caller = starknet::get_caller_address(); + self._deposit(caller, receiver, assets, shares); + + assets + } + + /// Returns the maximum amount of the underlying asset that can be withdrawn from the owner + /// balance in the Vault, through a `withdraw` call. + /// If the `LimitConfigTrait` is not defined for withdraws, returns the full balance of + /// assets for `owner` (converted to shares). + fn max_withdraw(self: @ComponentState, owner: ContractAddress) -> u256 { + match Limit::withdraw_limit(self, owner) { + Option::Some(limit) => limit, + Option::None => { + let erc20_component = get_dep_component!(self, ERC20); + let owner_bal = erc20_component.balance_of(owner); + self._convert_to_assets(owner_bal, Rounding::Floor) + } + } + } + + /// Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the + /// current block, given current on-chain conditions. + /// If the `FeeConfigTrait` is not defined for withdraws, returns the full amount of shares. + fn preview_withdraw(self: @ComponentState, assets: u256) -> u256 { + let adjusted_assets = Fee::adjust_withdraw(self, assets); + self._convert_to_shares(adjusted_assets, Rounding::Ceil) + } + + /// Burns shares from `owner` and sends exactly `assets` of underlying tokens to `receiver`. + /// + /// Requirements: + /// - `assets` is less than or equal to the max withdraw amount of `owner`. + /// + /// Emits a `Withdraw` event. + fn withdraw( + ref self: ComponentState, + assets: u256, + receiver: ContractAddress, + owner: ContractAddress + ) -> u256 { + let max_assets = self.max_withdraw(owner); + assert(assets <= max_assets, Errors::EXCEEDED_MAX_WITHDRAW); + + let shares = self.preview_withdraw(assets); + let caller = starknet::get_caller_address(); + self._withdraw(caller, receiver, owner, assets, shares); + + shares + } + + /// Returns the maximum amount of Vault shares that can be redeemed from the owner balance + /// in the Vault, through a `redeem` call. + /// If the `LimitConfigTrait` is not defined for redeems, returns the full balance of assets + /// for `owner`. + fn max_redeem(self: @ComponentState, owner: ContractAddress) -> u256 { + match Limit::redeem_limit(self, owner) { + Option::Some(limit) => limit, + Option::None => { + let erc20_component = get_dep_component!(self, ERC20); + erc20_component.balance_of(owner) + } + } + } + + /// Allows an on-chain or off-chain user to simulate the effects of their redeemption at the + /// current block, given current on-chain conditions. + /// If the `FeeConfigTrait` is not defined for redeems, returns the full amount of assets. + fn preview_redeem(self: @ComponentState, shares: u256) -> u256 { + let raw_amount = self._convert_to_assets(shares, Rounding::Floor); + Fee::adjust_redeem(self, raw_amount) + } + + /// Burns exactly `shares` from `owner` and sends assets of underlying tokens to `receiver`. + /// + /// Requirements: + /// - `shares` is less than or equal to the max redeem amount of `owner`. + /// + /// Emits a `Withdraw` event. + fn redeem( + ref self: ComponentState, + shares: u256, + receiver: ContractAddress, + owner: ContractAddress + ) -> u256 { + let max_shares = self.max_redeem(owner); + assert(shares <= max_shares, Errors::EXCEEDED_MAX_REDEEM); + + let assets = self.preview_redeem(shares); + let caller = starknet::get_caller_address(); + self._withdraw(caller, receiver, owner, assets, shares); + + assets + } + } + + #[embeddable_as(ERC4626MetadataImpl)] + impl ERC4626Metadata< + TContractState, + +HasComponent, + impl Immutable: ImmutableConfig, + impl ERC20: ERC20Component::HasComponent, + > of IERC20Metadata> { + /// Returns the name of the token. + fn name(self: @ComponentState) -> ByteArray { + let erc20_component = get_dep_component!(self, ERC20); + erc20_component.ERC20_name.read() + } + + /// Returns the ticker symbol of the token, usually a shorter version of the name. + fn symbol(self: @ComponentState) -> ByteArray { + let erc20_component = get_dep_component!(self, ERC20); + erc20_component.ERC20_symbol.read() + } + + /// Returns the cumulative number of decimals which includes both the underlying and offset + /// decimals. + /// Both of which must be defined in the `ImmutableConfig` inside the implementing contract. + fn decimals(self: @ComponentState) -> u8 { + Immutable::UNDERLYING_DECIMALS + Immutable::DECIMALS_OFFSET + } + } + + #[generate_trait] + pub impl InternalImpl< + TContractState, + +HasComponent, + impl Hooks: ERC4626HooksTrait, + impl Immutable: ImmutableConfig, + impl ERC20: ERC20Component::HasComponent, + +FeeConfigTrait, + +LimitConfigTrait, + +ERC20Component::ERC20HooksTrait, + +Drop + > of InternalTrait { + /// Validates the `ImmutableConfig` constants and sets the `asset_address` to the vault. + /// This should be set in the contract's constructor. + /// + /// Requirements: + /// - `asset_address` cannot be the zero address. + fn initializer(ref self: ComponentState, asset_address: ContractAddress) { + ImmutableConfig::validate(); + assert(!asset_address.is_zero(), Errors::INVALID_ASSET_ADDRESS); + self.ERC4626_asset.write(asset_address); + } + + /// Business logic for `deposit` and `mint`. + /// Transfers `assets` from `caller` to the Vault contract then mints `shares` to + /// `receiver`. + /// Fees can be transferred in the `ERC4626Hooks::after_deposit` hook which is executed + /// after the business logic. + /// + /// Requirements: + /// - `ERC20::transfer_from` must return true. + /// + /// Emits two `ERC20::Transfer` events (`ERC20::mint` and `ERC20::transfer_from`). + /// Emits a `Deposit` event. + fn _deposit( + ref self: ComponentState, + caller: ContractAddress, + receiver: ContractAddress, + assets: u256, + shares: u256 + ) { + // Transfer assets first + let this = starknet::get_contract_address(); + let asset_dispatcher = IERC20Dispatcher { contract_address: self.ERC4626_asset.read() }; + assert( + asset_dispatcher.transfer_from(caller, this, assets), Errors::TOKEN_TRANSFER_FAILED + ); + + // Mint shares after transferring assets + let mut erc20_component = get_dep_component_mut!(ref self, ERC20); + erc20_component.mint(receiver, shares); + self.emit(Deposit { sender: caller, owner: receiver, assets, shares }); + + // After deposit hook + Hooks::after_deposit(ref self, assets, shares); + } + + /// Business logic for `withdraw` and `redeem`. + /// Burns `shares` from `owner` and then transfers `assets` to `receiver`. + /// Fees can be transferred in the `ERC4626Hooks::before_withdraw` hook which is executed + /// before the business logic. + /// + /// Requirements: + /// - `ERC20::transfer` must return true. + /// + /// Emits two `ERC20::Transfer` events (`ERC20::burn` and `ERC20::transfer`). + /// Emits a `Withdraw` event. + fn _withdraw( + ref self: ComponentState, + caller: ContractAddress, + receiver: ContractAddress, + owner: ContractAddress, + assets: u256, + shares: u256 + ) { + // Before withdraw hook + Hooks::before_withdraw(ref self, assets, shares); + + // Burn shares first + let mut erc20_component = get_dep_component_mut!(ref self, ERC20); + if (caller != owner) { + erc20_component._spend_allowance(owner, caller, shares); + } + erc20_component.burn(owner, shares); + + // Transfer assets after burn + let asset_dispatcher = IERC20Dispatcher { contract_address: self.ERC4626_asset.read() }; + assert(asset_dispatcher.transfer(receiver, assets), Errors::TOKEN_TRANSFER_FAILED); + + self.emit(Withdraw { sender: caller, receiver, owner, assets, shares }); + } + + /// Internal conversion function (from assets to shares) with support for `rounding` + /// direction. + fn _convert_to_shares( + self: @ComponentState, assets: u256, rounding: Rounding + ) -> u256 { + let mut erc20_component = get_dep_component!(self, ERC20); + let total_supply = erc20_component.total_supply(); + + math::u256_mul_div( + assets, + total_supply + math::power(10, Immutable::DECIMALS_OFFSET.into()), + self.total_assets() + 1, + rounding + ) + } + + /// Internal conversion function (from shares to assets) with support for `rounding` + /// direction. + fn _convert_to_assets( + self: @ComponentState, shares: u256, rounding: Rounding + ) -> u256 { + let mut erc20_component = get_dep_component!(self, ERC20); + let total_supply = erc20_component.total_supply(); + + math::u256_mul_div( + shares, + self.total_assets() + 1, + total_supply + math::power(10, Immutable::DECIMALS_OFFSET.into()), + rounding + ) + } + } +} + +/// +/// Default (empty) traits +/// + +pub impl ERC4626HooksEmptyImpl< + TContractState +> of ERC4626Component::ERC4626HooksTrait {} +pub impl ERC4626DefaultNoFees of ERC4626Component::FeeConfigTrait {} +pub impl ERC4626DefaultLimits< + TContractState +> of ERC4626Component::LimitConfigTrait {} + +/// Implementation of the default `ERC4626Component::ImmutableConfig`. +/// +/// See +/// https://github.com/starknet-io/SNIPs/blob/963848f0752bde75c7087c2446d83b7da8118b25/SNIPS/snip-107.md#defaultconfig-implementation +/// +/// The default underlying decimals is set to `18`. +/// The default decimals offset is set to `0`. +pub impl DefaultConfig of ERC4626Component::ImmutableConfig { + const UNDERLYING_DECIMALS: u8 = ERC4626Component::DEFAULT_UNDERLYING_DECIMALS; + const DECIMALS_OFFSET: u8 = ERC4626Component::DEFAULT_DECIMALS_OFFSET; +} + +#[cfg(test)] +mod Test { + use openzeppelin_test_common::mocks::erc4626::ERC4626Mock; + use super::ERC4626Component::InternalImpl; + use super::ERC4626Component; + use super::ERC4626DefaultLimits; + use super::ERC4626DefaultNoFees; + + type ComponentState = ERC4626Component::ComponentState; + + fn COMPONENT_STATE() -> ComponentState { + ERC4626Component::component_state_for_testing() + } + + // Invalid decimals + impl InvalidImmutableConfig of ERC4626Component::ImmutableConfig { + const UNDERLYING_DECIMALS: u8 = 255; + const DECIMALS_OFFSET: u8 = 1; + } + + #[test] + #[should_panic(expected: 'ERC4626: decimals overflow')] + fn test_initializer_invalid_config_panics() { + let mut state = COMPONENT_STATE(); + let asset = starknet::contract_address_const::<'ASSET'>(); + + state.initializer(asset); + } +} diff --git a/packages/token/src/erc20/extensions/erc4626/interface.cairo b/packages/token/src/erc20/extensions/erc4626/interface.cairo new file mode 100644 index 000000000..2eb9e3b0b --- /dev/null +++ b/packages/token/src/erc20/extensions/erc4626/interface.cairo @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.17.0 (token/erc20/extensions/erc4626/interface.cairo) + +use starknet::ContractAddress; + +#[starknet::interface] +pub trait IERC4626 { + /// Returns the address of the underlying token used for the Vault for accounting, depositing, + /// and withdrawing. + /// + /// MUST be an ERC20 token contract. + /// MUST NOT panic. + fn asset(self: @TState) -> ContractAddress; + /// Returns the total amount of the underlying asset that is “managed” by Vault. + /// + /// SHOULD include any compounding that occurs from yield. + /// MUST be inclusive of any fees that are charged against assets in the Vault. + /// MUST NOT panic. + fn total_assets(self: @TState) -> u256; + /// Returns the amount of shares that the Vault would exchange for the amount of assets + /// provided, in an ideal scenario where all the conditions are met. + /// + /// MUST NOT be inclusive of any fees that are charged against assets in the Vault. + /// MUST NOT show any variations depending on the caller. + /// MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange. + /// MUST NOT panic. + fn convert_to_shares(self: @TState, assets: u256) -> u256; + /// Returns the amount of assets that the Vault would exchange for the amount of shares + /// provided, in an ideal scenario where all the conditions are met. + /// + /// MUST NOT be inclusive of any fees that are charged against assets in the Vault. + /// MUST NOT show any variations depending on the caller. + /// MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange. + /// MUST NOT panic. + /// + /// Note that this calculation MAY NOT reflect the “per-user” price-per-share, and instead + /// should reflect the “average-user’s” price-per-share, meaning what the average user + /// should expect to see when exchanging to and from. + fn convert_to_assets(self: @TState, shares: u256) -> u256; + /// Returns the maximum amount of the underlying asset that can be deposited into the Vault for + /// `receiver`, through a deposit call. + /// + /// MUST return a limited value if receiver is subject to some deposit limit. + /// MUST return 2 ** 256 - 1 if there is no limit on the maximum amount of assets that may be + /// deposited. + /// MUST NOT panic. + /// + /// Note that this calculation MAY NOT reflect the “per-user” price-per-share, and instead + /// should reflect the “average-user’s” price-per-share, meaning what the average user + /// should expect to see when exchanging to and from. + fn max_deposit(self: @TState, receiver: ContractAddress) -> u256; + /// Allows an on-chain or off-chain user to simulate the effects of their deposit at the current + /// block, given current on-chain conditions. + /// + /// MUST return as close to and no more than the exact amount of Vault shares that would be + /// minted in a deposit call in the same transaction i.e. deposit should return the same or more + /// shares as `preview_deposit` if called in the same transaction. + /// MUST NOT account for deposit limits like those returned from `max_deposit` and should always + /// act as though the deposit would be accepted, regardless if the user has enough tokens + /// approved, etc. + /// MUST be inclusive of deposit fees. Integrators should be aware of the existence of deposit + /// fees. + /// MUST NOT panic. + /// + /// Note that any unfavorable discrepancy between `convert_to_shares` and `preview_deposit` + /// SHOULD be considered slippage in share price or some other type of condition, meaning the + /// depositor will lose assets by depositing. + fn preview_deposit(self: @TState, assets: u256) -> u256; + /// Mints Vault shares to `receiver` by depositing exactly amount of `assets`. + /// + /// MUST emit the Deposit event. + /// MAY support an additional flow in which the underlying tokens are owned by the Vault + /// contract before the deposit execution, and are accounted for during deposit. + /// MUST panic if all of assets cannot be deposited (due to deposit limit being reached, + /// slippage, the user not approving enough underlying tokens to the Vault contract, etc). + /// + /// Note that most implementations will require pre-approval of the Vault with the Vault’s + /// underlying asset token. + fn deposit(ref self: TState, assets: u256, receiver: ContractAddress) -> u256; + /// Returns the maximum amount of the Vault shares that can be minted for the receiver, through + /// a mint call. + /// + /// MUST return a limited value if receiver is subject to some mint limit. + /// MUST return 2 ** 256 - 1 if there is no limit on the maximum amount of shares that may be + /// minted. + /// MUST NOT panic. + fn max_mint(self: @TState, receiver: ContractAddress) -> u256; + /// Allows an on-chain or off-chain user to simulate the effects of their mint at the current + /// block, given current on-chain conditions. + /// + /// MUST return as close to and no fewer than the exact amount of assets that would be deposited + /// in a `mint` call in the same transaction. I.e. `mint` should return the same or fewer assets + /// as `preview_mint` if called in the same transaction. + /// MUST NOT account for mint limits like those returned from `max_mint` and should always act + /// as though the mint would be accepted, regardless if the user has enough tokens approved, + /// etc. + /// MUST be inclusive of deposit fees. Integrators should be aware of the existence of deposit + /// fees. + /// MUST NOT panic. + /// + /// NOTE: Any unfavorable discrepancy between convertToAssets and previewMint SHOULD be + /// considered slippage in share price or some other type of condition, meaning the depositor + /// will lose assets by minting. + /// + /// Note that any unfavorable discrepancy between `convert_to_assets` and `preview_mint` SHOULD + /// be considered slippage in share price or some other type of condition, meaning the depositor + /// will lose assets by minting. + fn preview_mint(self: @TState, shares: u256) -> u256; + /// Mints exactly shares Vault shares to receiver by depositing amount of underlying tokens. + /// + /// MUST emit the `Deposit` event. + /// MAY support an additional flow in which the underlying tokens are owned by the Vault + /// contract before the mint execution, and are accounted for during mint. + /// MUST panic if all of shares cannot be minted (due to deposit limit being reached, slippage, + /// the user not approving enough underlying tokens to the Vault contract, etc). + /// + /// Note that most implementations will require pre-approval of the Vault with the Vault’s + /// underlying asset token. + fn mint(ref self: TState, shares: u256, receiver: ContractAddress) -> u256; + /// Returns the maximum amount of the underlying asset that can be withdrawn from the owner + /// balance in the Vault, through a withdraw call. + /// + /// MUST return a limited value if owner is subject to some withdrawal limit or timelock. + /// MUST NOT panic. + fn max_withdraw(self: @TState, owner: ContractAddress) -> u256; + /// Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the + /// current block, given current on-chain conditions. + /// + /// MUST return as close to and no fewer than the exact amount of Vault shares that would be + /// burned in a withdraw call in the same transaction i.e. withdraw should return the same or + /// fewer shares as preview_withdraw if called in the same transaction. + /// MUST NOT account for withdrawal limits like those returned from max_withdraw and should + /// always act as though the withdrawal would be accepted, regardless if the user has enough + /// shares, etc. + /// MUST be inclusive of withdrawal fees. Integrators should be aware of the existence of + /// withdrawal fees. + /// MUST not panic. + /// + /// Note that any unfavorable discrepancy between `convert_to_shares` and `preview_withdraw` + /// SHOULD be considered slippage in share price or some other type of condition, meaning the + /// depositor will lose assets by depositing. + fn preview_withdraw(self: @TState, assets: u256) -> u256; + /// Burns shares from owner and sends exactly assets of underlying tokens to receiver. + /// + /// MUST emit the `Withdraw` event. + /// MAY support an additional flow in which the underlying tokens are owned by the Vault + /// contract before the withdraw execution, and are accounted for during withdraw. + /// MUST revert if all of assets cannot be withdrawn (due to withdrawal limit being reached, + /// slippage, the owner not having enough shares, etc). + /// + /// Note that some implementations will require pre-requesting to the Vault before a withdrawal + /// may be performed. + /// Those methods should be performed separately. + fn withdraw( + ref self: TState, assets: u256, receiver: ContractAddress, owner: ContractAddress + ) -> u256; + /// Returns the maximum amount of Vault shares that can be redeemed from the owner balance in + /// the Vault, through a redeem call. + /// + /// MUST return a limited value if owner is subject to some withdrawal limit or timelock. + /// MUST return `ERC20::balance_of(owner)` if `owner` is not subject to any withdrawal limit or + /// timelock. + /// MUST NOT panic. + fn max_redeem(self: @TState, owner: ContractAddress) -> u256; + /// Allows an on-chain or off-chain user to simulate the effects of their redeemption at the + /// current block, given current on-chain conditions. + /// + /// MUST return as close to and no more than the exact amount of assets that would be withdrawn + /// in a redeem call in the same transaction i.e. redeem should return the same or more assets + /// as preview_redeem if called in the same transaction. + /// MUST NOT account for redemption limits like those returned from max_redeem and should always + /// act as though the redemption would be accepted, regardless if the user has enough shares, + /// etc. + /// MUST be inclusive of withdrawal fees. Integrators should be aware of the existence of + /// withdrawal fees. + /// MUST NOT panic. + /// + /// Note any unfavorable discrepancy between `convert_to_assets` and `preview_redeem` SHOULD be + /// considered slippage in share price or some other type of condition, meaning the depositor + /// will lose assets by redeeming. + fn preview_redeem(self: @TState, shares: u256) -> u256; + /// Burns exactly shares from owner and sends assets of underlying tokens to receiver. + /// + /// MUST emit the `Withdraw` event. + /// MAY support an additional flow in which the underlying tokens are owned by the Vault + /// contract before the redeem execution, and are accounted for during redeem. + /// MUST revert if all of shares cannot be redeemed (due to withdrawal limit being reached, + /// slippage, the owner not having enough shares, etc). + /// + /// Note some implementations will require pre-requesting to the Vault before a withdrawal may + /// be performed. + /// Those methods should be performed separately. + fn redeem( + ref self: TState, shares: u256, receiver: ContractAddress, owner: ContractAddress + ) -> u256; +} + +#[starknet::interface] +pub trait ERC4626ABI { + // IERC4626 + fn asset(self: @TState) -> ContractAddress; + fn total_assets(self: @TState) -> u256; + fn convert_to_shares(self: @TState, assets: u256) -> u256; + fn convert_to_assets(self: @TState, shares: u256) -> u256; + fn max_deposit(self: @TState, receiver: ContractAddress) -> u256; + fn preview_deposit(self: @TState, assets: u256) -> u256; + fn deposit(ref self: TState, assets: u256, receiver: ContractAddress) -> u256; + fn max_mint(self: @TState, receiver: ContractAddress) -> u256; + fn preview_mint(self: @TState, shares: u256) -> u256; + fn mint(ref self: TState, shares: u256, receiver: ContractAddress) -> u256; + fn max_withdraw(self: @TState, owner: ContractAddress) -> u256; + fn preview_withdraw(self: @TState, assets: u256) -> u256; + fn withdraw( + ref self: TState, assets: u256, receiver: ContractAddress, owner: ContractAddress + ) -> u256; + fn max_redeem(self: @TState, owner: ContractAddress) -> u256; + fn preview_redeem(self: @TState, shares: u256) -> u256; + fn redeem( + ref self: TState, shares: u256, receiver: ContractAddress, owner: ContractAddress + ) -> u256; + + // IERC20 + fn total_supply(self: @TState) -> u256; + fn balance_of(self: @TState, account: ContractAddress) -> u256; + fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress) -> u256; + fn transfer(ref self: TState, recipient: ContractAddress, amount: u256) -> bool; + fn transfer_from( + ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + fn approve(ref self: TState, spender: ContractAddress, amount: u256) -> bool; + + // IERC20Metadata + fn name(self: @TState) -> ByteArray; + fn symbol(self: @TState) -> ByteArray; + fn decimals(self: @TState) -> u8; + + // IERC20CamelOnly + fn totalSupply(self: @TState) -> u256; + fn balanceOf(self: @TState, account: ContractAddress) -> u256; + fn transferFrom( + ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; +} From acc204744862c3355137e650bec0ed8705a02050 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 18 Oct 2024 21:05:32 -0500 Subject: [PATCH 48/90] fix fmt --- packages/utils/src/tests.cairo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/utils/src/tests.cairo b/packages/utils/src/tests.cairo index 81bc17e0f..597d6ddef 100644 --- a/packages/utils/src/tests.cairo +++ b/packages/utils/src/tests.cairo @@ -1,4 +1,4 @@ -mod test_math; mod test_checkpoint; +mod test_math; mod test_nonces; mod test_snip12; From 2e368fd294807be8db51977f50536f2945947761 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 18 Oct 2024 21:08:13 -0500 Subject: [PATCH 49/90] update spdx --- packages/governance/src/votes/erc4626/erc4626.cairo | 2 +- packages/governance/src/votes/erc4626/interface.cairo | 2 +- packages/token/src/erc20/extensions/erc4626/erc4626.cairo | 2 +- packages/token/src/erc20/extensions/erc4626/interface.cairo | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/governance/src/votes/erc4626/erc4626.cairo b/packages/governance/src/votes/erc4626/erc4626.cairo index a59819c03..6a794bbe9 100644 --- a/packages/governance/src/votes/erc4626/erc4626.cairo +++ b/packages/governance/src/votes/erc4626/erc4626.cairo @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.17.0 (token/erc20/extensions/erc4626/erc4626.cairo) +// OpenZeppelin Contracts for Cairo v0.18.0 (token/erc20/extensions/erc4626/erc4626.cairo) /// # ERC4626 Component /// diff --git a/packages/governance/src/votes/erc4626/interface.cairo b/packages/governance/src/votes/erc4626/interface.cairo index 2eb9e3b0b..2aea38136 100644 --- a/packages/governance/src/votes/erc4626/interface.cairo +++ b/packages/governance/src/votes/erc4626/interface.cairo @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.17.0 (token/erc20/extensions/erc4626/interface.cairo) +// OpenZeppelin Contracts for Cairo v0.18.0 (token/erc20/extensions/erc4626/interface.cairo) use starknet::ContractAddress; diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo index 4ef16a38b..39a269958 100644 --- a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.17.0 (token/erc20/extensions/erc4626/erc4626.cairo) +// OpenZeppelin Contracts for Cairo v0.18.0 (token/erc20/extensions/erc4626/erc4626.cairo) /// # ERC4626 Component /// diff --git a/packages/token/src/erc20/extensions/erc4626/interface.cairo b/packages/token/src/erc20/extensions/erc4626/interface.cairo index 2eb9e3b0b..2aea38136 100644 --- a/packages/token/src/erc20/extensions/erc4626/interface.cairo +++ b/packages/token/src/erc20/extensions/erc4626/interface.cairo @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.17.0 (token/erc20/extensions/erc4626/interface.cairo) +// OpenZeppelin Contracts for Cairo v0.18.0 (token/erc20/extensions/erc4626/interface.cairo) use starknet::ContractAddress; From c343716795f5719bfeea7e95342769df2ce3ef4c Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 21 Nov 2024 12:09:06 -0600 Subject: [PATCH 50/90] add reqs to mul_div, fix spdx --- .../token/src/erc20/extensions/erc4626/erc4626.cairo | 2 +- .../token/src/erc20/extensions/erc4626/interface.cairo | 2 +- packages/utils/src/math.cairo | 9 +++++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo index 39a269958..d1dc05f40 100644 --- a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.18.0 (token/erc20/extensions/erc4626/erc4626.cairo) +// OpenZeppelin Contracts for Cairo v0.19.0 (token/erc20/extensions/erc4626/erc4626.cairo) /// # ERC4626 Component /// diff --git a/packages/token/src/erc20/extensions/erc4626/interface.cairo b/packages/token/src/erc20/extensions/erc4626/interface.cairo index 2aea38136..c941c12c0 100644 --- a/packages/token/src/erc20/extensions/erc4626/interface.cairo +++ b/packages/token/src/erc20/extensions/erc4626/interface.cairo @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.18.0 (token/erc20/extensions/erc4626/interface.cairo) +// OpenZeppelin Contracts for Cairo v0.19.0 (token/erc20/extensions/erc4626/interface.cairo) use starknet::ContractAddress; diff --git a/packages/utils/src/math.cairo b/packages/utils/src/math.cairo index e0e21c8c7..995039295 100644 --- a/packages/utils/src/math.cairo +++ b/packages/utils/src/math.cairo @@ -62,6 +62,11 @@ fn round_up(rounding: Rounding) -> bool { /// Returns the quotient of x * y / denominator and rounds up or down depending on `rounding`. /// Uses `u512_safe_div_rem_by_u256` for precision. +/// +/// Requirements: +/// +/// - `denominator` cannot be zero. +/// - The quotient cannot be greater than u256. pub fn u256_mul_div(x: u256, y: u256, denominator: u256, rounding: Rounding) -> u256 { let (q, r) = _raw_u256_mul_div(x, y, denominator); @@ -73,9 +78,9 @@ pub fn u256_mul_div(x: u256, y: u256, denominator: u256, rounding: Rounding) -> } fn _raw_u256_mul_div(x: u256, y: u256, denominator: u256) -> (u256, u256) { - assert(denominator != 0, 'Math: division by zero'); + assert(denominator != 0, 'mul_div division by zero'); let p = x.wide_mul(y); let (mut q, r) = u512_safe_div_rem_by_u256(p, denominator.try_into().unwrap()); - let q = q.try_into().expect('Math: quotient > u256'); + let q = q.try_into().expect('mul_div quotient > u256'); (q, r) } From 0e10817c3ea7386d805d08f9184305ade92dae67 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 21 Nov 2024 12:09:26 -0600 Subject: [PATCH 51/90] remove duplicate erc4626 in votes --- packages/governance/src/votes/erc4626.cairo | 9 - .../src/votes/erc4626/erc4626.cairo | 548 ------------------ .../src/votes/erc4626/interface.cairo | 243 -------- 3 files changed, 800 deletions(-) delete mode 100644 packages/governance/src/votes/erc4626.cairo delete mode 100644 packages/governance/src/votes/erc4626/erc4626.cairo delete mode 100644 packages/governance/src/votes/erc4626/interface.cairo diff --git a/packages/governance/src/votes/erc4626.cairo b/packages/governance/src/votes/erc4626.cairo deleted file mode 100644 index c0b2bc208..000000000 --- a/packages/governance/src/votes/erc4626.cairo +++ /dev/null @@ -1,9 +0,0 @@ -pub mod erc4626; -pub mod interface; -pub use erc4626::DefaultConfig; - -pub use erc4626::ERC4626Component; -pub use erc4626::ERC4626DefaultLimits; -pub use erc4626::ERC4626DefaultNoFees; -pub use erc4626::ERC4626HooksEmptyImpl; -pub use interface::IERC4626; diff --git a/packages/governance/src/votes/erc4626/erc4626.cairo b/packages/governance/src/votes/erc4626/erc4626.cairo deleted file mode 100644 index 6a794bbe9..000000000 --- a/packages/governance/src/votes/erc4626/erc4626.cairo +++ /dev/null @@ -1,548 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.18.0 (token/erc20/extensions/erc4626/erc4626.cairo) - -/// # ERC4626 Component -/// -/// ADD ME -#[starknet::component] -pub mod ERC4626Component { - use core::num::traits::{Bounded, Zero}; - use crate::erc20::ERC20Component::InternalImpl as ERC20InternalImpl; - use crate::erc20::ERC20Component; - use crate::erc20::extensions::erc4626::interface::IERC4626; - use crate::erc20::interface::{IERC20, IERC20Metadata}; - use crate::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; - use openzeppelin_utils::math::Rounding; - use openzeppelin_utils::math; - use starknet::ContractAddress; - use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; - - // The default values are only used when the DefaultConfig - // is in scope in the implementing contract. - pub const DEFAULT_UNDERLYING_DECIMALS: u8 = 18; - pub const DEFAULT_DECIMALS_OFFSET: u8 = 0; - - #[storage] - pub struct Storage { - ERC4626_asset: ContractAddress - } - - #[event] - #[derive(Drop, PartialEq, starknet::Event)] - pub enum Event { - Deposit: Deposit, - Withdraw: Withdraw, - } - - #[derive(Drop, PartialEq, starknet::Event)] - pub struct Deposit { - #[key] - pub sender: ContractAddress, - #[key] - pub owner: ContractAddress, - pub assets: u256, - pub shares: u256 - } - - #[derive(Drop, PartialEq, starknet::Event)] - pub struct Withdraw { - #[key] - pub sender: ContractAddress, - #[key] - pub receiver: ContractAddress, - #[key] - pub owner: ContractAddress, - pub assets: u256, - pub shares: u256 - } - - pub mod Errors { - pub const EXCEEDED_MAX_DEPOSIT: felt252 = 'ERC4626: exceeds max deposit'; - pub const EXCEEDED_MAX_MINT: felt252 = 'ERC4626: exceeds max mint'; - pub const EXCEEDED_MAX_WITHDRAW: felt252 = 'ERC4626: exceeds max withdraw'; - pub const EXCEEDED_MAX_REDEEM: felt252 = 'ERC4626: exceeds max redeem'; - pub const TOKEN_TRANSFER_FAILED: felt252 = 'ERC4626: token transfer failed'; - pub const INVALID_ASSET_ADDRESS: felt252 = 'ERC4626: asset address set to 0'; - pub const DECIMALS_OVERFLOW: felt252 = 'ERC4626: decimals overflow'; - } - - /// Constants expected to be defined at the contract level used to configure the component - /// behaviour. - /// - /// ADD ME... - pub trait ImmutableConfig { - const UNDERLYING_DECIMALS: u8; - const DECIMALS_OFFSET: u8; - - fn validate() { - assert( - Bounded::MAX - Self::UNDERLYING_DECIMALS >= Self::DECIMALS_OFFSET, - Errors::DECIMALS_OVERFLOW - ) - } - } - - /// Adjustments for fees expected to be defined on the contract level. - /// Defaults to no entry or exit fees. - /// To transfer fees, this trait needs to be coordinated with ERC4626Component::ERC4626Hooks. - pub trait FeeConfigTrait { - fn adjust_deposit(self: @ComponentState, assets: u256) -> u256 { - assets - } - - fn adjust_mint(self: @ComponentState, shares: u256) -> u256 { - shares - } - - fn adjust_withdraw(self: @ComponentState, assets: u256) -> u256 { - assets - } - - fn adjust_redeem(self: @ComponentState, shares: u256) -> u256 { - shares - } - } - - /// Sets custom limits to the target exchange type and is expected to be defined at the contract - /// level. - pub trait LimitConfigTrait { - fn deposit_limit( - self: @ComponentState, receiver: ContractAddress - ) -> Option:: { - Option::None - } - - fn mint_limit( - self: @ComponentState, receiver: ContractAddress - ) -> Option:: { - Option::None - } - - fn withdraw_limit( - self: @ComponentState, owner: ContractAddress - ) -> Option:: { - Option::None - } - - fn redeem_limit( - self: @ComponentState, owner: ContractAddress - ) -> Option:: { - Option::None - } - } - - /// Allows contracts to hook logic into deposit and withdraw transactions. - /// This is where contracts can transfer fees. - pub trait ERC4626HooksTrait { - fn before_withdraw(ref self: ComponentState, assets: u256, shares: u256) {} - fn after_deposit(ref self: ComponentState, assets: u256, shares: u256) {} - } - - #[embeddable_as(ERC4626Impl)] - impl ERC4626< - TContractState, - +HasComponent, - impl Fee: FeeConfigTrait, - impl Limit: LimitConfigTrait, - impl Hooks: ERC4626HooksTrait, - impl Immutable: ImmutableConfig, - impl ERC20: ERC20Component::HasComponent, - +ERC20Component::ERC20HooksTrait, - +Drop - > of IERC4626> { - /// Returns the address of the underlying token used for the Vault for accounting, - /// depositing, and withdrawing. - fn asset(self: @ComponentState) -> ContractAddress { - self.ERC4626_asset.read() - } - - /// Returns the total amount of the underlying asset that is “managed” by Vault. - fn total_assets(self: @ComponentState) -> u256 { - let this = starknet::get_contract_address(); - IERC20Dispatcher { contract_address: self.ERC4626_asset.read() }.balance_of(this) - } - - /// Returns the amount of shares that the Vault would exchange for the amount of assets - /// provided, in an ideal scenario where all the conditions are met. - fn convert_to_shares(self: @ComponentState, assets: u256) -> u256 { - self._convert_to_shares(assets, Rounding::Floor) - } - - /// Returns the amount of assets that the Vault would exchange for the amount of shares - /// provided, in an ideal scenario where all the conditions are met. - fn convert_to_assets(self: @ComponentState, shares: u256) -> u256 { - self._convert_to_assets(shares, Rounding::Floor) - } - - /// Returns the maximum amount of the underlying asset that can be deposited into the Vault - /// for the receiver, through a deposit call. - /// If the `LimitConfigTrait` is not defined for deposits, returns 2 ** 256 - 1. - fn max_deposit(self: @ComponentState, receiver: ContractAddress) -> u256 { - match Limit::deposit_limit(self, receiver) { - Option::Some(limit) => limit, - Option::None => Bounded::MAX - } - } - - /// Allows an on-chain or off-chain user to simulate the effects of their deposit at the - /// current block, given current on-chain conditions. - /// If the `FeeConfigTrait` is not defined for deposits, returns the full amount of shares. - fn preview_deposit(self: @ComponentState, assets: u256) -> u256 { - let adjusted_assets = Fee::adjust_deposit(self, assets); - self._convert_to_shares(adjusted_assets, Rounding::Floor) - } - - /// Mints Vault shares to `receiver` by depositing exactly `assets` of underlying tokens. - /// Returns the amount of newly-minted shares. - /// - /// Requirements: - /// - `assets` is less than or equal to the max deposit amount for `receiver`. - /// - /// Emits a `Deposit` event. - fn deposit( - ref self: ComponentState, assets: u256, receiver: ContractAddress - ) -> u256 { - let max_assets = self.max_deposit(receiver); - assert(assets <= max_assets, Errors::EXCEEDED_MAX_DEPOSIT); - - let shares = self.preview_deposit(assets); - let caller = starknet::get_caller_address(); - self._deposit(caller, receiver, assets, shares); - - shares - } - - /// Returns the maximum amount of the Vault shares that can be minted for `receiver` through - /// a `mint` call. - /// If the `LimitConfigTrait` is not defined for mints, returns 2 ** 256 - 1. - fn max_mint(self: @ComponentState, receiver: ContractAddress) -> u256 { - match Limit::mint_limit(self, receiver) { - Option::Some(limit) => limit, - Option::None => Bounded::MAX - } - } - - /// Allows an on-chain or off-chain user to simulate the effects of their mint at the - /// current block, given current on-chain conditions. - /// If the `FeeConfigTrait` is not defined for mints, returns the full amount of assets. - fn preview_mint(self: @ComponentState, shares: u256) -> u256 { - let raw_amount = self._convert_to_assets(shares, Rounding::Ceil); - Fee::adjust_mint(self, raw_amount) - } - - /// Mints exactly Vault `shares` to `receiver` by depositing amount of underlying tokens. - /// Returns the amount deposited assets. - /// - /// Requirements: - /// - `shares` is less than or equal to the max shares amount for `receiver`. - /// - /// Emits a `Deposit` event. - fn mint( - ref self: ComponentState, shares: u256, receiver: ContractAddress - ) -> u256 { - let max_shares = self.max_mint(receiver); - assert(shares <= max_shares, Errors::EXCEEDED_MAX_MINT); - - let assets = self.preview_mint(shares); - let caller = starknet::get_caller_address(); - self._deposit(caller, receiver, assets, shares); - - assets - } - - /// Returns the maximum amount of the underlying asset that can be withdrawn from the owner - /// balance in the Vault, through a `withdraw` call. - /// If the `LimitConfigTrait` is not defined for withdraws, returns the full balance of - /// assets for `owner` (converted to shares). - fn max_withdraw(self: @ComponentState, owner: ContractAddress) -> u256 { - match Limit::withdraw_limit(self, owner) { - Option::Some(limit) => limit, - Option::None => { - let erc20_component = get_dep_component!(self, ERC20); - let owner_bal = erc20_component.balance_of(owner); - self._convert_to_assets(owner_bal, Rounding::Floor) - } - } - } - - /// Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the - /// current block, given current on-chain conditions. - /// If the `FeeConfigTrait` is not defined for withdraws, returns the full amount of shares. - fn preview_withdraw(self: @ComponentState, assets: u256) -> u256 { - let adjusted_assets = Fee::adjust_withdraw(self, assets); - self._convert_to_shares(adjusted_assets, Rounding::Ceil) - } - - /// Burns shares from `owner` and sends exactly `assets` of underlying tokens to `receiver`. - /// - /// Requirements: - /// - `assets` is less than or equal to the max withdraw amount of `owner`. - /// - /// Emits a `Withdraw` event. - fn withdraw( - ref self: ComponentState, - assets: u256, - receiver: ContractAddress, - owner: ContractAddress - ) -> u256 { - let max_assets = self.max_withdraw(owner); - assert(assets <= max_assets, Errors::EXCEEDED_MAX_WITHDRAW); - - let shares = self.preview_withdraw(assets); - let caller = starknet::get_caller_address(); - self._withdraw(caller, receiver, owner, assets, shares); - - shares - } - - /// Returns the maximum amount of Vault shares that can be redeemed from the owner balance - /// in the Vault, through a `redeem` call. - /// If the `LimitConfigTrait` is not defined for redeems, returns the full balance of assets - /// for `owner`. - fn max_redeem(self: @ComponentState, owner: ContractAddress) -> u256 { - match Limit::redeem_limit(self, owner) { - Option::Some(limit) => limit, - Option::None => { - let erc20_component = get_dep_component!(self, ERC20); - erc20_component.balance_of(owner) - } - } - } - - /// Allows an on-chain or off-chain user to simulate the effects of their redeemption at the - /// current block, given current on-chain conditions. - /// If the `FeeConfigTrait` is not defined for redeems, returns the full amount of assets. - fn preview_redeem(self: @ComponentState, shares: u256) -> u256 { - let raw_amount = self._convert_to_assets(shares, Rounding::Floor); - Fee::adjust_redeem(self, raw_amount) - } - - /// Burns exactly `shares` from `owner` and sends assets of underlying tokens to `receiver`. - /// - /// Requirements: - /// - `shares` is less than or equal to the max redeem amount of `owner`. - /// - /// Emits a `Withdraw` event. - fn redeem( - ref self: ComponentState, - shares: u256, - receiver: ContractAddress, - owner: ContractAddress - ) -> u256 { - let max_shares = self.max_redeem(owner); - assert(shares <= max_shares, Errors::EXCEEDED_MAX_REDEEM); - - let assets = self.preview_redeem(shares); - let caller = starknet::get_caller_address(); - self._withdraw(caller, receiver, owner, assets, shares); - - assets - } - } - - #[embeddable_as(ERC4626MetadataImpl)] - impl ERC4626Metadata< - TContractState, - +HasComponent, - impl Immutable: ImmutableConfig, - impl ERC20: ERC20Component::HasComponent, - > of IERC20Metadata> { - /// Returns the name of the token. - fn name(self: @ComponentState) -> ByteArray { - let erc20_component = get_dep_component!(self, ERC20); - erc20_component.ERC20_name.read() - } - - /// Returns the ticker symbol of the token, usually a shorter version of the name. - fn symbol(self: @ComponentState) -> ByteArray { - let erc20_component = get_dep_component!(self, ERC20); - erc20_component.ERC20_symbol.read() - } - - /// Returns the cumulative number of decimals which includes both the underlying and offset - /// decimals. - /// Both of which must be defined in the `ImmutableConfig` inside the implementing contract. - fn decimals(self: @ComponentState) -> u8 { - Immutable::UNDERLYING_DECIMALS + Immutable::DECIMALS_OFFSET - } - } - - #[generate_trait] - pub impl InternalImpl< - TContractState, - +HasComponent, - impl Hooks: ERC4626HooksTrait, - impl Immutable: ImmutableConfig, - impl ERC20: ERC20Component::HasComponent, - +FeeConfigTrait, - +LimitConfigTrait, - +ERC20Component::ERC20HooksTrait, - +Drop - > of InternalTrait { - /// Validates the `ImmutableConfig` constants and sets the `asset_address` to the vault. - /// This should be set in the contract's constructor. - /// - /// Requirements: - /// - `asset_address` cannot be the zero address. - fn initializer(ref self: ComponentState, asset_address: ContractAddress) { - ImmutableConfig::validate(); - assert(!asset_address.is_zero(), Errors::INVALID_ASSET_ADDRESS); - self.ERC4626_asset.write(asset_address); - } - - /// Business logic for `deposit` and `mint`. - /// Transfers `assets` from `caller` to the Vault contract then mints `shares` to - /// `receiver`. - /// Fees can be transferred in the `ERC4626Hooks::after_deposit` hook which is executed - /// after the business logic. - /// - /// Requirements: - /// - `ERC20::transfer_from` must return true. - /// - /// Emits two `ERC20::Transfer` events (`ERC20::mint` and `ERC20::transfer_from`). - /// Emits a `Deposit` event. - fn _deposit( - ref self: ComponentState, - caller: ContractAddress, - receiver: ContractAddress, - assets: u256, - shares: u256 - ) { - // Transfer assets first - let this = starknet::get_contract_address(); - let asset_dispatcher = IERC20Dispatcher { contract_address: self.ERC4626_asset.read() }; - assert( - asset_dispatcher.transfer_from(caller, this, assets), Errors::TOKEN_TRANSFER_FAILED - ); - - // Mint shares after transferring assets - let mut erc20_component = get_dep_component_mut!(ref self, ERC20); - erc20_component.mint(receiver, shares); - self.emit(Deposit { sender: caller, owner: receiver, assets, shares }); - - // After deposit hook - Hooks::after_deposit(ref self, assets, shares); - } - - /// Business logic for `withdraw` and `redeem`. - /// Burns `shares` from `owner` and then transfers `assets` to `receiver`. - /// Fees can be transferred in the `ERC4626Hooks::before_withdraw` hook which is executed - /// before the business logic. - /// - /// Requirements: - /// - `ERC20::transfer` must return true. - /// - /// Emits two `ERC20::Transfer` events (`ERC20::burn` and `ERC20::transfer`). - /// Emits a `Withdraw` event. - fn _withdraw( - ref self: ComponentState, - caller: ContractAddress, - receiver: ContractAddress, - owner: ContractAddress, - assets: u256, - shares: u256 - ) { - // Before withdraw hook - Hooks::before_withdraw(ref self, assets, shares); - - // Burn shares first - let mut erc20_component = get_dep_component_mut!(ref self, ERC20); - if (caller != owner) { - erc20_component._spend_allowance(owner, caller, shares); - } - erc20_component.burn(owner, shares); - - // Transfer assets after burn - let asset_dispatcher = IERC20Dispatcher { contract_address: self.ERC4626_asset.read() }; - assert(asset_dispatcher.transfer(receiver, assets), Errors::TOKEN_TRANSFER_FAILED); - - self.emit(Withdraw { sender: caller, receiver, owner, assets, shares }); - } - - /// Internal conversion function (from assets to shares) with support for `rounding` - /// direction. - fn _convert_to_shares( - self: @ComponentState, assets: u256, rounding: Rounding - ) -> u256 { - let mut erc20_component = get_dep_component!(self, ERC20); - let total_supply = erc20_component.total_supply(); - - math::u256_mul_div( - assets, - total_supply + math::power(10, Immutable::DECIMALS_OFFSET.into()), - self.total_assets() + 1, - rounding - ) - } - - /// Internal conversion function (from shares to assets) with support for `rounding` - /// direction. - fn _convert_to_assets( - self: @ComponentState, shares: u256, rounding: Rounding - ) -> u256 { - let mut erc20_component = get_dep_component!(self, ERC20); - let total_supply = erc20_component.total_supply(); - - math::u256_mul_div( - shares, - self.total_assets() + 1, - total_supply + math::power(10, Immutable::DECIMALS_OFFSET.into()), - rounding - ) - } - } -} - -/// -/// Default (empty) traits -/// - -pub impl ERC4626HooksEmptyImpl< - TContractState -> of ERC4626Component::ERC4626HooksTrait {} -pub impl ERC4626DefaultNoFees of ERC4626Component::FeeConfigTrait {} -pub impl ERC4626DefaultLimits< - TContractState -> of ERC4626Component::LimitConfigTrait {} - -/// Implementation of the default `ERC4626Component::ImmutableConfig`. -/// -/// See -/// https://github.com/starknet-io/SNIPs/blob/963848f0752bde75c7087c2446d83b7da8118b25/SNIPS/snip-107.md#defaultconfig-implementation -/// -/// The default underlying decimals is set to `18`. -/// The default decimals offset is set to `0`. -pub impl DefaultConfig of ERC4626Component::ImmutableConfig { - const UNDERLYING_DECIMALS: u8 = ERC4626Component::DEFAULT_UNDERLYING_DECIMALS; - const DECIMALS_OFFSET: u8 = ERC4626Component::DEFAULT_DECIMALS_OFFSET; -} - -#[cfg(test)] -mod Test { - use openzeppelin_test_common::mocks::erc4626::ERC4626Mock; - use super::ERC4626Component::InternalImpl; - use super::ERC4626Component; - use super::ERC4626DefaultLimits; - use super::ERC4626DefaultNoFees; - - type ComponentState = ERC4626Component::ComponentState; - - fn COMPONENT_STATE() -> ComponentState { - ERC4626Component::component_state_for_testing() - } - - // Invalid decimals - impl InvalidImmutableConfig of ERC4626Component::ImmutableConfig { - const UNDERLYING_DECIMALS: u8 = 255; - const DECIMALS_OFFSET: u8 = 1; - } - - #[test] - #[should_panic(expected: 'ERC4626: decimals overflow')] - fn test_initializer_invalid_config_panics() { - let mut state = COMPONENT_STATE(); - let asset = starknet::contract_address_const::<'ASSET'>(); - - state.initializer(asset); - } -} diff --git a/packages/governance/src/votes/erc4626/interface.cairo b/packages/governance/src/votes/erc4626/interface.cairo deleted file mode 100644 index 2aea38136..000000000 --- a/packages/governance/src/votes/erc4626/interface.cairo +++ /dev/null @@ -1,243 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.18.0 (token/erc20/extensions/erc4626/interface.cairo) - -use starknet::ContractAddress; - -#[starknet::interface] -pub trait IERC4626 { - /// Returns the address of the underlying token used for the Vault for accounting, depositing, - /// and withdrawing. - /// - /// MUST be an ERC20 token contract. - /// MUST NOT panic. - fn asset(self: @TState) -> ContractAddress; - /// Returns the total amount of the underlying asset that is “managed” by Vault. - /// - /// SHOULD include any compounding that occurs from yield. - /// MUST be inclusive of any fees that are charged against assets in the Vault. - /// MUST NOT panic. - fn total_assets(self: @TState) -> u256; - /// Returns the amount of shares that the Vault would exchange for the amount of assets - /// provided, in an ideal scenario where all the conditions are met. - /// - /// MUST NOT be inclusive of any fees that are charged against assets in the Vault. - /// MUST NOT show any variations depending on the caller. - /// MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange. - /// MUST NOT panic. - fn convert_to_shares(self: @TState, assets: u256) -> u256; - /// Returns the amount of assets that the Vault would exchange for the amount of shares - /// provided, in an ideal scenario where all the conditions are met. - /// - /// MUST NOT be inclusive of any fees that are charged against assets in the Vault. - /// MUST NOT show any variations depending on the caller. - /// MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange. - /// MUST NOT panic. - /// - /// Note that this calculation MAY NOT reflect the “per-user” price-per-share, and instead - /// should reflect the “average-user’s” price-per-share, meaning what the average user - /// should expect to see when exchanging to and from. - fn convert_to_assets(self: @TState, shares: u256) -> u256; - /// Returns the maximum amount of the underlying asset that can be deposited into the Vault for - /// `receiver`, through a deposit call. - /// - /// MUST return a limited value if receiver is subject to some deposit limit. - /// MUST return 2 ** 256 - 1 if there is no limit on the maximum amount of assets that may be - /// deposited. - /// MUST NOT panic. - /// - /// Note that this calculation MAY NOT reflect the “per-user” price-per-share, and instead - /// should reflect the “average-user’s” price-per-share, meaning what the average user - /// should expect to see when exchanging to and from. - fn max_deposit(self: @TState, receiver: ContractAddress) -> u256; - /// Allows an on-chain or off-chain user to simulate the effects of their deposit at the current - /// block, given current on-chain conditions. - /// - /// MUST return as close to and no more than the exact amount of Vault shares that would be - /// minted in a deposit call in the same transaction i.e. deposit should return the same or more - /// shares as `preview_deposit` if called in the same transaction. - /// MUST NOT account for deposit limits like those returned from `max_deposit` and should always - /// act as though the deposit would be accepted, regardless if the user has enough tokens - /// approved, etc. - /// MUST be inclusive of deposit fees. Integrators should be aware of the existence of deposit - /// fees. - /// MUST NOT panic. - /// - /// Note that any unfavorable discrepancy between `convert_to_shares` and `preview_deposit` - /// SHOULD be considered slippage in share price or some other type of condition, meaning the - /// depositor will lose assets by depositing. - fn preview_deposit(self: @TState, assets: u256) -> u256; - /// Mints Vault shares to `receiver` by depositing exactly amount of `assets`. - /// - /// MUST emit the Deposit event. - /// MAY support an additional flow in which the underlying tokens are owned by the Vault - /// contract before the deposit execution, and are accounted for during deposit. - /// MUST panic if all of assets cannot be deposited (due to deposit limit being reached, - /// slippage, the user not approving enough underlying tokens to the Vault contract, etc). - /// - /// Note that most implementations will require pre-approval of the Vault with the Vault’s - /// underlying asset token. - fn deposit(ref self: TState, assets: u256, receiver: ContractAddress) -> u256; - /// Returns the maximum amount of the Vault shares that can be minted for the receiver, through - /// a mint call. - /// - /// MUST return a limited value if receiver is subject to some mint limit. - /// MUST return 2 ** 256 - 1 if there is no limit on the maximum amount of shares that may be - /// minted. - /// MUST NOT panic. - fn max_mint(self: @TState, receiver: ContractAddress) -> u256; - /// Allows an on-chain or off-chain user to simulate the effects of their mint at the current - /// block, given current on-chain conditions. - /// - /// MUST return as close to and no fewer than the exact amount of assets that would be deposited - /// in a `mint` call in the same transaction. I.e. `mint` should return the same or fewer assets - /// as `preview_mint` if called in the same transaction. - /// MUST NOT account for mint limits like those returned from `max_mint` and should always act - /// as though the mint would be accepted, regardless if the user has enough tokens approved, - /// etc. - /// MUST be inclusive of deposit fees. Integrators should be aware of the existence of deposit - /// fees. - /// MUST NOT panic. - /// - /// NOTE: Any unfavorable discrepancy between convertToAssets and previewMint SHOULD be - /// considered slippage in share price or some other type of condition, meaning the depositor - /// will lose assets by minting. - /// - /// Note that any unfavorable discrepancy between `convert_to_assets` and `preview_mint` SHOULD - /// be considered slippage in share price or some other type of condition, meaning the depositor - /// will lose assets by minting. - fn preview_mint(self: @TState, shares: u256) -> u256; - /// Mints exactly shares Vault shares to receiver by depositing amount of underlying tokens. - /// - /// MUST emit the `Deposit` event. - /// MAY support an additional flow in which the underlying tokens are owned by the Vault - /// contract before the mint execution, and are accounted for during mint. - /// MUST panic if all of shares cannot be minted (due to deposit limit being reached, slippage, - /// the user not approving enough underlying tokens to the Vault contract, etc). - /// - /// Note that most implementations will require pre-approval of the Vault with the Vault’s - /// underlying asset token. - fn mint(ref self: TState, shares: u256, receiver: ContractAddress) -> u256; - /// Returns the maximum amount of the underlying asset that can be withdrawn from the owner - /// balance in the Vault, through a withdraw call. - /// - /// MUST return a limited value if owner is subject to some withdrawal limit or timelock. - /// MUST NOT panic. - fn max_withdraw(self: @TState, owner: ContractAddress) -> u256; - /// Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the - /// current block, given current on-chain conditions. - /// - /// MUST return as close to and no fewer than the exact amount of Vault shares that would be - /// burned in a withdraw call in the same transaction i.e. withdraw should return the same or - /// fewer shares as preview_withdraw if called in the same transaction. - /// MUST NOT account for withdrawal limits like those returned from max_withdraw and should - /// always act as though the withdrawal would be accepted, regardless if the user has enough - /// shares, etc. - /// MUST be inclusive of withdrawal fees. Integrators should be aware of the existence of - /// withdrawal fees. - /// MUST not panic. - /// - /// Note that any unfavorable discrepancy between `convert_to_shares` and `preview_withdraw` - /// SHOULD be considered slippage in share price or some other type of condition, meaning the - /// depositor will lose assets by depositing. - fn preview_withdraw(self: @TState, assets: u256) -> u256; - /// Burns shares from owner and sends exactly assets of underlying tokens to receiver. - /// - /// MUST emit the `Withdraw` event. - /// MAY support an additional flow in which the underlying tokens are owned by the Vault - /// contract before the withdraw execution, and are accounted for during withdraw. - /// MUST revert if all of assets cannot be withdrawn (due to withdrawal limit being reached, - /// slippage, the owner not having enough shares, etc). - /// - /// Note that some implementations will require pre-requesting to the Vault before a withdrawal - /// may be performed. - /// Those methods should be performed separately. - fn withdraw( - ref self: TState, assets: u256, receiver: ContractAddress, owner: ContractAddress - ) -> u256; - /// Returns the maximum amount of Vault shares that can be redeemed from the owner balance in - /// the Vault, through a redeem call. - /// - /// MUST return a limited value if owner is subject to some withdrawal limit or timelock. - /// MUST return `ERC20::balance_of(owner)` if `owner` is not subject to any withdrawal limit or - /// timelock. - /// MUST NOT panic. - fn max_redeem(self: @TState, owner: ContractAddress) -> u256; - /// Allows an on-chain or off-chain user to simulate the effects of their redeemption at the - /// current block, given current on-chain conditions. - /// - /// MUST return as close to and no more than the exact amount of assets that would be withdrawn - /// in a redeem call in the same transaction i.e. redeem should return the same or more assets - /// as preview_redeem if called in the same transaction. - /// MUST NOT account for redemption limits like those returned from max_redeem and should always - /// act as though the redemption would be accepted, regardless if the user has enough shares, - /// etc. - /// MUST be inclusive of withdrawal fees. Integrators should be aware of the existence of - /// withdrawal fees. - /// MUST NOT panic. - /// - /// Note any unfavorable discrepancy between `convert_to_assets` and `preview_redeem` SHOULD be - /// considered slippage in share price or some other type of condition, meaning the depositor - /// will lose assets by redeeming. - fn preview_redeem(self: @TState, shares: u256) -> u256; - /// Burns exactly shares from owner and sends assets of underlying tokens to receiver. - /// - /// MUST emit the `Withdraw` event. - /// MAY support an additional flow in which the underlying tokens are owned by the Vault - /// contract before the redeem execution, and are accounted for during redeem. - /// MUST revert if all of shares cannot be redeemed (due to withdrawal limit being reached, - /// slippage, the owner not having enough shares, etc). - /// - /// Note some implementations will require pre-requesting to the Vault before a withdrawal may - /// be performed. - /// Those methods should be performed separately. - fn redeem( - ref self: TState, shares: u256, receiver: ContractAddress, owner: ContractAddress - ) -> u256; -} - -#[starknet::interface] -pub trait ERC4626ABI { - // IERC4626 - fn asset(self: @TState) -> ContractAddress; - fn total_assets(self: @TState) -> u256; - fn convert_to_shares(self: @TState, assets: u256) -> u256; - fn convert_to_assets(self: @TState, shares: u256) -> u256; - fn max_deposit(self: @TState, receiver: ContractAddress) -> u256; - fn preview_deposit(self: @TState, assets: u256) -> u256; - fn deposit(ref self: TState, assets: u256, receiver: ContractAddress) -> u256; - fn max_mint(self: @TState, receiver: ContractAddress) -> u256; - fn preview_mint(self: @TState, shares: u256) -> u256; - fn mint(ref self: TState, shares: u256, receiver: ContractAddress) -> u256; - fn max_withdraw(self: @TState, owner: ContractAddress) -> u256; - fn preview_withdraw(self: @TState, assets: u256) -> u256; - fn withdraw( - ref self: TState, assets: u256, receiver: ContractAddress, owner: ContractAddress - ) -> u256; - fn max_redeem(self: @TState, owner: ContractAddress) -> u256; - fn preview_redeem(self: @TState, shares: u256) -> u256; - fn redeem( - ref self: TState, shares: u256, receiver: ContractAddress, owner: ContractAddress - ) -> u256; - - // IERC20 - fn total_supply(self: @TState) -> u256; - fn balance_of(self: @TState, account: ContractAddress) -> u256; - fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress) -> u256; - fn transfer(ref self: TState, recipient: ContractAddress, amount: u256) -> bool; - fn transfer_from( - ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 - ) -> bool; - fn approve(ref self: TState, spender: ContractAddress, amount: u256) -> bool; - - // IERC20Metadata - fn name(self: @TState) -> ByteArray; - fn symbol(self: @TState) -> ByteArray; - fn decimals(self: @TState) -> u8; - - // IERC20CamelOnly - fn totalSupply(self: @TState) -> u256; - fn balanceOf(self: @TState, account: ContractAddress) -> u256; - fn transferFrom( - ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 - ) -> bool; -} From cbdd70add8f0eddb36142cd1b6dc44c5f60e6b11 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 21 Nov 2024 15:08:43 -0600 Subject: [PATCH 52/90] add/fix in-code docs --- .../erc20/extensions/erc4626/erc4626.cairo | 52 ++++++++++++++++--- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo index d1dc05f40..064022773 100644 --- a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -3,7 +3,31 @@ /// # ERC4626 Component /// -/// ADD MEEEEEEEEEEEEEEEEE AHHHH +/// The ERC4626 component is an extension of ERC20 and provides an implementation of the IERC4626 +/// interface which allows the minting and burning of "shares" in exchange for an underlying "asset." +/// The component leverages traits to configure fees, limits, and decimals. +/// +/// CAUTION: In empty (or nearly empty) ERC-4626 vaults, deposits are at high risk of being stolen through +/// frontrunning with a "donation" to the vault that inflates the price of a share. This is variously known +/// as a donation or inflation attack and is essntially a problem of slippage. Vault deployers can protect +/// against this attack by making an initial deposit of a non-trivial amount of the asset, such that price +/// manipulation becomes infeasible. Withdrawals may similarly be affected by slippage. Users can protect +/// against this attack as well as unexpected slippage in general by verifying the amount received is as +/// expected, using a wrapper that performs these checks. +/// +/// This implementation offers configurable virtual assets and shares to help developers mitigate that risk. +/// `ImmutableConfig::DECIMALS_OFFSET` corresponds to an offset in the decimal representation between the +/// underlying asset's decimals and vault decimals. This offset also determines the rate of virtual shares to +/// virtual assets in the vault, which itself determines the initial exchange rate. While not fully preventing +/// the attack, analysis shows that the default offset (0) makes it non-profitable even if an attacker is able +/// to capture value from multiple user deposits, as a result of the value being captured by the virtual shares +/// (out of the attacker's donation) matching the attacker's expected gains. With a larger offset, the attack +/// becomes orders of magnitude more expensive than it is profitable. +/// +/// The drawback of this approach is that the virtual shares do capture (a very small) part of the value being accrued +/// to the vault. Also, if the vault experiences losses and users try to exit the vault, the virtual shares and assets +/// will cause the first exiting user to experience reduced losses to the detriment to the last users who will +/// experience bigger losses. #[starknet::component] pub mod ERC4626Component { use core::num::traits::{Bounded, Zero}; @@ -34,6 +58,8 @@ pub mod ERC4626Component { Withdraw: Withdraw, } + /// Emitted when `sender` exchanges `assets` for `shares` and transfers those + /// `shares` to `owner`. #[derive(Drop, PartialEq, starknet::Event)] pub struct Deposit { #[key] @@ -44,6 +70,8 @@ pub mod ERC4626Component { pub shares: u256 } + /// Emitted when `sender` exchanges `shares`, owned by `owner`, for `assets` and transfers + /// those `assets` to `receiver`. #[derive(Drop, PartialEq, starknet::Event)] pub struct Withdraw { #[key] @@ -66,10 +94,20 @@ pub mod ERC4626Component { pub const DECIMALS_OVERFLOW: felt252 = 'ERC4626: decimals overflow'; } - /// Constants expected to be defined at the contract level used to configure the component - /// behaviour. + /// Constants expected to be defined at the contract level which configure virtual + /// assets and shares. + /// + /// `UNDERLYING_DECIMALS` should match the underlying asset's decimals. The default + /// value is `18`. + /// + /// `DECIMALS_OFFSET` corresponds to the representational offset between `UNDERLYING_DECIMALS` + /// and the vault decimals. The greater the offset, the more expensive it is for attackers to + /// execute an inflation attack. + /// + /// Requirements: + /// + /// - `UNDERLYING_DECIMALS`+ `DECIMALS_OFFSET` cannot exceed 255 (max u8). /// - /// ADD ME... pub trait ImmutableConfig { const UNDERLYING_DECIMALS: u8; const DECIMALS_OFFSET: u8; @@ -84,7 +122,7 @@ pub mod ERC4626Component { /// Adjustments for fees expected to be defined on the contract level. /// Defaults to no entry or exit fees. - /// To transfer fees, this trait needs to be coordinated with ERC4626Component::ERC4626Hooks. + /// To transfer fees, this trait needs to be coordinated with `ERC4626Component::ERC4626Hooks`. pub trait FeeConfigTrait { fn adjust_deposit(self: @ComponentState, assets: u256) -> u256 { assets @@ -510,8 +548,8 @@ pub impl ERC4626DefaultLimits< /// See /// https://github.com/starknet-io/SNIPs/blob/963848f0752bde75c7087c2446d83b7da8118b25/SNIPS/snip-107.md#defaultconfig-implementation /// -/// The default underlying decimals is set to `18`. -/// The default decimals offset is set to `0`. +/// The default `UNDERLYING_DECIMALS` is set to `18`. +/// The default `DECIMALS_OFFSET` is set to `0`. pub impl DefaultConfig of ERC4626Component::ImmutableConfig { const UNDERLYING_DECIMALS: u8 = ERC4626Component::DEFAULT_UNDERLYING_DECIMALS; const DECIMALS_OFFSET: u8 = ERC4626Component::DEFAULT_DECIMALS_OFFSET; From 970d6214bf819b1c72f793cd50596393d42a7bee Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 22 Nov 2024 10:24:49 -0600 Subject: [PATCH 53/90] fix param name --- packages/test_common/src/mocks/erc4626.cairo | 8 ++++---- .../src/erc20/extensions/erc4626/erc4626.cairo | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/test_common/src/mocks/erc4626.cairo b/packages/test_common/src/mocks/erc4626.cairo index e434f3adf..7c7bd2057 100644 --- a/packages/test_common/src/mocks/erc4626.cairo +++ b/packages/test_common/src/mocks/erc4626.cairo @@ -309,10 +309,10 @@ pub mod ERC4626FeesMock { } fn adjust_mint( - self: @ERC4626Component::ComponentState, shares: u256 + self: @ERC4626Component::ComponentState, assets: u256 ) -> u256 { let contract_state = ERC4626Component::HasComponent::get_contract(self); - contract_state.add_fee_to_mint(shares) + contract_state.add_fee_to_mint(assets) } fn adjust_withdraw( @@ -323,10 +323,10 @@ pub mod ERC4626FeesMock { } fn adjust_redeem( - self: @ERC4626Component::ComponentState, shares: u256 + self: @ERC4626Component::ComponentState, assets: u256 ) -> u256 { let contract_state = ERC4626Component::HasComponent::get_contract(self); - contract_state.remove_fee_from_redeem(shares) + contract_state.remove_fee_from_redeem(assets) } } diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo index 064022773..1b672c763 100644 --- a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -128,16 +128,16 @@ pub mod ERC4626Component { assets } - fn adjust_mint(self: @ComponentState, shares: u256) -> u256 { - shares + fn adjust_mint(self: @ComponentState, assets: u256) -> u256 { + assets } fn adjust_withdraw(self: @ComponentState, assets: u256) -> u256 { assets } - fn adjust_redeem(self: @ComponentState, shares: u256) -> u256 { - shares + fn adjust_redeem(self: @ComponentState, assets: u256) -> u256 { + assets } } @@ -264,8 +264,8 @@ pub mod ERC4626Component { /// current block, given current on-chain conditions. /// If the `FeeConfigTrait` is not defined for mints, returns the full amount of assets. fn preview_mint(self: @ComponentState, shares: u256) -> u256 { - let raw_amount = self._convert_to_assets(shares, Rounding::Ceil); - Fee::adjust_mint(self, raw_amount) + let full_assets = self._convert_to_assets(shares, Rounding::Ceil); + Fee::adjust_mint(self, full_assets) } /// Mints exactly Vault `shares` to `receiver` by depositing amount of underlying tokens. @@ -351,8 +351,8 @@ pub mod ERC4626Component { /// current block, given current on-chain conditions. /// If the `FeeConfigTrait` is not defined for redeems, returns the full amount of assets. fn preview_redeem(self: @ComponentState, shares: u256) -> u256 { - let raw_amount = self._convert_to_assets(shares, Rounding::Floor); - Fee::adjust_redeem(self, raw_amount) + let full_assets = self._convert_to_assets(shares, Rounding::Floor); + Fee::adjust_redeem(self, full_assets) } /// Burns exactly `shares` from `owner` and sends assets of underlying tokens to `receiver`. From 1418543868626a9922f2f0d7a36e035b5da021e6 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 25 Nov 2024 02:03:57 -0600 Subject: [PATCH 54/90] fix comments --- .../erc20/extensions/erc4626/erc4626.cairo | 59 ++++++++++++------- 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo index 1b672c763..902f33d30 100644 --- a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -4,30 +4,32 @@ /// # ERC4626 Component /// /// The ERC4626 component is an extension of ERC20 and provides an implementation of the IERC4626 -/// interface which allows the minting and burning of "shares" in exchange for an underlying "asset." -/// The component leverages traits to configure fees, limits, and decimals. +/// interface which allows the minting and burning of "shares" in exchange for an underlying +/// "asset." The component leverages traits to configure fees, limits, and decimals. /// -/// CAUTION: In empty (or nearly empty) ERC-4626 vaults, deposits are at high risk of being stolen through -/// frontrunning with a "donation" to the vault that inflates the price of a share. This is variously known -/// as a donation or inflation attack and is essntially a problem of slippage. Vault deployers can protect -/// against this attack by making an initial deposit of a non-trivial amount of the asset, such that price -/// manipulation becomes infeasible. Withdrawals may similarly be affected by slippage. Users can protect -/// against this attack as well as unexpected slippage in general by verifying the amount received is as -/// expected, using a wrapper that performs these checks. +/// CAUTION: In empty (or nearly empty) ERC-4626 vaults, deposits are at high risk of being stolen +/// through frontrunning with a "donation" to the vault that inflates the price of a share. This is +/// variously known as a donation or inflation attack and is essentially a problem of slippage. +/// Vault deployers can protect against this attack by making an initial deposit of a non-trivial +/// amount of the asset, such that price manipulation becomes infeasible. Withdrawals may similarly +/// be affected by slippage. Users can protect against this attack as well as unexpected slippage in +/// general by verifying the amount received is as expected, using a wrapper that performs these +/// checks. /// -/// This implementation offers configurable virtual assets and shares to help developers mitigate that risk. -/// `ImmutableConfig::DECIMALS_OFFSET` corresponds to an offset in the decimal representation between the -/// underlying asset's decimals and vault decimals. This offset also determines the rate of virtual shares to -/// virtual assets in the vault, which itself determines the initial exchange rate. While not fully preventing -/// the attack, analysis shows that the default offset (0) makes it non-profitable even if an attacker is able -/// to capture value from multiple user deposits, as a result of the value being captured by the virtual shares -/// (out of the attacker's donation) matching the attacker's expected gains. With a larger offset, the attack -/// becomes orders of magnitude more expensive than it is profitable. +/// This implementation offers configurable virtual assets and shares to help developers mitigate +/// that risk. `ImmutableConfig::DECIMALS_OFFSET` corresponds to an offset in the decimal +/// representation between the underlying asset's decimals and vault decimals. This offset also +/// determines the rate of virtual shares to virtual assets in the vault, which itself determines +/// the initial exchange rate. While not fully preventing the attack, analysis shows that the +/// default offset (0) makes it non-profitable even if an attacker is able to capture value from +/// multiple user deposits, as a result of the value being captured by the virtual shares (out of +/// the attacker's donation) matching the attacker's expected gains. With a larger offset, the +/// attack becomes orders of magnitude more expensive than it is profitable. /// -/// The drawback of this approach is that the virtual shares do capture (a very small) part of the value being accrued -/// to the vault. Also, if the vault experiences losses and users try to exit the vault, the virtual shares and assets -/// will cause the first exiting user to experience reduced losses to the detriment to the last users who will -/// experience bigger losses. +/// The drawback of this approach is that the virtual shares do capture (a very small) part of the +/// value being accrued to the vault. Also, if the vault experiences losses and users try to exit +/// the vault, the virtual shares and assets will cause the first exiting user to experience reduced +/// losses to the detriment to the last users who will experience bigger losses. #[starknet::component] pub mod ERC4626Component { use core::num::traits::{Bounded, Zero}; @@ -176,6 +178,10 @@ pub mod ERC4626Component { fn after_deposit(ref self: ComponentState, assets: u256, shares: u256) {} } + // + // External + // + #[embeddable_as(ERC4626Impl)] impl ERC4626< TContractState, @@ -234,6 +240,7 @@ pub mod ERC4626Component { /// Returns the amount of newly-minted shares. /// /// Requirements: + /// /// - `assets` is less than or equal to the max deposit amount for `receiver`. /// /// Emits a `Deposit` event. @@ -272,6 +279,7 @@ pub mod ERC4626Component { /// Returns the amount deposited assets. /// /// Requirements: + /// /// - `shares` is less than or equal to the max shares amount for `receiver`. /// /// Emits a `Deposit` event. @@ -314,6 +322,7 @@ pub mod ERC4626Component { /// Burns shares from `owner` and sends exactly `assets` of underlying tokens to `receiver`. /// /// Requirements: + /// /// - `assets` is less than or equal to the max withdraw amount of `owner`. /// /// Emits a `Withdraw` event. @@ -358,6 +367,7 @@ pub mod ERC4626Component { /// Burns exactly `shares` from `owner` and sends assets of underlying tokens to `receiver`. /// /// Requirements: + /// /// - `shares` is less than or equal to the max redeem amount of `owner`. /// /// Emits a `Withdraw` event. @@ -405,6 +415,10 @@ pub mod ERC4626Component { } } + // + // Internal + // + #[generate_trait] pub impl InternalImpl< TContractState, @@ -421,6 +435,7 @@ pub mod ERC4626Component { /// This should be set in the contract's constructor. /// /// Requirements: + /// /// - `asset_address` cannot be the zero address. fn initializer(ref self: ComponentState, asset_address: ContractAddress) { ImmutableConfig::validate(); @@ -435,6 +450,7 @@ pub mod ERC4626Component { /// after the business logic. /// /// Requirements: + /// /// - `ERC20::transfer_from` must return true. /// /// Emits two `ERC20::Transfer` events (`ERC20::mint` and `ERC20::transfer_from`). @@ -468,6 +484,7 @@ pub mod ERC4626Component { /// before the business logic. /// /// Requirements: + /// /// - `ERC20::transfer` must return true. /// /// Emits two `ERC20::Transfer` events (`ERC20::burn` and `ERC20::transfer`). From a10b28cf4ce12ce2b1956b7fd9097300439fe28d Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 25 Nov 2024 02:04:08 -0600 Subject: [PATCH 55/90] add multiple tx tests --- .../src/tests/erc4626/test_erc4626.cairo | 235 ++++++++++++++++++ 1 file changed, 235 insertions(+) diff --git a/packages/token/src/tests/erc4626/test_erc4626.cairo b/packages/token/src/tests/erc4626/test_erc4626.cairo index 44e127662..01b8c0e61 100644 --- a/packages/token/src/tests/erc4626/test_erc4626.cairo +++ b/packages/token/src/tests/erc4626/test_erc4626.cairo @@ -12,6 +12,7 @@ use openzeppelin_test_common::mocks::erc20::Type; use openzeppelin_test_common::mocks::erc20::{ IERC20ReentrantDispatcher, IERC20ReentrantDispatcherTrait }; +use openzeppelin_test_common::mocks::erc4626::ERC4626Mock; use openzeppelin_testing as utils; use openzeppelin_testing::constants::{NAME, SYMBOL, OTHER, RECIPIENT, ZERO, SPENDER}; use openzeppelin_testing::events::EventSpyExt; @@ -20,6 +21,10 @@ use openzeppelin_utils::serde::SerializedAppend; use snforge_std::{cheat_caller_address, CheatSpan, spy_events, EventSpy}; use starknet::{ContractAddress, contract_address_const}; +fn ASSET() -> ContractAddress { + contract_address_const::<'ASSET'>() +} + fn HOLDER() -> ContractAddress { contract_address_const::<'HOLDER'>() } @@ -52,6 +57,16 @@ fn parse_share_offset(share: u256) -> u256 { // Setup // +type ComponentState = ERC4626Component::ComponentState; + +fn COMPONENT_STATE() -> ComponentState { + ERC4626Component::component_state_for_testing() +} + +// +// Dispatchers +// + fn deploy_asset() -> IERC20ReentrantDispatcher { let mut asset_calldata: Array = array![]; asset_calldata.append_serde(NAME()); @@ -164,6 +179,23 @@ fn deploy_vault_limits(asset_address: ContractAddress) -> ERC4626ABIDispatcher { ERC4626ABIDispatcher { contract_address } } +// +// asset +// + +#[test] +fn test_asset() { + let mut state = COMPONENT_STATE(); + + let asset_address = state.asset(); + assert_eq!(asset_address, ZERO()); + + state.initializer(ASSET()); + + let asset_address = state.asset(); + assert_eq!(asset_address, ASSET()); +} + // // Metadata // @@ -1314,6 +1346,209 @@ fn test_output_fees_withdraw() { ); } +// +// Scenario inspired by solmate ERC4626 tests +// + +#[test] +fn test_multiple_txs_part_1() { + let mut asset = deploy_asset(); + let mut vault = deploy_vault(asset.contract_address); + + let alice = contract_address_const::<'alice'>(); + let bob = contract_address_const::<'bob'>(); + + asset.unsafe_mint(alice, 4_000); + asset.unsafe_mint(bob, 7_001); + + cheat_caller_address(asset.contract_address, alice, CheatSpan::TargetCalls(1)); + asset.approve(vault.contract_address, 4_000); + cheat_caller_address(asset.contract_address, bob, CheatSpan::TargetCalls(1)); + asset.approve(vault.contract_address, 7_001); + + // 1. Alice mints 2_000 shares (costs 2_000 tokens) + let mut spy = spy_events(); + cheat_caller_address(vault.contract_address, alice, CheatSpan::TargetCalls(1)); + vault.mint(2_000, alice); + + // Check events + spy.assert_event_approval(asset.contract_address, alice, vault.contract_address, 4_000 - 2_000); + spy.assert_event_transfer(asset.contract_address, alice, vault.contract_address, 2_000); + spy.assert_event_transfer(vault.contract_address, ZERO(), alice, 2_000); + spy.assert_only_event_deposit(vault.contract_address, alice, alice, 2_000, 2_000); + + assert_eq!(vault.preview_deposit(2_000), 2_000); + assert_eq!(vault.balance_of(alice), 2_000); + assert_eq!(vault.balance_of(bob), 0); + assert_eq!(vault.convert_to_assets(vault.balance_of(alice)), 2_000); + assert_eq!(vault.convert_to_assets(vault.balance_of(bob)), 0); + assert_eq!(vault.convert_to_shares(asset.balance_of(vault.contract_address)), 2_000); + assert_eq!(vault.total_supply(), 2_000); + assert_eq!(vault.total_assets(), 2_000); + + // 2. Bob deposits 4_000 tokens (mints 4_000 shares) + cheat_caller_address(vault.contract_address, bob, CheatSpan::TargetCalls(1)); + vault.mint(4_000, bob); + + // Check events + spy.assert_event_approval(asset.contract_address, bob, vault.contract_address, 7_001 - 4_000); + spy.assert_event_transfer(asset.contract_address, bob, vault.contract_address, 4_000); + spy.assert_event_transfer(vault.contract_address, ZERO(), bob, 4_000); + spy.assert_only_event_deposit(vault.contract_address, bob, bob, 4_000, 4_000); + + assert_eq!(vault.preview_deposit(4_000), 4_000); + assert_eq!(vault.balance_of(alice), 2_000); + assert_eq!(vault.balance_of(bob), 4_000); + assert_eq!(vault.convert_to_assets(vault.balance_of(alice)), 2_000); + assert_eq!(vault.convert_to_assets(vault.balance_of(bob)), 4_000); + assert_eq!(vault.convert_to_shares(asset.balance_of(vault.contract_address)), 6_000); + assert_eq!(vault.total_supply(), 6_000); + assert_eq!(vault.total_assets(), 6_000); + + // 3. Vault mutates by +3_000 tokens (simulated yield returned from strategy) + asset.unsafe_mint(vault.contract_address, 3_000); + + assert_eq!(vault.balance_of(alice), 2_000); + assert_eq!(vault.balance_of(bob), 4_000); + // was 3_000, but virtual assets/shares captures part of the yield + assert_eq!(vault.convert_to_assets(vault.balance_of(alice)), 2_999); + // was 6_000, but virtual assets/shares captures part of the yield + assert_eq!(vault.convert_to_assets(vault.balance_of(bob)), 5_999); + assert_eq!(vault.convert_to_shares(asset.balance_of(vault.contract_address)), 6_000); + assert_eq!(vault.total_supply(), 6_000); + assert_eq!(vault.total_assets(), 9_000); + + // 4. Alice deposits 2_000 tokens (mints 1_333 shares) + cheat_caller_address(vault.contract_address, alice, CheatSpan::TargetCalls(1)); + vault.deposit(2_000, alice); + + assert_eq!(vault.balance_of(alice), 3_333); + assert_eq!(vault.balance_of(bob), 4_000); + assert_eq!(vault.convert_to_assets(vault.balance_of(alice)), 4_999); + assert_eq!(vault.convert_to_assets(vault.balance_of(bob)), 6_000); + assert_eq!(vault.convert_to_shares(asset.balance_of(vault.contract_address)), 7_333); + assert_eq!(vault.total_supply(), 7_333); + assert_eq!(vault.total_assets(), 11_000); + + // 5. Bob mints 2_000 shares (costs 3_001 assets) + // NOTE: bob's assets spent rounds toward infinity + // NOTE: alice's vault assets rounds toward infinity + cheat_caller_address(vault.contract_address, bob, CheatSpan::TargetCalls(1)); + vault.mint(2_000, bob); + + assert_eq!(vault.balance_of(alice), 3_333); + assert_eq!(vault.balance_of(bob), 6_000); + assert_eq!(vault.convert_to_assets(vault.balance_of(alice)), 4_999); + assert_eq!(vault.convert_to_assets(vault.balance_of(bob)), 9_000); + assert_eq!(vault.convert_to_shares(asset.balance_of(vault.contract_address)), 9_333); + assert_eq!(vault.total_supply(), 9_333); + assert_eq!(vault.total_assets(), 14_000); + + // 6. Vault mutates by +3_000 tokens + // NOTE: Vault holds 17_001 tokens, but `assets_of` returns 17000. + asset.unsafe_mint(vault.contract_address, 3_000); + + assert_eq!(vault.balance_of(alice), 3_333); + assert_eq!(vault.balance_of(bob), 6_000); + // Was 6_071 + assert_eq!(vault.convert_to_assets(vault.balance_of(alice)), 6_070); + // Was 10_929 + assert_eq!(vault.convert_to_assets(vault.balance_of(bob)), 10_928); + assert_eq!(vault.convert_to_shares(asset.balance_of(vault.contract_address)), 9_333); + assert_eq!(vault.total_supply(), 9_333); + // Was 17_001 + assert_eq!(vault.total_assets(), 17_000); + + // 7. Alice redeems 1_333 shares (2_428 assets) + cheat_caller_address(vault.contract_address, alice, CheatSpan::TargetCalls(1)); + vault.redeem(1_333, alice, alice); + + assert_eq!(vault.balance_of(alice), 2_000); + assert_eq!(vault.balance_of(bob), 6_000); + assert_eq!(vault.convert_to_assets(vault.balance_of(alice)), 3_643); + assert_eq!(vault.convert_to_assets(vault.balance_of(bob)), 10_929); + assert_eq!(vault.convert_to_shares(asset.balance_of(vault.contract_address)), 8_000); + assert_eq!(vault.total_supply(), 8_000); + assert_eq!(vault.total_assets(), 14_573); +} + +#[test] +fn test_multiple_txs_part_2() { + // SNForge hangs, so the test is split in two. + let mut asset = deploy_asset(); + let mut vault = deploy_vault(asset.contract_address); + + let alice = contract_address_const::<'alice'>(); + let bob = contract_address_const::<'bob'>(); + + asset.unsafe_mint(alice, 4_000); + asset.unsafe_mint(bob, 7_001); + + cheat_caller_address(asset.contract_address, alice, CheatSpan::TargetCalls(1)); + asset.approve(vault.contract_address, 4_000); + cheat_caller_address(asset.contract_address, bob, CheatSpan::TargetCalls(1)); + asset.approve(vault.contract_address, 7_001); + + // Recreate state to where it left off from `test_multiple_txs_part_1`. + + // 1. Alice mints 2_000 shares (costs 2_000 tokens) + cheat_caller_address(vault.contract_address, alice, CheatSpan::TargetCalls(1)); + vault.mint(2_000, alice); + // 2. Bob deposits 4_000 tokens (mints 4_000 shares) + cheat_caller_address(vault.contract_address, bob, CheatSpan::TargetCalls(1)); + vault.mint(4_000, bob); + // 3. Vault mutates by +3_000 tokens (simulated yield returned from strategy) + asset.unsafe_mint(vault.contract_address, 3_000); + // 4. Alice deposits 2_000 tokens (mints 1_333 shares) + cheat_caller_address(vault.contract_address, alice, CheatSpan::TargetCalls(1)); + vault.deposit(2_000, alice); + // 5. Bob mints 2_000 shares (costs 3_001 assets) + cheat_caller_address(vault.contract_address, bob, CheatSpan::TargetCalls(1)); + vault.mint(2_000, bob); + // 6. Vault mutates by +3_000 tokens + asset.unsafe_mint(vault.contract_address, 3_000); + // 7. Alice redeems 1_333 shares (2_428 assets) + cheat_caller_address(vault.contract_address, alice, CheatSpan::TargetCalls(1)); + vault.redeem(1_333, alice, alice); + + // 8. Bob withdraws 2_929 assets (1_608 shares) + cheat_caller_address(vault.contract_address, bob, CheatSpan::TargetCalls(1)); + vault.withdraw(2_929, bob, bob); + + assert_eq!(vault.balance_of(alice), 2_000); + assert_eq!(vault.balance_of(bob), 4_392); + assert_eq!(vault.convert_to_assets(vault.balance_of(alice)), 3_643); + assert_eq!(vault.convert_to_assets(vault.balance_of(bob)), 8_000); + assert_eq!(vault.convert_to_shares(asset.balance_of(vault.contract_address)), 6_392); + assert_eq!(vault.total_supply(), 6_392); + assert_eq!(vault.total_assets(), 11_644); + + // 9. Alice withdraws 3_643 assets (2_000 shares) + // NOTE: bob's assets have been rounded back towards infinity + cheat_caller_address(vault.contract_address, alice, CheatSpan::TargetCalls(1)); + vault.withdraw(3_643, alice, alice); + + assert_eq!(vault.balance_of(alice), 0); + assert_eq!(vault.balance_of(bob), 4_392); + assert_eq!(vault.convert_to_assets(vault.balance_of(alice)), 0); + assert_eq!(vault.convert_to_assets(vault.balance_of(bob)), 8_000); + assert_eq!(vault.convert_to_shares(asset.balance_of(vault.contract_address)), 4_392); + assert_eq!(vault.total_supply(), 4_392); + assert_eq!(vault.total_assets(), 8_001); + + // 10. Bob redeems 4_392 shares (8_001) + cheat_caller_address(vault.contract_address, bob, CheatSpan::TargetCalls(1)); + vault.redeem(4_392, bob, bob); + + assert_eq!(vault.balance_of(alice), 0); + assert_eq!(vault.balance_of(bob), 0); + assert_eq!(vault.convert_to_assets(vault.balance_of(alice)), 0); + assert_eq!(vault.convert_to_assets(vault.balance_of(bob)), 0); + assert_eq!(vault.convert_to_shares(asset.balance_of(vault.contract_address)), 0); + assert_eq!(vault.total_supply(), 0); + assert_eq!(vault.total_assets(), 1); +} + // // Assertions/Helpers // From e67d312033cbe814428defd9e3ac51768501db19 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 25 Nov 2024 02:32:45 -0600 Subject: [PATCH 56/90] fix err msgs --- packages/utils/src/tests/test_math.cairo | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/utils/src/tests/test_math.cairo b/packages/utils/src/tests/test_math.cairo index aff4a9429..36532f72d 100644 --- a/packages/utils/src/tests/test_math.cairo +++ b/packages/utils/src/tests/test_math.cairo @@ -86,7 +86,7 @@ fn test_average_u256(a: u256, b: u256) { // #[test] -#[should_panic(expected: 'Math: division by zero')] +#[should_panic(expected: 'mul_div division by zero')] fn test_mul_div_divide_by_zero() { let x = 1; let y = 1; @@ -96,7 +96,7 @@ fn test_mul_div_divide_by_zero() { } #[test] -#[should_panic(expected: 'Math: quotient > u256')] +#[should_panic(expected: 'mul_div quotient > u256')] fn test_mul_div_result_gt_u256() { let x = 5; let y = Bounded::MAX; From ed727df0f350b355a43dcb6afe944df9e04213a8 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 25 Nov 2024 12:38:50 -0600 Subject: [PATCH 57/90] fix comments, fix test --- .../src/tests/erc4626/test_erc4626.cairo | 22 +++++-------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/packages/token/src/tests/erc4626/test_erc4626.cairo b/packages/token/src/tests/erc4626/test_erc4626.cairo index 01b8c0e61..930fa3c5e 100644 --- a/packages/token/src/tests/erc4626/test_erc4626.cairo +++ b/packages/token/src/tests/erc4626/test_erc4626.cairo @@ -1371,12 +1371,6 @@ fn test_multiple_txs_part_1() { cheat_caller_address(vault.contract_address, alice, CheatSpan::TargetCalls(1)); vault.mint(2_000, alice); - // Check events - spy.assert_event_approval(asset.contract_address, alice, vault.contract_address, 4_000 - 2_000); - spy.assert_event_transfer(asset.contract_address, alice, vault.contract_address, 2_000); - spy.assert_event_transfer(vault.contract_address, ZERO(), alice, 2_000); - spy.assert_only_event_deposit(vault.contract_address, alice, alice, 2_000, 2_000); - assert_eq!(vault.preview_deposit(2_000), 2_000); assert_eq!(vault.balance_of(alice), 2_000); assert_eq!(vault.balance_of(bob), 0); @@ -1390,12 +1384,6 @@ fn test_multiple_txs_part_1() { cheat_caller_address(vault.contract_address, bob, CheatSpan::TargetCalls(1)); vault.mint(4_000, bob); - // Check events - spy.assert_event_approval(asset.contract_address, bob, vault.contract_address, 7_001 - 4_000); - spy.assert_event_transfer(asset.contract_address, bob, vault.contract_address, 4_000); - spy.assert_event_transfer(vault.contract_address, ZERO(), bob, 4_000); - spy.assert_only_event_deposit(vault.contract_address, bob, bob, 4_000, 4_000); - assert_eq!(vault.preview_deposit(4_000), 4_000); assert_eq!(vault.balance_of(alice), 2_000); assert_eq!(vault.balance_of(bob), 4_000); @@ -1410,9 +1398,9 @@ fn test_multiple_txs_part_1() { assert_eq!(vault.balance_of(alice), 2_000); assert_eq!(vault.balance_of(bob), 4_000); - // was 3_000, but virtual assets/shares captures part of the yield + // Was 3_000, but virtual assets/shares captures part of the yield assert_eq!(vault.convert_to_assets(vault.balance_of(alice)), 2_999); - // was 6_000, but virtual assets/shares captures part of the yield + // Was 6_000, but virtual assets/shares captures part of the yield assert_eq!(vault.convert_to_assets(vault.balance_of(bob)), 5_999); assert_eq!(vault.convert_to_shares(asset.balance_of(vault.contract_address)), 6_000); assert_eq!(vault.total_supply(), 6_000); @@ -1431,8 +1419,8 @@ fn test_multiple_txs_part_1() { assert_eq!(vault.total_assets(), 11_000); // 5. Bob mints 2_000 shares (costs 3_001 assets) - // NOTE: bob's assets spent rounds toward infinity - // NOTE: alice's vault assets rounds toward infinity + // NOTE: Bob's assets spent rounds toward infinity + // NOTE: Alice's vault assets rounds toward infinity cheat_caller_address(vault.contract_address, bob, CheatSpan::TargetCalls(1)); vault.mint(2_000, bob); @@ -1524,7 +1512,7 @@ fn test_multiple_txs_part_2() { assert_eq!(vault.total_assets(), 11_644); // 9. Alice withdraws 3_643 assets (2_000 shares) - // NOTE: bob's assets have been rounded back towards infinity + // NOTE: Bob's assets have been rounded back towards infinity cheat_caller_address(vault.contract_address, alice, CheatSpan::TargetCalls(1)); vault.withdraw(3_643, alice, alice); From b7fb9de5d1ca982fb542d208ecd2e2042013afca Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 26 Nov 2024 15:38:11 -0600 Subject: [PATCH 58/90] add note to power --- packages/utils/src/math.cairo | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/utils/src/math.cairo b/packages/utils/src/math.cairo index 995039295..b54235cbc 100644 --- a/packages/utils/src/math.cairo +++ b/packages/utils/src/math.cairo @@ -22,7 +22,10 @@ pub fn average< (a & b) + (a ^ b) / 2_u8.into() } -/// ADD MEE +/// TMP. Raises `base` to the power of `exp`. Will panic if the result is greater than 2 ** 256 - 1. +/// +/// NOTE: This should be removed in favor of the corelib's Pow implementation when available. +/// https://github.com/starkware-libs/cairo/pull/6694 pub fn power, +PartialEq, +TryInto, +Into, +Into>( base: T, exp: T ) -> T { From 44055bd64ad8aca1597febb401e701929be18c0f Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 26 Nov 2024 15:39:21 -0600 Subject: [PATCH 59/90] use math:: prefix in tests --- packages/utils/src/tests/test_math.cairo | 27 ++++++++++++------------ 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/packages/utils/src/tests/test_math.cairo b/packages/utils/src/tests/test_math.cairo index 36532f72d..36016bdf1 100644 --- a/packages/utils/src/tests/test_math.cairo +++ b/packages/utils/src/tests/test_math.cairo @@ -2,8 +2,7 @@ use core::integer::{u512, u512_safe_div_rem_by_u256}; use core::num::traits::Bounded; use core::num::traits::OverflowingAdd; use crate::math::Rounding; -use crate::math::average; -use crate::math::u256_mul_div; +use crate::math; // // average @@ -11,7 +10,7 @@ use crate::math::u256_mul_div; #[test] fn test_average_u8(a: u8, b: u8) { - let actual = average(a, b); + let actual = math::average(a, b); let a: u256 = a.into(); let b: u256 = b.into(); @@ -22,7 +21,7 @@ fn test_average_u8(a: u8, b: u8) { #[test] fn test_average_u16(a: u16, b: u16) { - let actual = average(a, b); + let actual = math::average(a, b); let a: u256 = a.into(); let b: u256 = b.into(); @@ -33,7 +32,7 @@ fn test_average_u16(a: u16, b: u16) { #[test] fn test_average_u32(a: u32, b: u32) { - let actual = average(a, b); + let actual = math::average(a, b); let a: u256 = a.into(); let b: u256 = b.into(); @@ -44,7 +43,7 @@ fn test_average_u32(a: u32, b: u32) { #[test] fn test_average_u64(a: u64, b: u64) { - let actual = average(a, b); + let actual = math::average(a, b); let a: u256 = a.into(); let b: u256 = b.into(); @@ -55,7 +54,7 @@ fn test_average_u64(a: u64, b: u64) { #[test] fn test_average_u128(a: u128, b: u128) { - let actual = average(a, b); + let actual = math::average(a, b); let a: u256 = a.into(); let b: u256 = b.into(); @@ -66,7 +65,7 @@ fn test_average_u128(a: u128, b: u128) { #[test] fn test_average_u256(a: u256, b: u256) { - let actual = average(a, b); + let actual = math::average(a, b); let mut expected = 0; let (sum, overflow) = a.overflowing_add(b); @@ -92,7 +91,7 @@ fn test_mul_div_divide_by_zero() { let y = 1; let denominator = 0; - u256_mul_div(x, y, denominator, Rounding::Floor); + math::u256_mul_div(x, y, denominator, Rounding::Floor); } #[test] @@ -102,7 +101,7 @@ fn test_mul_div_result_gt_u256() { let y = Bounded::MAX; let denominator = 2; - u256_mul_div(x, y, denominator, Rounding::Floor); + math::u256_mul_div(x, y, denominator, Rounding::Floor); } #[test] @@ -115,7 +114,7 @@ fn test_mul_div_round_down_small_values() { for round in round_down { for args in args_list { let (x, y, denominator, expected) = args; - assert_eq!(u256_mul_div(*x, *y, *denominator, round), *expected); + assert_eq!(math::u256_mul_div(*x, *y, *denominator, round), *expected); } } } @@ -137,7 +136,7 @@ fn test_mul_div_round_down_large_values() { for round in round_down { for args in args_list { let (x, y, denominator, expected) = args; - assert_eq!(u256_mul_div(*x, *y, *denominator, round), *expected); + assert_eq!(math::u256_mul_div(*x, *y, *denominator, round), *expected); }; }; } @@ -152,7 +151,7 @@ fn test_mul_div_round_up_small_values() { for round in round_up { for args in args_list { let (x, y, denominator, expected) = args; - assert_eq!(u256_mul_div(*x, *y, *denominator, round), *expected); + assert_eq!(math::u256_mul_div(*x, *y, *denominator, round), *expected); } } } @@ -174,7 +173,7 @@ fn test_mul_div_round_up_large_values() { for round in round_up { for args in args_list { let (x, y, denominator, expected) = args; - assert_eq!(u256_mul_div(*x, *y, *denominator, round), *expected); + assert_eq!(math::u256_mul_div(*x, *y, *denominator, round), *expected); }; }; } From 8d8e066603e78394b3aade2047a1933106abc6d1 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 26 Nov 2024 16:33:51 -0600 Subject: [PATCH 60/90] improve u256_mul_div --- packages/utils/src/math.cairo | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/packages/utils/src/math.cairo b/packages/utils/src/math.cairo index b54235cbc..1e43d5421 100644 --- a/packages/utils/src/math.cairo +++ b/packages/utils/src/math.cairo @@ -49,20 +49,6 @@ pub enum Rounding { Expand // Away from zero } -fn cast_rounding(rounding: Rounding) -> u8 { - match rounding { - Rounding::Floor => 0, - Rounding::Ceil => 1, - Rounding::Trunc => 2, - Rounding::Expand => 3 - } -} - -fn round_up(rounding: Rounding) -> bool { - let u8_rounding = cast_rounding(rounding); - u8_rounding % 2 == 1 -} - /// Returns the quotient of x * y / denominator and rounds up or down depending on `rounding`. /// Uses `u512_safe_div_rem_by_u256` for precision. /// @@ -73,17 +59,24 @@ fn round_up(rounding: Rounding) -> bool { pub fn u256_mul_div(x: u256, y: u256, denominator: u256, rounding: Rounding) -> u256 { let (q, r) = _raw_u256_mul_div(x, y, denominator); - // Cast to felts for bitwise op - let is_rounded_up: felt252 = round_up(rounding).into(); - let has_remainder: felt252 = (r > 0).into(); + let is_rounded_up = match rounding { + Rounding::Ceil => 1, + Rounding::Expand => 1, + _ => 0 + }; + + let has_remainder = match r > 0 { + true => 1, + false => 0 + }; - q + (is_rounded_up.into() & has_remainder.into()) + q + (is_rounded_up & has_remainder) } fn _raw_u256_mul_div(x: u256, y: u256, denominator: u256) -> (u256, u256) { - assert(denominator != 0, 'mul_div division by zero'); + let denominator = denominator.try_into().expect('mul_div division by zero'); let p = x.wide_mul(y); - let (mut q, r) = u512_safe_div_rem_by_u256(p, denominator.try_into().unwrap()); + let (mut q, r) = u512_safe_div_rem_by_u256(p, denominator); let q = q.try_into().expect('mul_div quotient > u256'); (q, r) } From 450efa4ad429b7b9bd5d5f9464d6084066f6a0e0 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 26 Nov 2024 21:30:43 -0600 Subject: [PATCH 61/90] remove unused var --- packages/token/src/tests/erc4626/test_erc4626.cairo | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/token/src/tests/erc4626/test_erc4626.cairo b/packages/token/src/tests/erc4626/test_erc4626.cairo index 930fa3c5e..28c73c6a5 100644 --- a/packages/token/src/tests/erc4626/test_erc4626.cairo +++ b/packages/token/src/tests/erc4626/test_erc4626.cairo @@ -1367,7 +1367,6 @@ fn test_multiple_txs_part_1() { asset.approve(vault.contract_address, 7_001); // 1. Alice mints 2_000 shares (costs 2_000 tokens) - let mut spy = spy_events(); cheat_caller_address(vault.contract_address, alice, CheatSpan::TargetCalls(1)); vault.mint(2_000, alice); From d912c6dcf3c17ea436826fdb78805f3129b97681 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 26 Nov 2024 21:33:23 -0600 Subject: [PATCH 62/90] remove line --- packages/test_common/src/mocks/erc20.cairo | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/test_common/src/mocks/erc20.cairo b/packages/test_common/src/mocks/erc20.cairo index 9b2e09df8..798487dc6 100644 --- a/packages/test_common/src/mocks/erc20.cairo +++ b/packages/test_common/src/mocks/erc20.cairo @@ -1,6 +1,5 @@ use starknet::ContractAddress; - #[starknet::contract] pub mod DualCaseERC20Mock { use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; From 0cd10dad710ffc66f3afa52b797815509f2c8a12 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 26 Nov 2024 21:42:10 -0600 Subject: [PATCH 63/90] improve var name --- packages/utils/src/tests/test_math.cairo | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/utils/src/tests/test_math.cairo b/packages/utils/src/tests/test_math.cairo index 36016bdf1..9b32fc2fe 100644 --- a/packages/utils/src/tests/test_math.cairo +++ b/packages/utils/src/tests/test_math.cairo @@ -111,10 +111,10 @@ fn test_mul_div_round_down_small_values() { (3, 4, 5, 2), (3, 5, 5, 3)] .span(); - for round in round_down { + for rounding in round_down { for args in args_list { let (x, y, denominator, expected) = args; - assert_eq!(math::u256_mul_div(*x, *y, *denominator, round), *expected); + assert_eq!(math::u256_mul_div(*x, *y, *denominator, rounding), *expected); } } } @@ -133,10 +133,10 @@ fn test_mul_div_round_down_large_values() { ] .span(); - for round in round_down { + for rounding in round_down { for args in args_list { let (x, y, denominator, expected) = args; - assert_eq!(math::u256_mul_div(*x, *y, *denominator, round), *expected); + assert_eq!(math::u256_mul_div(*x, *y, *denominator, rounding), *expected); }; }; } @@ -148,10 +148,10 @@ fn test_mul_div_round_up_small_values() { (3, 4, 5, 3), (3, 5, 5, 3)] .span(); - for round in round_up { + for rounding in round_up { for args in args_list { let (x, y, denominator, expected) = args; - assert_eq!(math::u256_mul_div(*x, *y, *denominator, round), *expected); + assert_eq!(math::u256_mul_div(*x, *y, *denominator, rounding), *expected); } } } @@ -170,10 +170,10 @@ fn test_mul_div_round_up_large_values() { ] .span(); - for round in round_up { + for rounding in round_up { for args in args_list { let (x, y, denominator, expected) = args; - assert_eq!(math::u256_mul_div(*x, *y, *denominator, round), *expected); + assert_eq!(math::u256_mul_div(*x, *y, *denominator, rounding), *expected); }; }; } From 3ef163ff6a497b5ca3fe4b17c54fd974e405e632 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 29 Nov 2024 02:01:21 -0600 Subject: [PATCH 64/90] add changelog entries --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c9cf83c6..e9f6e8cb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- ERC4626Component (#1170) +- `Math::u256_mul_div` (#1170) - SRC9 (Outside Execution) integration to account presets (#1201) - `SNIP12HashSpanImpl` to `openzeppelin_utils::cryptography::snip12` (#1180) - GovernorComponent with the following extensions: (#1180) From c8b716117236b93e27e71c9360c23071cdac3846 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 29 Nov 2024 16:07:26 -0600 Subject: [PATCH 65/90] fix version in spdx --- packages/token/src/erc20/extensions/erc4626/erc4626.cairo | 2 +- packages/token/src/erc20/extensions/erc4626/interface.cairo | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo index 902f33d30..43d1d3b10 100644 --- a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.19.0 (token/erc20/extensions/erc4626/erc4626.cairo) +// OpenZeppelin Contracts for Cairo v0.20.0-rc.0 (token/erc20/extensions/erc4626/erc4626.cairo) /// # ERC4626 Component /// diff --git a/packages/token/src/erc20/extensions/erc4626/interface.cairo b/packages/token/src/erc20/extensions/erc4626/interface.cairo index c941c12c0..9a0219146 100644 --- a/packages/token/src/erc20/extensions/erc4626/interface.cairo +++ b/packages/token/src/erc20/extensions/erc4626/interface.cairo @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts for Cairo v0.19.0 (token/erc20/extensions/erc4626/interface.cairo) +// OpenZeppelin Contracts for Cairo v0.20.0-rc.0 (token/erc20/extensions/erc4626/interface.cairo) use starknet::ContractAddress; From 1c221a580fa6cea2249b3a2f6a5cd617360935bb Mon Sep 17 00:00:00 2001 From: Andrew Fleming Date: Thu, 5 Dec 2024 02:28:56 -0600 Subject: [PATCH 66/90] Apply suggestions from code review Co-authored-by: Eric Nordelo --- packages/token/src/erc20/extensions/erc4626/erc4626.cairo | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo index 43d1d3b10..69b89ac7d 100644 --- a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -203,7 +203,8 @@ pub mod ERC4626Component { /// Returns the total amount of the underlying asset that is “managed” by Vault. fn total_assets(self: @ComponentState) -> u256 { let this = starknet::get_contract_address(); - IERC20Dispatcher { contract_address: self.ERC4626_asset.read() }.balance_of(this) + let asset_dispatcher = IERC20Dispatcher { contract_address: self.ERC4626_asset.read() }; + asset_dispatcher.balance_of(this) } /// Returns the amount of shares that the Vault would exchange for the amount of assets From 58d26be8fe3dba02184f7c236f76a2dac69a4b29 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 9 Dec 2024 12:45:49 -0600 Subject: [PATCH 67/90] use corelib pow, remove power fn --- .../erc20/extensions/erc4626/erc4626.cairo | 7 +++--- .../src/tests/erc4626/test_erc4626.cairo | 25 +++++++++---------- packages/utils/src/math.cairo | 19 -------------- 3 files changed, 16 insertions(+), 35 deletions(-) diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo index 66d9b388d..d59a91407 100644 --- a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -32,7 +32,7 @@ /// losses to the detriment to the last users who will experience bigger losses. #[starknet::component] pub mod ERC4626Component { - use core::num::traits::{Bounded, Zero}; + use core::num::traits::{Bounded, Pow, Zero}; use crate::erc20::ERC20Component; use crate::erc20::ERC20Component::InternalImpl as ERC20InternalImpl; use crate::erc20::extensions::erc4626::interface::IERC4626; @@ -525,7 +525,8 @@ pub mod ERC4626Component { math::u256_mul_div( assets, - total_supply + math::power(10, Immutable::DECIMALS_OFFSET.into()), + //total_supply + math::power(10, Immutable::DECIMALS_OFFSET.into()), + total_supply + 10_u256.pow(Immutable::DECIMALS_OFFSET.into()), self.total_assets() + 1, rounding, ) @@ -542,7 +543,7 @@ pub mod ERC4626Component { math::u256_mul_div( shares, self.total_assets() + 1, - total_supply + math::power(10, Immutable::DECIMALS_OFFSET.into()), + total_supply + 10_u256.pow(Immutable::DECIMALS_OFFSET.into()), rounding, ) } diff --git a/packages/token/src/tests/erc4626/test_erc4626.cairo b/packages/token/src/tests/erc4626/test_erc4626.cairo index 501eb59f1..49b88d9d1 100644 --- a/packages/token/src/tests/erc4626/test_erc4626.cairo +++ b/packages/token/src/tests/erc4626/test_erc4626.cairo @@ -1,4 +1,4 @@ -use core::num::traits::Bounded; +use core::num::traits::{Bounded, Pow}; use crate::erc20::ERC20Component::InternalImpl as ERC20InternalImpl; use crate::erc20::extensions::erc4626::DefaultConfig; use crate::erc20::extensions::erc4626::ERC4626Component; @@ -16,7 +16,6 @@ use openzeppelin_test_common::mocks::erc4626::ERC4626Mock; use openzeppelin_testing as utils; use openzeppelin_testing::constants::{NAME, OTHER, RECIPIENT, SPENDER, SYMBOL, ZERO}; use openzeppelin_testing::events::EventSpyExt; -use openzeppelin_utils::math; use openzeppelin_utils::serde::SerializedAppend; use snforge_std::{CheatSpan, EventSpy, cheat_caller_address, spy_events}; use starknet::{ContractAddress, contract_address_const}; @@ -46,11 +45,11 @@ const NO_OFFSET_DECIMALS: u8 = 0; const OFFSET_DECIMALS: u8 = 1; fn parse_token(token: u256) -> u256 { - token * math::power(10, DEFAULT_DECIMALS.into()) + token * 10_u256.pow(DEFAULT_DECIMALS.into()) } -fn parse_share_offset(share: u256) -> u256 { - share * math::power(10, DEFAULT_DECIMALS.into() + OFFSET_DECIMALS.into()) +fn parse_share_offset(shares: u256) -> u256 { + shares * 10_u256.pow(DEFAULT_DECIMALS.into() + OFFSET_DECIMALS.into()) } // @@ -407,7 +406,7 @@ fn test_inflation_attack_deposit() { let (asset, vault) = setup_inflation_attack(); let virtual_assets = 1; let offset = 1; - let virtual_shares = math::power(10, offset); + let virtual_shares = 10_u256.pow(offset); let effective_assets = vault.total_assets() + virtual_assets; let effective_shares = vault.total_supply() + virtual_shares; @@ -456,7 +455,7 @@ fn test_inflation_attack_mint() { let (asset, vault) = setup_inflation_attack(); let virtual_assets = 1; let offset = 1; - let virtual_shares = math::power(10, offset); + let virtual_shares = 10_u256.pow(offset); let effective_assets = vault.total_assets() + virtual_assets; let effective_shares = vault.total_supply() + virtual_shares; @@ -594,7 +593,7 @@ fn test_full_vault_deposit() { let virtual_assets = 1; let offset = 1; - let virtual_shares = math::power(10, offset); + let virtual_shares = 10_u256.pow(offset); let effective_assets = vault.total_assets() + virtual_assets; let effective_shares = vault.total_supply() + virtual_shares; @@ -644,7 +643,7 @@ fn test_full_vault_mint() { let virtual_assets = 1; let offset = 1; - let virtual_shares = math::power(10, offset); + let virtual_shares = 10_u256.pow(offset); let effective_assets = vault.total_assets() + virtual_assets; let effective_shares = vault.total_supply() + virtual_shares; @@ -693,7 +692,7 @@ fn test_full_vault_withdraw() { let virtual_assets = 1; let offset = 1; - let virtual_shares = math::power(10, offset); + let virtual_shares = 10_u256.pow(offset); let effective_assets = vault.total_assets() + virtual_assets; let effective_shares = vault.total_supply() + virtual_shares; @@ -746,7 +745,7 @@ fn test_full_vault_withdraw_with_approval() { let virtual_assets = 1; let offset = 1; - let virtual_shares = math::power(10, offset); + let virtual_shares = 10_u256.pow(offset); let effective_assets = vault.total_assets() + virtual_assets; let effective_shares = vault.total_supply() + virtual_shares; @@ -793,7 +792,7 @@ fn test_full_vault_redeem() { let virtual_assets = 1; let offset = 1; - let virtual_shares = math::power(10, offset); + let virtual_shares = 10_u256.pow(offset); let effective_assets = vault.total_assets() + virtual_assets; let effective_shares = vault.total_supply() + virtual_shares; @@ -840,7 +839,7 @@ fn test_full_vault_redeem_with_approval() { let virtual_assets = 1; let offset = 1; - let virtual_shares = math::power(10, offset); + let virtual_shares = 10_u256.pow(offset); let effective_assets = vault.total_assets() + virtual_assets; let effective_shares = vault.total_supply() + virtual_shares; diff --git a/packages/utils/src/math.cairo b/packages/utils/src/math.cairo index d1f499f75..a91f97416 100644 --- a/packages/utils/src/math.cairo +++ b/packages/utils/src/math.cairo @@ -22,25 +22,6 @@ pub fn average< (a & b) + (a ^ b) / 2_u8.into() } -/// TMP. Raises `base` to the power of `exp`. Will panic if the result is greater than 2 ** 256 - 1. -/// -/// NOTE: This should be removed in favor of the corelib's Pow implementation when available. -/// https://github.com/starkware-libs/cairo/pull/6694 -pub fn power, +PartialEq, +TryInto, +Into, +Into>( - base: T, exp: T, -) -> T { - assert!(base != 0_u8.into(), "Math: base cannot be zero"); - let base: u256 = base.into(); - let exp: u256 = exp.into(); - let mut result: u256 = 1; - - for _ in 0..exp { - result *= base; - }; - - result.try_into().unwrap() -} - #[derive(Drop, Copy, Debug)] pub enum Rounding { Floor, // Toward negative infinity From c699dc4978f4690a2709553e8bfe3cd9ac1f60b2 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 9 Dec 2024 16:00:50 -0600 Subject: [PATCH 68/90] add zero addr test --- packages/token/src/tests/erc4626/test_erc4626.cairo | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/token/src/tests/erc4626/test_erc4626.cairo b/packages/token/src/tests/erc4626/test_erc4626.cairo index 49b88d9d1..497f149b0 100644 --- a/packages/token/src/tests/erc4626/test_erc4626.cairo +++ b/packages/token/src/tests/erc4626/test_erc4626.cairo @@ -178,6 +178,18 @@ fn deploy_vault_limits(asset_address: ContractAddress) -> ERC4626ABIDispatcher { ERC4626ABIDispatcher { contract_address } } +// +// initializer +// + +#[test] +#[should_panic(expected: 'ERC4626: asset address set to 0')] +fn test_initializer_zero_address_asset() { + let mut state = COMPONENT_STATE(); + + state.initializer(ZERO()); +} + // // asset // From 0ec51cfb7312abc028372d3a803b3e96e9045822 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 9 Dec 2024 18:57:59 -0600 Subject: [PATCH 69/90] fix max and preview comments --- .../erc20/extensions/erc4626/erc4626.cairo | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo index d59a91407..c4861beb7 100644 --- a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -221,7 +221,9 @@ pub mod ERC4626Component { /// Returns the maximum amount of the underlying asset that can be deposited into the Vault /// for the receiver, through a deposit call. - /// If the `LimitConfigTrait` is not defined for deposits, returns 2 ** 256 - 1. + /// + /// The default max deposit value is 2 ** 256 - 1. + /// This can be changed in the implementing contract by defining custom logic in `LimitConfigTrait::deposit_limit`. fn max_deposit(self: @ComponentState, receiver: ContractAddress) -> u256 { match Limit::deposit_limit(self, receiver) { Option::Some(limit) => limit, @@ -231,7 +233,9 @@ pub mod ERC4626Component { /// Allows an on-chain or off-chain user to simulate the effects of their deposit at the /// current block, given current on-chain conditions. - /// If the `FeeConfigTrait` is not defined for deposits, returns the full amount of shares. + /// + /// The default deposit preview value is the full amount of shares. + /// This can be changed in the implementing contract by defining custom logic in `FeeConfigTrait::adjust_deposit`. fn preview_deposit(self: @ComponentState, assets: u256) -> u256 { let adjusted_assets = Fee::adjust_deposit(self, assets); self._convert_to_shares(adjusted_assets, Rounding::Floor) @@ -260,7 +264,9 @@ pub mod ERC4626Component { /// Returns the maximum amount of the Vault shares that can be minted for `receiver` through /// a `mint` call. - /// If the `LimitConfigTrait` is not defined for mints, returns 2 ** 256 - 1. + /// + /// The default max mint value is 2 ** 256 - 1. + /// This can be changed in the implementing contract by defining custom logic in `LimitConfigTrait::mint_limit`. fn max_mint(self: @ComponentState, receiver: ContractAddress) -> u256 { match Limit::mint_limit(self, receiver) { Option::Some(limit) => limit, @@ -270,7 +276,9 @@ pub mod ERC4626Component { /// Allows an on-chain or off-chain user to simulate the effects of their mint at the /// current block, given current on-chain conditions. - /// If the `FeeConfigTrait` is not defined for mints, returns the full amount of assets. + /// + /// The default mint preview value is the full amount of assets. + /// This can be changed in the implementing contract by defining custom logic in `FeeConfigTrait::adjust_mint`. fn preview_mint(self: @ComponentState, shares: u256) -> u256 { let full_assets = self._convert_to_assets(shares, Rounding::Ceil); Fee::adjust_mint(self, full_assets) @@ -299,8 +307,9 @@ pub mod ERC4626Component { /// Returns the maximum amount of the underlying asset that can be withdrawn from the owner /// balance in the Vault, through a `withdraw` call. - /// If the `LimitConfigTrait` is not defined for withdraws, returns the full balance of - /// assets for `owner` (converted to shares). + /// + /// The default max withdraw value is the full balance of assets for `owner` (converted from shares). + /// This can be changed in the implementing contract by defining custom logic in `LimitConfigTrait::withdraw_limit`. fn max_withdraw(self: @ComponentState, owner: ContractAddress) -> u256 { match Limit::withdraw_limit(self, owner) { Option::Some(limit) => limit, @@ -314,7 +323,9 @@ pub mod ERC4626Component { /// Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the /// current block, given current on-chain conditions. - /// If the `FeeConfigTrait` is not defined for withdraws, returns the full amount of shares. + /// + /// The default withdraw preview value is the full amount of shares. + /// This can be changed in the implementing contract by defining custom logic in `FeeConfigTrait::adjust_withdraw`. fn preview_withdraw(self: @ComponentState, assets: u256) -> u256 { let adjusted_assets = Fee::adjust_withdraw(self, assets); self._convert_to_shares(adjusted_assets, Rounding::Ceil) @@ -345,8 +356,9 @@ pub mod ERC4626Component { /// Returns the maximum amount of Vault shares that can be redeemed from the owner balance /// in the Vault, through a `redeem` call. - /// If the `LimitConfigTrait` is not defined for redeems, returns the full balance of assets - /// for `owner`. + /// + /// The default max redeem value is the full balance of assets for `owner`. + /// This can be changed in the implementing contract by defining custom logic in `LimitConfigTrait::redeem_limit`. fn max_redeem(self: @ComponentState, owner: ContractAddress) -> u256 { match Limit::redeem_limit(self, owner) { Option::Some(limit) => limit, @@ -359,7 +371,9 @@ pub mod ERC4626Component { /// Allows an on-chain or off-chain user to simulate the effects of their redeemption at the /// current block, given current on-chain conditions. - /// If the `FeeConfigTrait` is not defined for redeems, returns the full amount of assets. + /// + /// The default redeem preview value is the full amount of assets. + /// This can be changed in the implementing contract by defining custom logic in `FeeConfigTrait::adjust_redeem`. fn preview_redeem(self: @ComponentState, shares: u256) -> u256 { let full_assets = self._convert_to_assets(shares, Rounding::Floor); Fee::adjust_redeem(self, full_assets) From f84d7d1bfce893c49d60dfe9d902854102899a74 Mon Sep 17 00:00:00 2001 From: Andrew Fleming Date: Mon, 9 Dec 2024 19:05:45 -0600 Subject: [PATCH 70/90] Apply suggestions from code review Co-authored-by: immrsd <103599616+immrsd@users.noreply.github.com> --- .../src/erc20/extensions/erc4626/erc4626.cairo | 14 ++++++-------- packages/utils/src/math.cairo | 8 +++----- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo index 69b89ac7d..6aa8cbc32 100644 --- a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -148,7 +148,7 @@ pub mod ERC4626Component { pub trait LimitConfigTrait { fn deposit_limit( self: @ComponentState, receiver: ContractAddress - ) -> Option:: { + ) -> Option { Option::None } @@ -440,7 +440,7 @@ pub mod ERC4626Component { /// - `asset_address` cannot be the zero address. fn initializer(ref self: ComponentState, asset_address: ContractAddress) { ImmutableConfig::validate(); - assert(!asset_address.is_zero(), Errors::INVALID_ASSET_ADDRESS); + assert(asset_address.is_non_zero(), Errors::INVALID_ASSET_ADDRESS); self.ERC4626_asset.write(asset_address); } @@ -503,7 +503,7 @@ pub mod ERC4626Component { // Burn shares first let mut erc20_component = get_dep_component_mut!(ref self, ERC20); - if (caller != owner) { + if caller != owner { erc20_component._spend_allowance(owner, caller, shares); } erc20_component.burn(owner, shares); @@ -520,7 +520,7 @@ pub mod ERC4626Component { fn _convert_to_shares( self: @ComponentState, assets: u256, rounding: Rounding ) -> u256 { - let mut erc20_component = get_dep_component!(self, ERC20); + let erc20_component = get_dep_component!(self, ERC20); let total_supply = erc20_component.total_supply(); math::u256_mul_div( @@ -536,7 +536,7 @@ pub mod ERC4626Component { fn _convert_to_assets( self: @ComponentState, shares: u256, rounding: Rounding ) -> u256 { - let mut erc20_component = get_dep_component!(self, ERC20); + let erc20_component = get_dep_component!(self, ERC20); let total_supply = erc20_component.total_supply(); math::u256_mul_div( @@ -577,9 +577,7 @@ pub impl DefaultConfig of ERC4626Component::ImmutableConfig { mod Test { use openzeppelin_test_common::mocks::erc4626::ERC4626Mock; use super::ERC4626Component::InternalImpl; - use super::ERC4626Component; - use super::ERC4626DefaultLimits; - use super::ERC4626DefaultNoFees; + use super::{ERC4626Component, ERC4626DefaultLimits, ERC4626DefaultNoFees}; type ComponentState = ERC4626Component::ComponentState; diff --git a/packages/utils/src/math.cairo b/packages/utils/src/math.cairo index aa106f0a8..1fa5c4b6d 100644 --- a/packages/utils/src/math.cairo +++ b/packages/utils/src/math.cairo @@ -62,13 +62,11 @@ pub fn u256_mul_div(x: u256, y: u256, denominator: u256, rounding: Rounding) -> let is_rounded_up = match rounding { Rounding::Ceil => 1, Rounding::Expand => 1, - _ => 0 + Rounding::Trunc => 0, + Rounding::Expand => 0 }; - let has_remainder = match r > 0 { - true => 1, - false => 0 - }; + let has_remainder = if r > 0 { 1 } else { 0 }; q + (is_rounded_up & has_remainder) } From 960f3383d723ad3029cca91589ecde8f54b574fb Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 9 Dec 2024 20:10:20 -0600 Subject: [PATCH 71/90] improve interface fmt --- .../src/erc20/extensions/erc4626/interface.cairo | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/token/src/erc20/extensions/erc4626/interface.cairo b/packages/token/src/erc20/extensions/erc4626/interface.cairo index af891c9d9..53d5bf494 100644 --- a/packages/token/src/erc20/extensions/erc4626/interface.cairo +++ b/packages/token/src/erc20/extensions/erc4626/interface.cairo @@ -11,12 +11,14 @@ pub trait IERC4626 { /// MUST be an ERC20 token contract. /// MUST NOT panic. fn asset(self: @TState) -> ContractAddress; + /// Returns the total amount of the underlying asset that is “managed” by Vault. /// /// SHOULD include any compounding that occurs from yield. /// MUST be inclusive of any fees that are charged against assets in the Vault. /// MUST NOT panic. fn total_assets(self: @TState) -> u256; + /// Returns the amount of shares that the Vault would exchange for the amount of assets /// provided, in an ideal scenario where all the conditions are met. /// @@ -25,6 +27,7 @@ pub trait IERC4626 { /// MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange. /// MUST NOT panic. fn convert_to_shares(self: @TState, assets: u256) -> u256; + /// Returns the amount of assets that the Vault would exchange for the amount of shares /// provided, in an ideal scenario where all the conditions are met. /// @@ -37,6 +40,7 @@ pub trait IERC4626 { /// should reflect the “average-user’s” price-per-share, meaning what the average user /// should expect to see when exchanging to and from. fn convert_to_assets(self: @TState, shares: u256) -> u256; + /// Returns the maximum amount of the underlying asset that can be deposited into the Vault for /// `receiver`, through a deposit call. /// @@ -49,6 +53,7 @@ pub trait IERC4626 { /// should reflect the “average-user’s” price-per-share, meaning what the average user /// should expect to see when exchanging to and from. fn max_deposit(self: @TState, receiver: ContractAddress) -> u256; + /// Allows an on-chain or off-chain user to simulate the effects of their deposit at the current /// block, given current on-chain conditions. /// @@ -66,6 +71,7 @@ pub trait IERC4626 { /// SHOULD be considered slippage in share price or some other type of condition, meaning the /// depositor will lose assets by depositing. fn preview_deposit(self: @TState, assets: u256) -> u256; + /// Mints Vault shares to `receiver` by depositing exactly amount of `assets`. /// /// MUST emit the Deposit event. @@ -77,6 +83,7 @@ pub trait IERC4626 { /// Note that most implementations will require pre-approval of the Vault with the Vault’s /// underlying asset token. fn deposit(ref self: TState, assets: u256, receiver: ContractAddress) -> u256; + /// Returns the maximum amount of the Vault shares that can be minted for the receiver, through /// a mint call. /// @@ -85,6 +92,7 @@ pub trait IERC4626 { /// minted. /// MUST NOT panic. fn max_mint(self: @TState, receiver: ContractAddress) -> u256; + /// Allows an on-chain or off-chain user to simulate the effects of their mint at the current /// block, given current on-chain conditions. /// @@ -106,6 +114,7 @@ pub trait IERC4626 { /// be considered slippage in share price or some other type of condition, meaning the depositor /// will lose assets by minting. fn preview_mint(self: @TState, shares: u256) -> u256; + /// Mints exactly shares Vault shares to receiver by depositing amount of underlying tokens. /// /// MUST emit the `Deposit` event. @@ -117,12 +126,14 @@ pub trait IERC4626 { /// Note that most implementations will require pre-approval of the Vault with the Vault’s /// underlying asset token. fn mint(ref self: TState, shares: u256, receiver: ContractAddress) -> u256; + /// Returns the maximum amount of the underlying asset that can be withdrawn from the owner /// balance in the Vault, through a withdraw call. /// /// MUST return a limited value if owner is subject to some withdrawal limit or timelock. /// MUST NOT panic. fn max_withdraw(self: @TState, owner: ContractAddress) -> u256; + /// Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the /// current block, given current on-chain conditions. /// @@ -140,6 +151,7 @@ pub trait IERC4626 { /// SHOULD be considered slippage in share price or some other type of condition, meaning the /// depositor will lose assets by depositing. fn preview_withdraw(self: @TState, assets: u256) -> u256; + /// Burns shares from owner and sends exactly assets of underlying tokens to receiver. /// /// MUST emit the `Withdraw` event. @@ -154,6 +166,7 @@ pub trait IERC4626 { fn withdraw( ref self: TState, assets: u256, receiver: ContractAddress, owner: ContractAddress, ) -> u256; + /// Returns the maximum amount of Vault shares that can be redeemed from the owner balance in /// the Vault, through a redeem call. /// @@ -162,6 +175,7 @@ pub trait IERC4626 { /// timelock. /// MUST NOT panic. fn max_redeem(self: @TState, owner: ContractAddress) -> u256; + /// Allows an on-chain or off-chain user to simulate the effects of their redeemption at the /// current block, given current on-chain conditions. /// @@ -179,6 +193,7 @@ pub trait IERC4626 { /// considered slippage in share price or some other type of condition, meaning the depositor /// will lose assets by redeeming. fn preview_redeem(self: @TState, shares: u256) -> u256; + /// Burns exactly shares from owner and sends assets of underlying tokens to receiver. /// /// MUST emit the `Withdraw` event. From b147fb4abc95f319a6a0e7c7c80c52fdd22012dc Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 10 Dec 2024 22:39:03 -0600 Subject: [PATCH 72/90] re-enable cairo-coverage in CI --- .github/workflows/test.yml | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c68479d59..a28b36343 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,10 +34,8 @@ jobs: with: starknet-foundry-version: ${{ env.FOUNDRY_VERSION }} - # Issue with cairo-coverage. Re-add to CI once issues are fixed. - # - # - name: Install cairo-coverage - # run: curl -L https://raw.githubusercontent.com/software-mansion/cairo-coverage/main/scripts/install.sh | sh + - name: Install cairo-coverage + run: curl -L https://raw.githubusercontent.com/software-mansion/cairo-coverage/main/scripts/install.sh | sh - name: Markdown lint uses: DavidAnson/markdownlint-cli2-action@eb5ca3ab411449c66620fe7f1b3c9e10547144b0 # v16 @@ -52,13 +50,11 @@ jobs: - name: Run tests run: snforge test --workspace - # Issue with cairo-coverage. Re-add to CI once issues are fixed. - # - # - name: Run tests and generate coverage report - # run: snforge test --workspace --coverage - # - # - name: Upload coverage to Codecov - # uses: codecov/codecov-action@v4 - # with: - # file: ./coverage.lcov - # token: ${{ secrets.CODECOV_TOKEN }} + - name: Run tests and generate coverage report + run: snforge test --workspace --coverage + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + file: ./coverage.lcov + token: ${{ secrets.CODECOV_TOKEN }} From 978ac98fd0729253e1e45b43d75ca9e749fecf25 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 10 Dec 2024 22:40:01 -0600 Subject: [PATCH 73/90] tmp lower codecov coverage --- codecov.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/codecov.yml b/codecov.yml index dacdbb20d..6f76a0050 100644 --- a/codecov.yml +++ b/codecov.yml @@ -5,7 +5,8 @@ comment: coverage: # The value range where you want the value to be green # Hold ourselves to a high bar. - range: 90..100 + # TMP 80 floor until cairo-coverage becomes more stable + range: 80..100 status: project: coverage: @@ -16,7 +17,8 @@ coverage: patch: default: # Require new code to have 90%+ coverage. - target: 90% + # TMP target until cairo-coverage becomes more stable + target: 80% threshold: 2% ignore: @@ -27,4 +29,3 @@ ignore: github_checks: annotations: false - \ No newline at end of file From 533f7bab5299742dbe274a883142517cd2206b30 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 11 Dec 2024 00:58:53 -0600 Subject: [PATCH 74/90] tmp increase threshold --- codecov.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/codecov.yml b/codecov.yml index 6f76a0050..75b444982 100644 --- a/codecov.yml +++ b/codecov.yml @@ -12,14 +12,15 @@ coverage: coverage: # Use the coverage from the base commit (pull request base) coverage to compare against. # Once we have a baseline we can be more strict. + # TMP threshold until cairo-coverage becomes more stable target: auto - threshold: 2% + threshold: 4% patch: default: # Require new code to have 90%+ coverage. - # TMP target until cairo-coverage becomes more stable + # TMP target and threshold until cairo-coverage becomes more stable target: 80% - threshold: 2% + threshold: 4% ignore: - "**/tests/**" From 84e36812e90a2ce1b5b23e973d4cf413cd0b2b74 Mon Sep 17 00:00:00 2001 From: Andrew Fleming Date: Tue, 17 Dec 2024 20:06:02 -0600 Subject: [PATCH 75/90] Apply suggestions from code review Co-authored-by: immrsd <103599616+immrsd@users.noreply.github.com> --- packages/test_common/src/mocks/erc20.cairo | 4 ++-- packages/token/src/erc20/extensions/erc4626.cairo | 2 +- .../token/src/erc20/extensions/erc4626/erc4626.cairo | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/test_common/src/mocks/erc20.cairo b/packages/test_common/src/mocks/erc20.cairo index 3891fba49..225d60a9b 100644 --- a/packages/test_common/src/mocks/erc20.cairo +++ b/packages/test_common/src/mocks/erc20.cairo @@ -322,7 +322,7 @@ pub mod ERC20ReentrantMock { ) { let mut contract_state = self.get_contract_mut(); - if (contract_state.reenter_type.read() == Type::Before) { + if contract_state.reenter_type.read() == Type::Before { contract_state.reenter_type.write(Type::No); contract_state.function_call(); } @@ -336,7 +336,7 @@ pub mod ERC20ReentrantMock { ) { let mut contract_state = self.get_contract_mut(); - if (contract_state.reenter_type.read() == Type::After) { + if contract_state.reenter_type.read() == Type::After { contract_state.reenter_type.write(Type::No); contract_state.function_call(); } diff --git a/packages/token/src/erc20/extensions/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626.cairo index c0b2bc208..f9e34c1aa 100644 --- a/packages/token/src/erc20/extensions/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626.cairo @@ -1,7 +1,7 @@ pub mod erc4626; pub mod interface; -pub use erc4626::DefaultConfig; +pub use erc4626::DefaultConfig; pub use erc4626::ERC4626Component; pub use erc4626::ERC4626DefaultLimits; pub use erc4626::ERC4626DefaultNoFees; diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo index 67977e6de..d86fc8ac7 100644 --- a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -29,7 +29,7 @@ /// The drawback of this approach is that the virtual shares do capture (a very small) part of the /// value being accrued to the vault. Also, if the vault experiences losses and users try to exit /// the vault, the virtual shares and assets will cause the first exiting user to experience reduced -/// losses to the detriment to the last users who will experience bigger losses. +/// losses to the detriment to the last users who will experience bigger losses. #[starknet::component] pub mod ERC4626Component { use core::num::traits::{Bounded, Pow, Zero}; @@ -512,6 +512,7 @@ pub mod ERC4626Component { /// - `ERC20::transfer` must return true. /// /// Emits two `ERC20::Transfer` events (`ERC20::burn` and `ERC20::transfer`). + /// /// Emits a `Withdraw` event. fn _withdraw( ref self: ComponentState, @@ -548,7 +549,6 @@ pub mod ERC4626Component { math::u256_mul_div( assets, - //total_supply + math::power(10, Immutable::DECIMALS_OFFSET.into()), total_supply + 10_u256.pow(Immutable::DECIMALS_OFFSET.into()), self.total_assets() + 1, rounding, @@ -573,9 +573,9 @@ pub mod ERC4626Component { } } -/// -/// Default (empty) traits -/// +// +// Default (empty) traits +// pub impl ERC4626HooksEmptyImpl< TContractState, From 9a6a14adabf10eadcf85a5a3ee568e66dffbdaeb Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 18 Dec 2024 00:13:14 -0600 Subject: [PATCH 76/90] fix comment --- packages/token/src/erc20/extensions/erc4626/erc4626.cairo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo index d86fc8ac7..32ffae03c 100644 --- a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -122,7 +122,7 @@ pub mod ERC4626Component { } } - /// Adjustments for fees expected to be defined on the contract level. + /// Adjustments for fees expected to be defined at the contract level. /// Defaults to no entry or exit fees. /// To transfer fees, this trait needs to be coordinated with `ERC4626Component::ERC4626Hooks`. pub trait FeeConfigTrait { From 9c69ca254e798468ca7059d473a8d16c6f1137fc Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 18 Dec 2024 00:40:40 -0600 Subject: [PATCH 77/90] add LimitConfig comments --- .../token/src/erc20/extensions/erc4626/erc4626.cairo | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo index 32ffae03c..abbe406d6 100644 --- a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -143,27 +143,35 @@ pub mod ERC4626Component { } } - /// Sets custom limits to the target exchange type and is expected to be defined at the contract + /// Sets limits to the target exchange type and is expected to be defined at the contract /// level. pub trait LimitConfigTrait { + /// The max deposit allowed. + /// Defaults (`Option::None`) to 2 ** 256 - 1. fn deposit_limit( self: @ComponentState, receiver: ContractAddress, ) -> Option { Option::None } + /// The max deposit allowed. + /// Defaults (`Option::None`) to 2 ** 256 - 1. fn mint_limit( self: @ComponentState, receiver: ContractAddress, ) -> Option { Option::None } + /// The max withdraw allowed. + /// Defaults (`Option::None`) to the full asset balance of `owner` converted from shares. fn withdraw_limit( self: @ComponentState, owner: ContractAddress, ) -> Option { Option::None } + /// The max deposit allowed. + /// Defaults (`Option::None`) to the full asset balance of `owner`. fn redeem_limit( self: @ComponentState, owner: ContractAddress, ) -> Option { From 0c3f8e059c3c5e6e4ea92ee7aa35106bf472adef Mon Sep 17 00:00:00 2001 From: Andrew Fleming Date: Wed, 18 Dec 2024 13:12:50 -0600 Subject: [PATCH 78/90] Apply suggestions from code review Co-authored-by: Eric Nordelo --- .../token/src/erc20/extensions/erc4626/erc4626.cairo | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo index abbe406d6..d7e369247 100644 --- a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -5,7 +5,7 @@ /// /// The ERC4626 component is an extension of ERC20 and provides an implementation of the IERC4626 /// interface which allows the minting and burning of "shares" in exchange for an underlying -/// "asset." The component leverages traits to configure fees, limits, and decimals. +/// "asset". The component leverages traits to configure fees, limits, and decimals. /// /// CAUTION: In empty (or nearly empty) ERC-4626 vaults, deposits are at high risk of being stolen /// through frontrunning with a "donation" to the vault that inflates the price of a share. This is @@ -108,8 +108,7 @@ pub mod ERC4626Component { /// /// Requirements: /// - /// - `UNDERLYING_DECIMALS`+ `DECIMALS_OFFSET` cannot exceed 255 (max u8). - /// + /// - `UNDERLYING_DECIMALS` + `DECIMALS_OFFSET` cannot exceed 255 (max u8). pub trait ImmutableConfig { const UNDERLYING_DECIMALS: u8; const DECIMALS_OFFSET: u8; @@ -329,8 +328,8 @@ pub mod ERC4626Component { Option::Some(limit) => limit, Option::None => { let erc20_component = get_dep_component!(self, ERC20); - let owner_bal = erc20_component.balance_of(owner); - self._convert_to_assets(owner_bal, Rounding::Floor) + let owner_shares = erc20_component.balance_of(owner); + self._convert_to_assets(owner_shares, Rounding::Floor) }, } } From 8852efeed5906af73f524d7a4e0a8320e725522a Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 18 Dec 2024 17:45:33 -0600 Subject: [PATCH 79/90] add HasComponent to traits --- .../token/src/erc20/extensions/erc4626/erc4626.cairo | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo index d7e369247..8ddbd76d5 100644 --- a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -124,7 +124,7 @@ pub mod ERC4626Component { /// Adjustments for fees expected to be defined at the contract level. /// Defaults to no entry or exit fees. /// To transfer fees, this trait needs to be coordinated with `ERC4626Component::ERC4626Hooks`. - pub trait FeeConfigTrait { + pub trait FeeConfigTrait> { fn adjust_deposit(self: @ComponentState, assets: u256) -> u256 { assets } @@ -144,7 +144,7 @@ pub mod ERC4626Component { /// Sets limits to the target exchange type and is expected to be defined at the contract /// level. - pub trait LimitConfigTrait { + pub trait LimitConfigTrait> { /// The max deposit allowed. /// Defaults (`Option::None`) to 2 ** 256 - 1. fn deposit_limit( @@ -180,7 +180,7 @@ pub mod ERC4626Component { /// Allows contracts to hook logic into deposit and withdraw transactions. /// This is where contracts can transfer fees. - pub trait ERC4626HooksTrait { + pub trait ERC4626HooksTrait> { fn before_withdraw(ref self: ComponentState, assets: u256, shares: u256) {} fn after_deposit(ref self: ComponentState, assets: u256, shares: u256) {} } @@ -586,10 +586,12 @@ pub mod ERC4626Component { pub impl ERC4626HooksEmptyImpl< TContractState, + +ERC4626Component::HasComponent > of ERC4626Component::ERC4626HooksTrait {} -pub impl ERC4626DefaultNoFees of ERC4626Component::FeeConfigTrait {} +pub impl ERC4626DefaultNoFees> of ERC4626Component::FeeConfigTrait {} pub impl ERC4626DefaultLimits< TContractState, + +ERC4626Component::HasComponent > of ERC4626Component::LimitConfigTrait {} /// Implementation of the default `ERC4626Component::ImmutableConfig`. From 8729faadce5e01c44deb8d0eaad0ef09a2e0b8ae Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 18 Dec 2024 17:46:05 -0600 Subject: [PATCH 80/90] fix fmt --- .../token/src/erc20/extensions/erc4626/erc4626.cairo | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo index 8ddbd76d5..bf782bc64 100644 --- a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -585,13 +585,13 @@ pub mod ERC4626Component { // pub impl ERC4626HooksEmptyImpl< - TContractState, - +ERC4626Component::HasComponent + TContractState, +ERC4626Component::HasComponent, > of ERC4626Component::ERC4626HooksTrait {} -pub impl ERC4626DefaultNoFees> of ERC4626Component::FeeConfigTrait {} +pub impl ERC4626DefaultNoFees< + TContractState, +ERC4626Component::HasComponent, +> of ERC4626Component::FeeConfigTrait {} pub impl ERC4626DefaultLimits< - TContractState, - +ERC4626Component::HasComponent + TContractState, +ERC4626Component::HasComponent, > of ERC4626Component::LimitConfigTrait {} /// Implementation of the default `ERC4626Component::ImmutableConfig`. From fc09ba92bc83ce4166552e55ab303fc9b9788d1a Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 18 Dec 2024 19:27:36 -0600 Subject: [PATCH 81/90] fix comments --- packages/token/src/erc20/extensions/erc4626/erc4626.cairo | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo index bf782bc64..4fd691603 100644 --- a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -215,13 +215,13 @@ pub mod ERC4626Component { } /// Returns the amount of shares that the Vault would exchange for the amount of assets - /// provided, in an ideal scenario where all the conditions are met. + /// provided irrespective of slippage or fees. fn convert_to_shares(self: @ComponentState, assets: u256) -> u256 { self._convert_to_shares(assets, Rounding::Floor) } /// Returns the amount of assets that the Vault would exchange for the amount of shares - /// provided, in an ideal scenario where all the conditions are met. + /// provided irrespective of slippage or fees. fn convert_to_assets(self: @ComponentState, shares: u256) -> u256 { self._convert_to_assets(shares, Rounding::Floor) } From cf24a40a3bb1814ce228ba0ffe8e0fb59e129c28 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 18 Dec 2024 21:06:33 -0600 Subject: [PATCH 82/90] fix interface comments --- .../erc20/extensions/erc4626/interface.cairo | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/packages/token/src/erc20/extensions/erc4626/interface.cairo b/packages/token/src/erc20/extensions/erc4626/interface.cairo index 53d5bf494..933877419 100644 --- a/packages/token/src/erc20/extensions/erc4626/interface.cairo +++ b/packages/token/src/erc20/extensions/erc4626/interface.cairo @@ -20,25 +20,29 @@ pub trait IERC4626 { fn total_assets(self: @TState) -> u256; /// Returns the amount of shares that the Vault would exchange for the amount of assets - /// provided, in an ideal scenario where all the conditions are met. + /// provided irrespective of slippage or fees. /// /// MUST NOT be inclusive of any fees that are charged against assets in the Vault. /// MUST NOT show any variations depending on the caller. /// MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange. /// MUST NOT panic. + /// + /// NOTE: This calculation MAY NOT reflect the "per-user" price-per-share, and instead should reflect the + /// "average-user's" price-per-share, meaning what the average user should expect to see when exchanging + /// to and from. fn convert_to_shares(self: @TState, assets: u256) -> u256; /// Returns the amount of assets that the Vault would exchange for the amount of shares - /// provided, in an ideal scenario where all the conditions are met. + /// provided irrespective of slippage or fees. /// /// MUST NOT be inclusive of any fees that are charged against assets in the Vault. /// MUST NOT show any variations depending on the caller. /// MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange. /// MUST NOT panic. /// - /// Note that this calculation MAY NOT reflect the “per-user” price-per-share, and instead - /// should reflect the “average-user’s” price-per-share, meaning what the average user - /// should expect to see when exchanging to and from. + /// NOTE: This calculation MAY NOT reflect the “per-user” price-per-share, and instead should reflect + /// the “average-user’s” price-per-share, meaning what the average user should expect to see when exchanging + /// to and from. fn convert_to_assets(self: @TState, shares: u256) -> u256; /// Returns the maximum amount of the underlying asset that can be deposited into the Vault for @@ -48,10 +52,6 @@ pub trait IERC4626 { /// MUST return 2 ** 256 - 1 if there is no limit on the maximum amount of assets that may be /// deposited. /// MUST NOT panic. - /// - /// Note that this calculation MAY NOT reflect the “per-user” price-per-share, and instead - /// should reflect the “average-user’s” price-per-share, meaning what the average user - /// should expect to see when exchanging to and from. fn max_deposit(self: @TState, receiver: ContractAddress) -> u256; /// Allows an on-chain or off-chain user to simulate the effects of their deposit at the current @@ -67,7 +67,7 @@ pub trait IERC4626 { /// fees. /// MUST NOT panic. /// - /// Note that any unfavorable discrepancy between `convert_to_shares` and `preview_deposit` + /// NOTE: Any unfavorable discrepancy between `convert_to_shares` and `preview_deposit` /// SHOULD be considered slippage in share price or some other type of condition, meaning the /// depositor will lose assets by depositing. fn preview_deposit(self: @TState, assets: u256) -> u256; @@ -80,7 +80,7 @@ pub trait IERC4626 { /// MUST panic if all of assets cannot be deposited (due to deposit limit being reached, /// slippage, the user not approving enough underlying tokens to the Vault contract, etc). /// - /// Note that most implementations will require pre-approval of the Vault with the Vault’s + /// NOTE: Most implementations will require pre-approval of the Vault with the Vault’s /// underlying asset token. fn deposit(ref self: TState, assets: u256, receiver: ContractAddress) -> u256; @@ -109,10 +109,6 @@ pub trait IERC4626 { /// NOTE: Any unfavorable discrepancy between convertToAssets and previewMint SHOULD be /// considered slippage in share price or some other type of condition, meaning the depositor /// will lose assets by minting. - /// - /// Note that any unfavorable discrepancy between `convert_to_assets` and `preview_mint` SHOULD - /// be considered slippage in share price or some other type of condition, meaning the depositor - /// will lose assets by minting. fn preview_mint(self: @TState, shares: u256) -> u256; /// Mints exactly shares Vault shares to receiver by depositing amount of underlying tokens. @@ -123,7 +119,7 @@ pub trait IERC4626 { /// MUST panic if all of shares cannot be minted (due to deposit limit being reached, slippage, /// the user not approving enough underlying tokens to the Vault contract, etc). /// - /// Note that most implementations will require pre-approval of the Vault with the Vault’s + /// NOTE: Most implementations will require pre-approval of the Vault with the Vault’s /// underlying asset token. fn mint(ref self: TState, shares: u256, receiver: ContractAddress) -> u256; @@ -147,7 +143,7 @@ pub trait IERC4626 { /// withdrawal fees. /// MUST not panic. /// - /// Note that any unfavorable discrepancy between `convert_to_shares` and `preview_withdraw` + /// NOTE: Any unfavorable discrepancy between `convert_to_shares` and `preview_withdraw` /// SHOULD be considered slippage in share price or some other type of condition, meaning the /// depositor will lose assets by depositing. fn preview_withdraw(self: @TState, assets: u256) -> u256; @@ -160,7 +156,7 @@ pub trait IERC4626 { /// MUST revert if all of assets cannot be withdrawn (due to withdrawal limit being reached, /// slippage, the owner not having enough shares, etc). /// - /// Note that some implementations will require pre-requesting to the Vault before a withdrawal + /// NOTE: Some implementations will require pre-requesting to the Vault before a withdrawal /// may be performed. /// Those methods should be performed separately. fn withdraw( @@ -189,7 +185,7 @@ pub trait IERC4626 { /// withdrawal fees. /// MUST NOT panic. /// - /// Note any unfavorable discrepancy between `convert_to_assets` and `preview_redeem` SHOULD be + /// NOTE: Any unfavorable discrepancy between `convert_to_assets` and `preview_redeem` SHOULD be /// considered slippage in share price or some other type of condition, meaning the depositor /// will lose assets by redeeming. fn preview_redeem(self: @TState, shares: u256) -> u256; @@ -202,7 +198,7 @@ pub trait IERC4626 { /// MUST revert if all of shares cannot be redeemed (due to withdrawal limit being reached, /// slippage, the owner not having enough shares, etc). /// - /// Note some implementations will require pre-requesting to the Vault before a withdrawal may + /// NOTE: Some implementations will require pre-requesting to the Vault before a withdrawal may /// be performed. /// Those methods should be performed separately. fn redeem( From 6462c6c146fec237d507b47787d2c831621f7601 Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 22 Dec 2024 18:15:32 -0600 Subject: [PATCH 83/90] tidy up mock --- packages/test_common/src/mocks/erc4626.cairo | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/packages/test_common/src/mocks/erc4626.cairo b/packages/test_common/src/mocks/erc4626.cairo index addbe5878..80cf37169 100644 --- a/packages/test_common/src/mocks/erc4626.cairo +++ b/packages/test_common/src/mocks/erc4626.cairo @@ -215,11 +215,10 @@ pub mod ERC4626FeesMock { use openzeppelin_token::erc20::extensions::erc4626::ERC4626Component::InternalTrait as ERC4626InternalTrait; use openzeppelin_token::erc20::extensions::erc4626::ERC4626DefaultLimits; use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use openzeppelin_token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; use openzeppelin_utils::math; use openzeppelin_utils::math::Rounding; - use openzeppelin_utils::serde::SerializedAppend; use starknet::ContractAddress; - use starknet::SyscallResultTrait; use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; component!(path: ERC4626Component, storage: erc4626, event: ERC4626Event); @@ -356,17 +355,9 @@ pub mod ERC4626FeesMock { #[generate_trait] pub impl InternalImpl of InternalTrait { fn transfer_fees(ref self: ContractState, recipient: ContractAddress, fee: u256) { - let asset_addr = self.asset(); - let selector = selector!("transfer"); - let mut calldata: Array = array![]; - calldata.append_serde(recipient); - calldata.append_serde(fee); - - let ret = starknet::syscalls::call_contract_syscall( - asset_addr, selector, calldata.span(), - ) - .unwrap_syscall(); - assert_eq!(*ret.at(0), 1); // true + let asset_address = self.asset(); + let asset_dispatcher = IERC20Dispatcher { contract_address: asset_address }; + assert(asset_dispatcher.transfer(recipient, fee), 'Fee transfer failed'); } fn remove_fee_from_deposit(self: @ContractState, assets: u256) -> u256 { From 8722e2d1f7eb06aea4acdd489bbd2d4165fa07aa Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 22 Dec 2024 18:16:18 -0600 Subject: [PATCH 84/90] fix fmt --- packages/test_common/src/mocks/erc4626.cairo | 2 +- .../src/erc20/extensions/erc4626/interface.cairo | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/test_common/src/mocks/erc4626.cairo b/packages/test_common/src/mocks/erc4626.cairo index 80cf37169..b2588a46d 100644 --- a/packages/test_common/src/mocks/erc4626.cairo +++ b/packages/test_common/src/mocks/erc4626.cairo @@ -214,8 +214,8 @@ pub mod ERC4626FeesMock { use openzeppelin_token::erc20::extensions::erc4626::ERC4626Component::FeeConfigTrait; use openzeppelin_token::erc20::extensions::erc4626::ERC4626Component::InternalTrait as ERC4626InternalTrait; use openzeppelin_token::erc20::extensions::erc4626::ERC4626DefaultLimits; - use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; use openzeppelin_token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; + use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; use openzeppelin_utils::math; use openzeppelin_utils::math::Rounding; use starknet::ContractAddress; diff --git a/packages/token/src/erc20/extensions/erc4626/interface.cairo b/packages/token/src/erc20/extensions/erc4626/interface.cairo index 933877419..286977769 100644 --- a/packages/token/src/erc20/extensions/erc4626/interface.cairo +++ b/packages/token/src/erc20/extensions/erc4626/interface.cairo @@ -27,9 +27,9 @@ pub trait IERC4626 { /// MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange. /// MUST NOT panic. /// - /// NOTE: This calculation MAY NOT reflect the "per-user" price-per-share, and instead should reflect the - /// "average-user's" price-per-share, meaning what the average user should expect to see when exchanging - /// to and from. + /// NOTE: This calculation MAY NOT reflect the "per-user" price-per-share, and instead should + /// reflect the "average-user's" price-per-share, meaning what the average user should expect to + /// see when exchanging to and from. fn convert_to_shares(self: @TState, assets: u256) -> u256; /// Returns the amount of assets that the Vault would exchange for the amount of shares @@ -40,9 +40,9 @@ pub trait IERC4626 { /// MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange. /// MUST NOT panic. /// - /// NOTE: This calculation MAY NOT reflect the “per-user” price-per-share, and instead should reflect - /// the “average-user’s” price-per-share, meaning what the average user should expect to see when exchanging - /// to and from. + /// NOTE: This calculation MAY NOT reflect the “per-user” price-per-share, and instead + /// should reflect the “average-user’s” price-per-share, meaning what the average user + /// should expect to see when exchanging to and from. fn convert_to_assets(self: @TState, shares: u256) -> u256; /// Returns the maximum amount of the underlying asset that can be deposited into the Vault for From 4eb0b835c76b650640edd5e95d6c8e08aa386139 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 23 Dec 2024 16:06:39 -0600 Subject: [PATCH 85/90] add fees comment to mock --- packages/test_common/src/mocks/erc4626.cairo | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/test_common/src/mocks/erc4626.cairo b/packages/test_common/src/mocks/erc4626.cairo index b2588a46d..6217000ba 100644 --- a/packages/test_common/src/mocks/erc4626.cairo +++ b/packages/test_common/src/mocks/erc4626.cairo @@ -208,6 +208,11 @@ pub mod ERC4626LimitsMock { } } +/// The mock contract charges fees in terms of assets, not shares. +/// This means that the fees are calculated based on the amount of assets that are being deposited or withdrawn, +/// and not based on the amount of shares that are being minted or redeemed. +/// This is an opinionated design decision for the purpose of testing. +/// DO NOT USE IN PRODUCTION #[starknet::contract] pub mod ERC4626FeesMock { use openzeppelin_token::erc20::extensions::erc4626::ERC4626Component; From 894e43bbff93f99116306f0a3db4ead8dca102e5 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 23 Dec 2024 16:08:41 -0600 Subject: [PATCH 86/90] improve comments --- .../erc20/extensions/erc4626/erc4626.cairo | 51 ++++++++++++++----- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo index 4fd691603..2cd3d6fdb 100644 --- a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -124,19 +124,30 @@ pub mod ERC4626Component { /// Adjustments for fees expected to be defined at the contract level. /// Defaults to no entry or exit fees. /// To transfer fees, this trait needs to be coordinated with `ERC4626Component::ERC4626Hooks`. + /// + /// See the example: + /// https://github.com/OpenZeppelin/cairo-contracts/tree/main/packages/test_common/src/mocks/erc4626.cairo pub trait FeeConfigTrait> { + /// Adjusts deposits within `preview_deposit` to account for entry fees. + /// Entry fees should be transferred in the `after_deposit` hook. fn adjust_deposit(self: @ComponentState, assets: u256) -> u256 { assets } + /// Adjusts mints within `preview_mint` to account for entry fees. + /// Entry fees should be transferred in the `after_deposit` hook. fn adjust_mint(self: @ComponentState, assets: u256) -> u256 { assets } + /// Adjusts withdraws within `preview_withdraw` to account for exit fees. + /// Exit fees should be transferred in the `before_withdraw` hook. fn adjust_withdraw(self: @ComponentState, assets: u256) -> u256 { assets } + /// Adjusts redeems within `preview_redeem` to account for exit fees. + /// Exit fees should be transferred in the `before_withdraw` hook. fn adjust_redeem(self: @ComponentState, assets: u256) -> u256 { assets } @@ -180,6 +191,14 @@ pub mod ERC4626Component { /// Allows contracts to hook logic into deposit and withdraw transactions. /// This is where contracts can transfer fees. + /// + /// NOTE: ERC4626 preview methods must be inclusive of any entry or exit fees. + /// The AdjustFeesTrait will adjust these values accordingly; therefore, + /// fees must be set in the AdjustFeesTrait if the using contract enforces + /// entry or exit fees. + /// + /// See the example: + /// https://github.com/OpenZeppelin/cairo-contracts/tree/main/packages/test_common/src/mocks/erc4626.cairo pub trait ERC4626HooksTrait> { fn before_withdraw(ref self: ComponentState, assets: u256, shares: u256) {} fn after_deposit(ref self: ComponentState, assets: u256, shares: u256) {} @@ -243,8 +262,10 @@ pub mod ERC4626Component { /// current block, given current on-chain conditions. /// /// The default deposit preview value is the full amount of shares. - /// This can be changed in the implementing contract by defining custom logic in - /// `FeeConfigTrait::adjust_deposit`. + /// This can be changed to account for fees, for example, in the implementing contract by defining + /// custom logic in `FeeConfigTrait::adjust_deposit`. + /// + /// NOTE: `preview_deposit` must be inclusive of entry fees to be compliant with the IERC4626 spec. fn preview_deposit(self: @ComponentState, assets: u256) -> u256 { let adjusted_assets = Fee::adjust_deposit(self, assets); self._convert_to_shares(adjusted_assets, Rounding::Floor) @@ -288,8 +309,10 @@ pub mod ERC4626Component { /// current block, given current on-chain conditions. /// /// The default mint preview value is the full amount of assets. - /// This can be changed in the implementing contract by defining custom logic in - /// `FeeConfigTrait::adjust_mint`. + /// This can be changed to account for fees, for example, in the implementing contract by defining + /// custom logic in `FeeConfigTrait::adjust_mint`. + /// + /// NOTE: `preview_mint` must be inclusive of entry fees to be compliant with the IERC4626 spec. fn preview_mint(self: @ComponentState, shares: u256) -> u256 { let full_assets = self._convert_to_assets(shares, Rounding::Ceil); Fee::adjust_mint(self, full_assets) @@ -338,8 +361,10 @@ pub mod ERC4626Component { /// current block, given current on-chain conditions. /// /// The default withdraw preview value is the full amount of shares. - /// This can be changed in the implementing contract by defining custom logic in - /// `FeeConfigTrait::adjust_withdraw`. + /// This can be changed to account for fees, for example, in the implementing contract by defining + /// custom logic in `FeeConfigTrait::adjust_withdraw`. + /// + /// NOTE: `preview_withdraw` must be inclusive of exit fees to be compliant with the IERC4626 spec. fn preview_withdraw(self: @ComponentState, assets: u256) -> u256 { let adjusted_assets = Fee::adjust_withdraw(self, assets); self._convert_to_shares(adjusted_assets, Rounding::Ceil) @@ -388,8 +413,10 @@ pub mod ERC4626Component { /// current block, given current on-chain conditions. /// /// The default redeem preview value is the full amount of assets. - /// This can be changed in the implementing contract by defining custom logic in - /// `FeeConfigTrait::adjust_redeem`. + /// This can be changed to account for fees, for example, in the implementing contract by defining + /// custom logic in `FeeConfigTrait::adjust_redeem`. + /// + /// NOTE: `preview_redeem` must be inclusive of exit fees to be compliant with the IERC4626 spec. fn preview_redeem(self: @ComponentState, shares: u256) -> u256 { let full_assets = self._convert_to_assets(shares, Rounding::Floor); Fee::adjust_redeem(self, full_assets) @@ -474,11 +501,11 @@ pub mod ERC4626Component { self.ERC4626_asset.write(asset_address); } - /// Business logic for `deposit` and `mint`. + /// Internal logic for `deposit` and `mint`. /// Transfers `assets` from `caller` to the Vault contract then mints `shares` to /// `receiver`. /// Fees can be transferred in the `ERC4626Hooks::after_deposit` hook which is executed - /// after the business logic. + /// after assets are transferred and shares are minted. /// /// Requirements: /// @@ -509,10 +536,10 @@ pub mod ERC4626Component { Hooks::after_deposit(ref self, assets, shares); } - /// Business logic for `withdraw` and `redeem`. + /// Internal logic for `withdraw` and `redeem`. /// Burns `shares` from `owner` and then transfers `assets` to `receiver`. /// Fees can be transferred in the `ERC4626Hooks::before_withdraw` hook which is executed - /// before the business logic. + /// before shares are burned and assets are transferred. /// /// Requirements: /// From 379f8749626de8d6bf401109ec8d1e2503e23c7d Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 23 Dec 2024 16:35:34 -0600 Subject: [PATCH 87/90] add comments to hook fns --- packages/token/src/erc20/extensions/erc4626/erc4626.cairo | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo index 2cd3d6fdb..baa6a7621 100644 --- a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -193,14 +193,18 @@ pub mod ERC4626Component { /// This is where contracts can transfer fees. /// /// NOTE: ERC4626 preview methods must be inclusive of any entry or exit fees. - /// The AdjustFeesTrait will adjust these values accordingly; therefore, - /// fees must be set in the AdjustFeesTrait if the using contract enforces + /// The `AdjustFeesTrait` will adjust these values accordingly; therefore, + /// fees must be set in the `AdjustFeesTrait` if the using contract enforces /// entry or exit fees. /// /// See the example: /// https://github.com/OpenZeppelin/cairo-contracts/tree/main/packages/test_common/src/mocks/erc4626.cairo pub trait ERC4626HooksTrait> { + /// Hooks into `InternalImpl::_withdraw`. + /// Executes logic before burning shares and transferring assets. fn before_withdraw(ref self: ComponentState, assets: u256, shares: u256) {} + /// Hooks into `InternalImpl::_deposit`. + /// Executes logic after transferring assets and minting shares. fn after_deposit(ref self: ComponentState, assets: u256, shares: u256) {} } From 3e8f237f91df21265e051772828214eb1749a92a Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 23 Dec 2024 21:03:46 -0600 Subject: [PATCH 88/90] fix fmt --- packages/test_common/src/mocks/erc4626.cairo | 4 +-- .../erc20/extensions/erc4626/erc4626.cairo | 28 +++++++++++-------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/packages/test_common/src/mocks/erc4626.cairo b/packages/test_common/src/mocks/erc4626.cairo index 6217000ba..81f9a4f21 100644 --- a/packages/test_common/src/mocks/erc4626.cairo +++ b/packages/test_common/src/mocks/erc4626.cairo @@ -209,8 +209,8 @@ pub mod ERC4626LimitsMock { } /// The mock contract charges fees in terms of assets, not shares. -/// This means that the fees are calculated based on the amount of assets that are being deposited or withdrawn, -/// and not based on the amount of shares that are being minted or redeemed. +/// This means that the fees are calculated based on the amount of assets that are being deposited +/// or withdrawn, and not based on the amount of shares that are being minted or redeemed. /// This is an opinionated design decision for the purpose of testing. /// DO NOT USE IN PRODUCTION #[starknet::contract] diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo index baa6a7621..81d150247 100644 --- a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -266,10 +266,11 @@ pub mod ERC4626Component { /// current block, given current on-chain conditions. /// /// The default deposit preview value is the full amount of shares. - /// This can be changed to account for fees, for example, in the implementing contract by defining - /// custom logic in `FeeConfigTrait::adjust_deposit`. + /// This can be changed to account for fees, for example, in the implementing contract by + /// defining custom logic in `FeeConfigTrait::adjust_deposit`. /// - /// NOTE: `preview_deposit` must be inclusive of entry fees to be compliant with the IERC4626 spec. + /// NOTE: `preview_deposit` must be inclusive of entry fees to be compliant with the + /// IERC4626 spec. fn preview_deposit(self: @ComponentState, assets: u256) -> u256 { let adjusted_assets = Fee::adjust_deposit(self, assets); self._convert_to_shares(adjusted_assets, Rounding::Floor) @@ -313,10 +314,11 @@ pub mod ERC4626Component { /// current block, given current on-chain conditions. /// /// The default mint preview value is the full amount of assets. - /// This can be changed to account for fees, for example, in the implementing contract by defining - /// custom logic in `FeeConfigTrait::adjust_mint`. + /// This can be changed to account for fees, for example, in the implementing contract by + /// defining custom logic in `FeeConfigTrait::adjust_mint`. /// - /// NOTE: `preview_mint` must be inclusive of entry fees to be compliant with the IERC4626 spec. + /// NOTE: `preview_mint` must be inclusive of entry fees to be compliant with the IERC4626 + /// spec. fn preview_mint(self: @ComponentState, shares: u256) -> u256 { let full_assets = self._convert_to_assets(shares, Rounding::Ceil); Fee::adjust_mint(self, full_assets) @@ -365,10 +367,11 @@ pub mod ERC4626Component { /// current block, given current on-chain conditions. /// /// The default withdraw preview value is the full amount of shares. - /// This can be changed to account for fees, for example, in the implementing contract by defining - /// custom logic in `FeeConfigTrait::adjust_withdraw`. + /// This can be changed to account for fees, for example, in the implementing contract by + /// defining custom logic in `FeeConfigTrait::adjust_withdraw`. /// - /// NOTE: `preview_withdraw` must be inclusive of exit fees to be compliant with the IERC4626 spec. + /// NOTE: `preview_withdraw` must be inclusive of exit fees to be compliant with the + /// IERC4626 spec. fn preview_withdraw(self: @ComponentState, assets: u256) -> u256 { let adjusted_assets = Fee::adjust_withdraw(self, assets); self._convert_to_shares(adjusted_assets, Rounding::Ceil) @@ -417,10 +420,11 @@ pub mod ERC4626Component { /// current block, given current on-chain conditions. /// /// The default redeem preview value is the full amount of assets. - /// This can be changed to account for fees, for example, in the implementing contract by defining - /// custom logic in `FeeConfigTrait::adjust_redeem`. + /// This can be changed to account for fees, for example, in the implementing contract by + /// defining custom logic in `FeeConfigTrait::adjust_redeem`. /// - /// NOTE: `preview_redeem` must be inclusive of exit fees to be compliant with the IERC4626 spec. + /// NOTE: `preview_redeem` must be inclusive of exit fees to be compliant with the IERC4626 + /// spec. fn preview_redeem(self: @ComponentState, shares: u256) -> u256 { let full_assets = self._convert_to_assets(shares, Rounding::Floor); Fee::adjust_redeem(self, full_assets) From 02206f07fb3dafbcafc473efbbabd7221eb69ba9 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 24 Dec 2024 00:14:28 -0600 Subject: [PATCH 89/90] improve FeeConfigTrait NOTE --- .../token/src/erc20/extensions/erc4626/erc4626.cairo | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo index 81d150247..8d3e8a6a4 100644 --- a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -123,9 +123,16 @@ pub mod ERC4626Component { /// Adjustments for fees expected to be defined at the contract level. /// Defaults to no entry or exit fees. - /// To transfer fees, this trait needs to be coordinated with `ERC4626Component::ERC4626Hooks`. /// - /// See the example: + /// NOTE: The FeeConfigTrait hooks directly into the preview methods of the ERC4626 component. + /// The preview methods must return as close to the exact amount of shares or assets as possible if the + /// actual (previewed) operation occurred in the same transaction (according to IERC4626 spec). + /// All operations use their corresponding preview method as the value of assets or shares being moved. + /// Therefore, adjusting an operation's assets in FeeConfigTrait consequently adjusts the assets + /// (or assets to be converted into shares) in both the preview operation and the actual operation. + /// + /// NOTE: To transfer fees, this trait needs to be coordinated with `ERC4626Component::ERC4626Hooks`. + /// See the ERC4626FeesMock example: /// https://github.com/OpenZeppelin/cairo-contracts/tree/main/packages/test_common/src/mocks/erc4626.cairo pub trait FeeConfigTrait> { /// Adjusts deposits within `preview_deposit` to account for entry fees. From 4263919cb7408ef0c932746032dcf3e3f3df6265 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 24 Dec 2024 00:14:56 -0600 Subject: [PATCH 90/90] fix fmt --- .../src/erc20/extensions/erc4626/erc4626.cairo | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo index 8d3e8a6a4..9488a210d 100644 --- a/packages/token/src/erc20/extensions/erc4626/erc4626.cairo +++ b/packages/token/src/erc20/extensions/erc4626/erc4626.cairo @@ -125,13 +125,17 @@ pub mod ERC4626Component { /// Defaults to no entry or exit fees. /// /// NOTE: The FeeConfigTrait hooks directly into the preview methods of the ERC4626 component. - /// The preview methods must return as close to the exact amount of shares or assets as possible if the - /// actual (previewed) operation occurred in the same transaction (according to IERC4626 spec). - /// All operations use their corresponding preview method as the value of assets or shares being moved. + /// The preview methods must return as close to the exact amount of shares or assets as possible + /// if the actual (previewed) operation occurred in the same transaction (according to IERC4626 + /// spec). + /// All operations use their corresponding preview method as the value of assets or shares being + /// moved. /// Therefore, adjusting an operation's assets in FeeConfigTrait consequently adjusts the assets - /// (or assets to be converted into shares) in both the preview operation and the actual operation. + /// (or assets to be converted into shares) in both the preview operation and the actual + /// operation. /// - /// NOTE: To transfer fees, this trait needs to be coordinated with `ERC4626Component::ERC4626Hooks`. + /// NOTE: To transfer fees, this trait needs to be coordinated with + /// `ERC4626Component::ERC4626Hooks`. /// See the ERC4626FeesMock example: /// https://github.com/OpenZeppelin/cairo-contracts/tree/main/packages/test_common/src/mocks/erc4626.cairo pub trait FeeConfigTrait> {