diff --git a/onchain/src/contracts/account.cairo b/onchain/src/contracts/account.cairo index c8d67338..fcbdf16b 100644 --- a/onchain/src/contracts/account.cairo +++ b/onchain/src/contracts/account.cairo @@ -5,12 +5,12 @@ mod Account { use openzeppelin::introspection::src5::SRC5Component; use starknet::account::Call; - use vault::spending_limit::daily_limit::DailyLimitComponent; - use vault::spending_limit::daily_limit::interface::IDailyLimit; + use vault::spending_limit::weekly_limit::WeeklyLimitComponent; + use vault::spending_limit::weekly_limit::interface::IWeeklyLimit; component!(path: AccountComponent, storage: account, event: AccountEvent); component!(path: SRC5Component, storage: src5, event: SRC5Event); - component!(path: DailyLimitComponent, storage: daily_limit, event: DailyLimitEvent); + component!(path: WeeklyLimitComponent, storage: weekly_limit, event: WeeklyLimitEvent); // Account #[abi(embed_v0)] @@ -19,12 +19,10 @@ mod Account { impl PublicKeyCamelImpl = AccountComponent::PublicKeyCamelImpl; #[abi(embed_v0)] impl DeclarerImpl = AccountComponent::DeclarerImpl; - #[abi(embed_v0)] - impl DeployableImpl = AccountComponent::DeployableImpl; impl AccountInternalImpl = AccountComponent::InternalImpl; // Daily Limit - impl DailyLimitInternalImpl = DailyLimitComponent::InternalImpl; + impl WeeklyLimitInternalImpl = WeeklyLimitComponent::InternalImpl; // SRC5 #[abi(embed_v0)] @@ -37,7 +35,7 @@ mod Account { #[substorage(v0)] src5: SRC5Component::Storage, #[substorage(v0)] - daily_limit: DailyLimitComponent::Storage, + weekly_limit: WeeklyLimitComponent::Storage, } #[event] @@ -48,7 +46,7 @@ mod Account { #[flat] SRC5Event: SRC5Component::Event, #[flat] - DailyLimitEvent: DailyLimitComponent::Event, + WeeklyLimitEvent: WeeklyLimitComponent::Event, } // @@ -58,7 +56,7 @@ mod Account { #[constructor] fn constructor(ref self: ContractState, public_key: felt252, limit: u256) { self.account.initializer(:public_key); - self.daily_limit.initializer(:limit); + self.weekly_limit.initializer(:limit); } // @@ -89,13 +87,9 @@ mod Account { // #[abi(embed_v0)] - impl DailyLimit of IDailyLimit { - fn get_daily_limit(self: @ContractState) -> u256 { - self.daily_limit.get_daily_limit() - } - - fn set_daily_limit(ref self: ContractState, new_limit: u256) { - self.daily_limit.set_daily_limit(:new_limit); + impl WeeklyLimit of IWeeklyLimit { + fn get_weekly_limit(self: @ContractState) -> u256 { + self.weekly_limit.get_weekly_limit() } } } diff --git a/onchain/src/spending_limit.cairo b/onchain/src/spending_limit.cairo index 18877ea5..a9acb38f 100644 --- a/onchain/src/spending_limit.cairo +++ b/onchain/src/spending_limit.cairo @@ -1 +1 @@ -pub mod daily_limit; +pub mod weekly_limit; diff --git a/onchain/src/spending_limit/daily_limit.cairo b/onchain/src/spending_limit/daily_limit.cairo deleted file mode 100644 index 9a0bb028..00000000 --- a/onchain/src/spending_limit/daily_limit.cairo +++ /dev/null @@ -1,5 +0,0 @@ -pub mod daily_limit; -pub mod interface; - -use daily_limit::DailyLimitComponent; -use interface::{IDailyLimitDispatcher, IDailyLimitDispatcherTrait}; diff --git a/onchain/src/spending_limit/daily_limit/daily_limit.cairo b/onchain/src/spending_limit/daily_limit/daily_limit.cairo deleted file mode 100644 index 82cca4f4..00000000 --- a/onchain/src/spending_limit/daily_limit/daily_limit.cairo +++ /dev/null @@ -1,152 +0,0 @@ -#[starknet::component] -mod DailyLimitComponent { - use array::ArrayTrait; - use array::IndexView; - use ecdsa::check_ecdsa_signature; - use starknet::account::Call; - use starknet::info::get_block_timestamp; - - use vault::spending_limit::daily_limit::interface; - - const DAY_IN_SECONDS: u64 = 86400; - const VALID: felt252 = 'VALID'; - - // - // Storage - // - - #[storage] - struct Storage { - limit: u256, - current_value: u256, - last_modification: u64, - } - - // - // Events - // - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - LimitUpdated: LimitUpdated - } - - #[derive(Drop, starknet::Event)] - struct LimitUpdated { - new_value: u256, - last_modification: u64, - } - - #[generate_trait] - impl InternalImpl< - TContractState, +HasComponent - > of InternalTrait { - #[inline(always)] - fn initializer(ref self: ComponentState, limit: u256) { - self.limit.write(limit); - } - - #[inline(always)] - fn check_below_limit_and_update( - ref self: ComponentState, value: u256 - ) -> bool { - let block_timestamp = get_block_timestamp(); - let new_value = if block_timestamp % DAY_IN_SECONDS == self - .last_modification - .read() % DAY_IN_SECONDS { - self.current_value.read() + value - } else { - value - }; - if new_value <= self.limit.read() { - self.current_value.write(new_value); - self.emit(LimitUpdated { new_value, last_modification: block_timestamp }); - true - } else { - false - } - } - - fn validate_sum_under_limit( - self: @ComponentState, ref calls: Span - ) -> bool { - let mut value = 0_u256; - loop { - match calls.pop_front() { - Option::Some(call) => { - if call.selector == @selector!("transfer") { - value += (*call.calldata[0]).into(); - } - }, - Option::None => { break; }, - } - }; - self.is_below_limit(:value) - } - - #[inline(always)] - fn is_below_limit(self: @ComponentState, value: u256) -> bool { - value <= self.limit.read() - } - - // Limit value mgmt - - fn get_daily_limit(self: @ComponentState) -> u256 { - self.limit.read() - } - - fn set_daily_limit(ref self: ComponentState, new_limit: u256) { - self.limit.write(new_limit); - } - } -} - -// -// Tests -// - -#[cfg(test)] -mod test { - use vault::spending_limit::daily_limit::DailyLimitComponent::InternalTrait; - use vault::spending_limit::daily_limit::interface::IDailyLimit; - - #[starknet::contract] - mod mock_contract { - use super::super::DailyLimitComponent; - component!(path: DailyLimitComponent, storage: spending_limit, event: SpendingLimitEvent); - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - SpendingLimitEvent: DailyLimitComponent::Event, - } - #[storage] - struct Storage { - #[substorage(v0)] - spending_limit: DailyLimitComponent::Storage, - } - } - - type ComponentState = super::DailyLimitComponent::ComponentState; - - fn COMPONENT() -> ComponentState { - super::DailyLimitComponent::component_state_for_testing() - } - - #[test] - fn test_is_below_limit() { - let mut component = COMPONENT(); - // 0 <= 0 - assert!(component.is_below_limit(0)); - // 1 <= 0 - assert!(!component.is_below_limit(1)); - // Set limit to 2 - component.initializer(2); - // 1 <= 2 - assert!(component.is_below_limit(1)); - // 3 <= 2 - assert!(!component.is_below_limit(3)); - } -} diff --git a/onchain/src/spending_limit/daily_limit/interface.cairo b/onchain/src/spending_limit/daily_limit/interface.cairo deleted file mode 100644 index ae338fe1..00000000 --- a/onchain/src/spending_limit/daily_limit/interface.cairo +++ /dev/null @@ -1,8 +0,0 @@ -use starknet::account::Call; - -#[starknet::interface] -trait IDailyLimit { - fn get_daily_limit(self: @TState) -> u256; - - fn set_daily_limit(ref self: TState, new_limit: u256); -} diff --git a/onchain/src/spending_limit/weekly_limit.cairo b/onchain/src/spending_limit/weekly_limit.cairo new file mode 100644 index 00000000..49086c64 --- /dev/null +++ b/onchain/src/spending_limit/weekly_limit.cairo @@ -0,0 +1,5 @@ +pub mod interface; +pub mod weekly_limit; +use interface::{IWeeklyLimitDispatcher, IWeeklyLimitDispatcherTrait}; + +use weekly_limit::WeeklyLimitComponent; diff --git a/onchain/src/spending_limit/weekly_limit/interface.cairo b/onchain/src/spending_limit/weekly_limit/interface.cairo new file mode 100644 index 00000000..c7851f77 --- /dev/null +++ b/onchain/src/spending_limit/weekly_limit/interface.cairo @@ -0,0 +1,6 @@ +use starknet::account::Call; + +#[starknet::interface] +trait IWeeklyLimit { + fn get_weekly_limit(self: @TState) -> u256; +} diff --git a/onchain/src/spending_limit/weekly_limit/weekly_limit.cairo b/onchain/src/spending_limit/weekly_limit/weekly_limit.cairo new file mode 100644 index 00000000..36a4d23c --- /dev/null +++ b/onchain/src/spending_limit/weekly_limit/weekly_limit.cairo @@ -0,0 +1,179 @@ +#[starknet::component] +mod WeeklyLimitComponent { + use core::option::OptionTrait; + use core::starknet::SyscallResultTrait; + use core::traits::Into; + use core::traits::TryInto; + use starknet::info::get_block_timestamp; + + /// Number of seconds in an hour. + const HOUR_IN_SECONDS: u64 = consteval_int!(60 * 60); + /// Number of seconds in a week. + const WEEK_IN_SECONDS: u64 = consteval_int!(60 * 60 * 24 * 7); + + // + // Storage + // + #[storage] + struct Storage { + limit: u256, + last_modification: u64, + } + + // + // Events + // + #[event] + #[derive(Drop, starknet::Event)] + enum Event {} + + #[derive(Drop, starknet::Event)] + struct LimitUpdated { + new_value: u256, + last_modification: u64, + } + + #[generate_trait] + impl InternalImpl< + TContractState, +HasComponent + > of InternalTrait { + /// Initializes this component with a spending limit for 7 rolling days. + #[inline(always)] + fn initializer(ref self: ComponentState, limit: u256) { + self.limit.write(limit); + } + + /// Checks if the user is allowed to spend `value` according to his weekly limit. + /// + /// # Arguments + /// + /// * `self` - The component state. + /// * `value` - The amount the user wants to spend. + /// + /// # Returns + /// + /// * `bool` - Is the user allowed to spend `value` according to the limit previously set. + fn is_allowed_to_spend(ref self: ComponentState, mut value: u256) -> bool { + if !self.is_below_limit(value) { + return false; + } + let block_timestamp = get_block_timestamp(); + + // Get the previous hour timestamp. + let rounded_down_hour = block_timestamp - (block_timestamp % HOUR_IN_SECONDS); + // Get the timestamp of 1 week before to compute all the expenses during that week. + let mut hour_index = rounded_down_hour - WEEK_IN_SECONDS; + while hour_index <= rounded_down_hour { + // Get the low value of the expenses for that hour. + let low = starknet::syscalls::storage_read_syscall( + // Using the timestamp as the storage address to avoid computing hashes. + // These will never collide as the time can only go forward. + // Unwrap can't fail because our value is a [u64] which is small enough. + 0, Into::::into(hour_index).try_into().unwrap() + ) + .unwrap_syscall() + .try_into() + .unwrap(); + // Get the high value of the expenses for that hour. + let high = starknet::syscalls::storage_read_syscall( + // Using the timestamp as the storage address to avoid computing hashes. + // These will never collide as the time can only go forward. + // Unwrap can't fail because our value is a [u64] which is small enough. + 0, Into::::into(hour_index + 1).try_into().unwrap() + ) + .unwrap_syscall() + .try_into() + // Can't panic because it was serialized as [u256] + .unwrap(); + value += u256 { low, high }; + hour_index += HOUR_IN_SECONDS; + }; + // Is value + week expenses under the weekly limit. + value <= self.limit.read() + } + + /// Checks if `value` is below limit. + /// This is mostly an internal function to avoid looping over the last week + /// if the value is above the limit. + #[inline(always)] + fn is_below_limit(self: @ComponentState, value: u256) -> bool { + value <= self.limit.read() + } + + /// Returns the 7 rolling days limit. + fn get_weekly_limit(self: @ComponentState) -> u256 { + self.limit.read() + } + + /// Sets the 7 rolling days limit. + fn set_daily_limit(ref self: ComponentState, new_limit: u256) { + self.limit.write(new_limit); + } + } +} + +// +// Tests +// + +#[cfg(test)] +mod test { + use vault::spending_limit::weekly_limit::WeeklyLimitComponent::InternalTrait; + use vault::spending_limit::weekly_limit::interface::IWeeklyLimit; + + #[starknet::contract] + mod mock_contract { + use super::super::WeeklyLimitComponent; + component!(path: WeeklyLimitComponent, storage: spending_limit, event: SpendingLimitEvent); + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + SpendingLimitEvent: WeeklyLimitComponent::Event, + } + #[storage] + struct Storage { + #[substorage(v0)] + spending_limit: WeeklyLimitComponent::Storage, + } + } + + type ComponentState = super::WeeklyLimitComponent::ComponentState; + + fn COMPONENT() -> ComponentState { + super::WeeklyLimitComponent::component_state_for_testing() + } + + #[test] + fn test_is_below_limit() { + let mut component = COMPONENT(); + // 0 <= 0 + assert!(component.is_below_limit(0)); + // 1 <= 0 + assert!(!component.is_below_limit(1)); + // Set limit to 2 + component.initializer(2); + // 1 <= 2 + assert!(component.is_below_limit(1)); + // 3 <= 2 + assert!(!component.is_below_limit(3)); + } + + #[test] + fn test_is_allowed_simple() { + starknet::testing::set_block_timestamp(8626176); + let mut component = COMPONENT(); + // NO PREVIOUS EXPENSES SO THE VALUE IS THE ONLY THING CONSIDERED. + // Not inialized so 0 <= 0 + assert!(component.is_allowed_to_spend(0)); + // 1 <= 0 + assert!(!component.is_allowed_to_spend(1)); + // Set limit to 2 + component.initializer(2); + // 1 <= 2 + assert!(component.is_allowed_to_spend(1)); + // 3 <= 2 + assert!(!component.is_allowed_to_spend(3)); + } +}