From cb3e4dde713edeb50222a4f5d9b8c0c9c3deb4f9 Mon Sep 17 00:00:00 2001 From: baitcode Date: Tue, 10 Dec 2024 23:58:48 +0300 Subject: [PATCH 01/40] Initial implementation of staked seconds calculation --- src/staker.cairo | 122 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 121 insertions(+), 1 deletion(-) diff --git a/src/staker.cairo b/src/staker.cairo index 4943f4c..b163a0b 100644 --- a/src/staker.cairo +++ b/src/staker.cairo @@ -52,15 +52,20 @@ pub trait IStaker { fn get_average_delegated_over_last( self: @TContractState, delegate: ContractAddress, period: u64, ) -> u128; + + // Gets the cumulative staked amount * per second staked for the given timestamp and account. + fn get_staked_per_second(self: @TContractState, from: ContractAddress, timestamp: u64) -> u128; } #[starknet::contract] pub mod Staker { - use core::num::traits::zero::{Zero}; + use starknet::storage::MutableVecTrait; +use core::num::traits::zero::{Zero}; use governance::interfaces::erc20::{IERC20Dispatcher, IERC20DispatcherTrait}; use starknet::storage::{ Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess, + Vec, VecTrait, }; use starknet::{ get_block_timestamp, get_caller_address, get_contract_address, @@ -96,6 +101,13 @@ pub mod Staker { } } + #[derive(Drop, Serde, starknet::Store)] + struct StakingLogRecord { + timestamp: u64, + total_staked: u128, + cumulative_staked_per_second: u128 + } + #[storage] struct Storage { token: IERC20Dispatcher, @@ -104,6 +116,8 @@ pub mod Staker { amount_delegated: Map, delegated_cumulative_num_snapshots: Map, delegated_cumulative_snapshot: Map>, + + staking_log: Map> } #[constructor] @@ -212,8 +226,96 @@ pub mod Staker { self.find_delegated_cumulative(delegate, mid, max_index_exclusive, timestamp) } } + + fn log_change(ref self: ContractState, delegate: ContractAddress, amount: u128, is_add: bool) { + let from = get_caller_address(); + let log = self.staking_log.entry(from); + + if let Option::Some(last_record_ptr) = log.get(log.len() - 1) { + let mut last_record = last_record_ptr.read(); + + let mut record = if last_record.timestamp == get_block_timestamp() { + // update record + last_record_ptr + } else { + // create new record + log.append() + }; + + // Might be zero + let time_diff = get_block_timestamp() - last_record.timestamp; + + let staked_seconds = last_record.total_staked * time_diff.into() / 1000; // staked seconds + + let total_staked = if is_add { + // overflow check + assert(last_record.total_staked + amount > last_record.total_staked, 'BAD AMOUNT'); + last_record.total_staked + amount + } else { + // underflow check + assert(last_record.total_staked > amount, 'BAD AMOUNT'); + last_record.total_staked - amount + }; + + // Add a new record. + record.write( + StakingLogRecord { + timestamp: get_block_timestamp(), + total_staked: total_staked, + cumulative_staked_per_second: last_record.cumulative_staked_per_second + staked_seconds, + } + ); + } else { + // Add the first record + if is_add { + log.append().write( + StakingLogRecord { + timestamp: get_block_timestamp(), + total_staked: amount, + cumulative_staked_per_second: 0, + } + ); + } else { + assert(false, 'IMPOSSIBRU'); // TODO: fix + } + } + } + + fn find_in_change_log(self: @ContractState, from: ContractAddress, timestamp: u64) -> Option { + // Find first log record in an array whos timestamp is less or equal to timestamp. + // Uses binary search. + + // TODO(baitcode): Should probably be an argument. But seems not possible. + let log = self.staking_log.entry(from); + + let mut left = 0; + let mut right = log.len() - 1; + + // To avoid reading from the storage multiple times. + let mut result_ptr = Option::None; + + while left <= right { + let center = (right - left) / 2; + let record = log.at(center); + + if record.timestamp.read() <= timestamp { + result_ptr = Option::Some(record); + left = center + 1; + } else { + right = center - 1; + } + }; + + if let Option::Some(result) = result_ptr { + return Option::Some(result.read()); + } + + return Option::None; + } } + + #[abi(embed_v0)] impl StakerImpl of IStaker { fn get_token(self: @ContractState) -> ContractAddress { @@ -253,6 +355,9 @@ pub mod Staker { self .amount_delegated .write(delegate, self.insert_snapshot(delegate, get_block_timestamp()) + amount); + + self.log_change(delegate, amount, true); + self.emit(Staked { from, delegate, amount }); } @@ -280,6 +385,9 @@ pub mod Staker { .amount_delegated .write(delegate, self.insert_snapshot(delegate, get_block_timestamp()) - amount); assert(self.token.read().transfer(recipient, amount.into()), 'TRANSFER_FAILED'); + + self.log_change(delegate, amount, false); + self.emit(Withdrawn { from, delegate, to: recipient, amount }); } @@ -333,5 +441,17 @@ pub mod Staker { let now = get_block_timestamp(); self.get_average_delegated(delegate, now - period, now) } + + fn get_staked_per_second( + self: @ContractState, from: ContractAddress, timestamp: u64, + ) -> u128 { + if let Option::Some(log_record) = self.find_in_change_log(from, timestamp) { + let time_diff = timestamp - log_record.timestamp; + let staked_seconds = log_record.total_staked * time_diff.into() / 1000; // staked seconds + return log_record.cumulative_staked_per_second + staked_seconds; + } else { + return 0; + } + } } } From 01b3f736355f48a03a30547f3624372177682304 Mon Sep 17 00:00:00 2001 From: baitcode Date: Wed, 11 Dec 2024 00:01:56 +0300 Subject: [PATCH 02/40] a bit of naming --- src/staker.cairo | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/staker.cairo b/src/staker.cairo index b163a0b..6ba00b9 100644 --- a/src/staker.cairo +++ b/src/staker.cairo @@ -54,7 +54,7 @@ pub trait IStaker { ) -> u128; // Gets the cumulative staked amount * per second staked for the given timestamp and account. - fn get_staked_per_second(self: @TContractState, from: ContractAddress, timestamp: u64) -> u128; + fn get_staked_seconds(self: @TContractState, for_address: ContractAddress, at_ts: u64) -> u128; } #[starknet::contract] @@ -442,11 +442,11 @@ use core::num::traits::zero::{Zero}; self.get_average_delegated(delegate, now - period, now) } - fn get_staked_per_second( - self: @ContractState, from: ContractAddress, timestamp: u64, + fn get_staked_seconds( + self: @ContractState, for_address: ContractAddress, at_ts: u64, ) -> u128 { - if let Option::Some(log_record) = self.find_in_change_log(from, timestamp) { - let time_diff = timestamp - log_record.timestamp; + if let Option::Some(log_record) = self.find_in_change_log(for_address, at_ts) { + let time_diff = at_ts - log_record.timestamp; let staked_seconds = log_record.total_staked * time_diff.into() / 1000; // staked seconds return log_record.cumulative_staked_per_second + staked_seconds; } else { From 924aef370e9adfc6791acf5d8951203843448eb0 Mon Sep 17 00:00:00 2001 From: baitcode Date: Wed, 11 Dec 2024 00:17:27 +0300 Subject: [PATCH 03/40] added upgrade method. NOTE: constructor changed. Staker now requires governor. --- src/staker.cairo | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/staker.cairo b/src/staker.cairo index 6ba00b9..bf61a0b 100644 --- a/src/staker.cairo +++ b/src/staker.cairo @@ -1,4 +1,4 @@ -use starknet::{ContractAddress}; +use starknet::{ContractAddress, ClassHash}; #[starknet::interface] pub trait IStaker { @@ -55,6 +55,9 @@ pub trait IStaker { // Gets the cumulative staked amount * per second staked for the given timestamp and account. fn get_staked_seconds(self: @TContractState, for_address: ContractAddress, at_ts: u64) -> u128; + + // Replaces the code at this address. This must be self-called via a governor proposal. + fn upgrade(ref self: TContractState, class_hash: ClassHash); } #[starknet::contract] @@ -67,9 +70,10 @@ use core::num::traits::zero::{Zero}; StoragePointerReadAccess, StoragePointerWriteAccess, Vec, VecTrait, }; + use starknet::{ get_block_timestamp, get_caller_address, get_contract_address, - storage_access::{StorePacking}, + replace_class_syscall, storage_access::{StorePacking}, ClassHash, }; use super::{ContractAddress, IStaker}; @@ -111,6 +115,7 @@ use core::num::traits::zero::{Zero}; #[storage] struct Storage { token: IERC20Dispatcher, + governor: ContractAddress, // owner, delegate => amount staked: Map<(ContractAddress, ContractAddress), u128>, amount_delegated: Map, @@ -121,8 +126,9 @@ use core::num::traits::zero::{Zero}; } #[constructor] - fn constructor(ref self: ContractState, token: IERC20Dispatcher) { + fn constructor(ref self: ContractState, token: IERC20Dispatcher, governor: ContractAddress) { self.token.write(token); + self.governor.write(governor); } #[derive(starknet::Event, PartialEq, Debug, Drop)] @@ -281,6 +287,12 @@ use core::num::traits::zero::{Zero}; } } + fn check_governor_call(self: @ContractState) { + assert(self.governor.read().is_non_zero(), 'GOVERNOR_UNDEFINED'); + assert(get_caller_address().is_non_zero(), 'GOVERNOR_ONLY'); + assert(get_caller_address() == self.governor.read(), 'GOVERNOR_ONLY'); + } + fn find_in_change_log(self: @ContractState, from: ContractAddress, timestamp: u64) -> Option { // Find first log record in an array whos timestamp is less or equal to timestamp. // Uses binary search. @@ -453,5 +465,13 @@ use core::num::traits::zero::{Zero}; return 0; } } + + fn upgrade(ref self: ContractState, class_hash: ClassHash) { + assert(class_hash.is_non_zero(), 'INVALID_CLASS_HASH'); + self.check_governor_call(); + replace_class_syscall(class_hash).unwrap(); + + // TODO(baitcode): Ideally we should emit an event here. + } } } From 88f8db8c4e08589503000635044832789265f0d8 Mon Sep 17 00:00:00 2001 From: baitcode Date: Wed, 11 Dec 2024 00:20:41 +0300 Subject: [PATCH 04/40] fix import --- src/staker.cairo | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/staker.cairo b/src/staker.cairo index bf61a0b..6daf0d3 100644 --- a/src/staker.cairo +++ b/src/staker.cairo @@ -63,7 +63,7 @@ pub trait IStaker { #[starknet::contract] pub mod Staker { use starknet::storage::MutableVecTrait; -use core::num::traits::zero::{Zero}; + use core::num::traits::zero::{Zero}; use governance::interfaces::erc20::{IERC20Dispatcher, IERC20DispatcherTrait}; use starknet::storage::{ Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePathEntry, @@ -73,7 +73,7 @@ use core::num::traits::zero::{Zero}; use starknet::{ get_block_timestamp, get_caller_address, get_contract_address, - replace_class_syscall, storage_access::{StorePacking}, ClassHash, + syscalls::{replace_class_syscall}, storage_access::{StorePacking}, ClassHash, }; use super::{ContractAddress, IStaker}; From 9553035d3a18cd7a1207a963aba2a0ca231f2c1f Mon Sep 17 00:00:00 2001 From: baitcode Date: Wed, 11 Dec 2024 00:41:29 +0300 Subject: [PATCH 05/40] TODO: removal --- src/staker.cairo | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/staker.cairo b/src/staker.cairo index 6daf0d3..431c217 100644 --- a/src/staker.cairo +++ b/src/staker.cairo @@ -272,18 +272,16 @@ pub mod Staker { } ); } else { - // Add the first record - if is_add { - log.append().write( - StakingLogRecord { - timestamp: get_block_timestamp(), - total_staked: amount, - cumulative_staked_per_second: 0, - } - ); - } else { - assert(false, 'IMPOSSIBRU'); // TODO: fix - } + // Add the first record. If withdrawal, then it's underflow. + assert(is_add, 'BAD AMOUNT'); + + log.append().write( + StakingLogRecord { + timestamp: get_block_timestamp(), + total_staked: amount, + cumulative_staked_per_second: 0, + } + ); } } From aedb403a1c66c15ede1b41cbe439a3fffa62da5c Mon Sep 17 00:00:00 2001 From: baitcode Date: Wed, 11 Dec 2024 00:45:08 +0300 Subject: [PATCH 06/40] remove todo --- src/staker.cairo | 1 - 1 file changed, 1 deletion(-) diff --git a/src/staker.cairo b/src/staker.cairo index 431c217..3a7759a 100644 --- a/src/staker.cairo +++ b/src/staker.cairo @@ -295,7 +295,6 @@ pub mod Staker { // Find first log record in an array whos timestamp is less or equal to timestamp. // Uses binary search. - // TODO(baitcode): Should probably be an argument. But seems not possible. let log = self.staking_log.entry(from); let mut left = 0; From 9ee93244e7bbe4ccb84ef137e539f7489897600c Mon Sep 17 00:00:00 2001 From: baitcode Date: Wed, 11 Dec 2024 21:58:08 +0300 Subject: [PATCH 07/40] test --- src/staker.cairo | 18 ++++++------ src/staker_test.cairo | 66 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 9 deletions(-) diff --git a/src/staker.cairo b/src/staker.cairo index 3a7759a..729251a 100644 --- a/src/staker.cairo +++ b/src/staker.cairo @@ -54,7 +54,7 @@ pub trait IStaker { ) -> u128; // Gets the cumulative staked amount * per second staked for the given timestamp and account. - fn get_staked_seconds(self: @TContractState, for_address: ContractAddress, at_ts: u64) -> u128; + fn get_staked_seconds_at(self: @TContractState, owner: ContractAddress, timestamp: u64) -> u128; // Replaces the code at this address. This must be self-called via a governor proposal. fn upgrade(ref self: TContractState, class_hash: ClassHash); @@ -109,7 +109,7 @@ pub mod Staker { struct StakingLogRecord { timestamp: u64, total_staked: u128, - cumulative_staked_per_second: u128 + cumulative_staked_seconds: u128 } #[storage] @@ -268,7 +268,7 @@ pub mod Staker { StakingLogRecord { timestamp: get_block_timestamp(), total_staked: total_staked, - cumulative_staked_per_second: last_record.cumulative_staked_per_second + staked_seconds, + cumulative_staked_seconds: last_record.cumulative_staked_seconds + staked_seconds, } ); } else { @@ -279,7 +279,7 @@ pub mod Staker { StakingLogRecord { timestamp: get_block_timestamp(), total_staked: amount, - cumulative_staked_per_second: 0, + cumulative_staked_seconds: 0, } ); } @@ -451,13 +451,13 @@ pub mod Staker { self.get_average_delegated(delegate, now - period, now) } - fn get_staked_seconds( - self: @ContractState, for_address: ContractAddress, at_ts: u64, + fn get_staked_seconds_at( + self: @ContractState, owner: ContractAddress, timestamp: u64, ) -> u128 { - if let Option::Some(log_record) = self.find_in_change_log(for_address, at_ts) { - let time_diff = at_ts - log_record.timestamp; + if let Option::Some(log_record) = self.find_in_change_log(owner, timestamp) { + let time_diff = timestamp - log_record.timestamp; let staked_seconds = log_record.total_staked * time_diff.into() / 1000; // staked seconds - return log_record.cumulative_staked_per_second + staked_seconds; + return log_record.cumulative_staked_seconds + staked_seconds; } else { return 0; } diff --git a/src/staker_test.cairo b/src/staker_test.cairo index faf0f74..c6e1e4a 100644 --- a/src/staker_test.cairo +++ b/src/staker_test.cairo @@ -11,6 +11,7 @@ use starknet::{contract_address_const, get_contract_address, syscalls::deploy_sy pub(crate) fn setup(amount: u256) -> (IStakerDispatcher, IERC20Dispatcher) { let token = deploy_token(get_contract_address(), amount); + let (staker_address, _) = deploy_syscall( Staker::TEST_CLASS_HASH.try_into().unwrap(), 0, @@ -394,3 +395,68 @@ fn test_delegate_undelegate() { assert(staker.get_delegated_at(delegatee, timestamp: 5) == 12345, 'at 5'); assert(staker.get_delegated_at(delegatee, timestamp: 6) == 0, 'at 6'); } + +mod staker_staked_seconds_calculation { + use starknet::{ + get_caller_address + }; + use super::{ + setup, contract_address_const, set_block_timestamp, + IERC20DispatcherTrait, IStakerDispatcherTrait + }; + + #[test] + fn test_should_stake_10000_tokens_for_5_seconds_adding_10000_every_second_to_staked_seconds() { + let (staker, token) = setup(1000); + + // Caller is token owner + let token_owner = get_caller_address(); + + // Allow staker to spend 10000 tokens from owner account + token.approve(staker.contract_address, 10000); + + // Adress to delegate tokens to + let delegatee = contract_address_const::<1234567890>(); + + set_block_timestamp(0); + staker.stake(delegatee); // Will transfer 10000 tokens to contract account and setup delegatee + + set_block_timestamp(5000); // 5 seconds passed + staker.withdraw(delegatee, token_owner); // Will withdraw all 10000 tokens back to owner + + set_block_timestamp(10000); + assert(staker.get_staked(delegatee, token_owner) == 0, 'Not all tokens were withdrawn'); + + assert(staker.get_staked_seconds_at(token_owner, 0) == 0, 'At 0 should be 0'); + assert(staker.get_staked_seconds_at(token_owner, 1000) == 10000, 'At 1s should be 10000 tok*sec'); + assert(staker.get_staked_seconds_at(token_owner, 1100) == 10000, 'At 1.1s should be 10000 tok*sec'); + assert(staker.get_staked_seconds_at(token_owner, 1500) == 10000, 'At 1.5s should be 10000 tok*sec'); + assert(staker.get_staked_seconds_at(token_owner, 1900) == 10000, 'At 1.9s should be 10000 tok*sec'); + assert(staker.get_staked_seconds_at(token_owner, 2000) == 20000, 'At 2s should be 20000 tok*sec'); + assert(staker.get_staked_seconds_at(token_owner, 3000) == 30000, 'At 3s should be 30000 tok*sec'); + assert(staker.get_staked_seconds_at(token_owner, 4000) == 40000, 'At 4s should be 40000 tok*sec'); + assert(staker.get_staked_seconds_at(token_owner, 5000) == 50000, 'At 5s should be 50000 tok*sec'); + assert(staker.get_staked_seconds_at(token_owner, 6000) == 50000, 'At 6s should be 50000 tok*sec'); + assert(staker.get_staked_seconds_at(token_owner, 7000) == 50000, 'At 7s should be 50000 tok*sec'); + } + + #[test] + #[should_panic(expected: ('FUTURE', 'ENTRYPOINT_FAILED'))] + fn test_raises_error_if_no_history_exists_and_withdrawal_happens() { + let (staker, token) = setup(1000); + + // Caller is token owner + let token_owner = get_caller_address(); + + // Allow staker to spend 10000 tokens from owner account + token.approve(staker.contract_address, 10000); + + // Adress to delegate tokens to + let delegatee = contract_address_const::<1234567890>(); + + set_block_timestamp(5000); // 5 seconds passed + staker.withdraw(delegatee, token_owner); // Will withdraw all 10000 tokens back to owner + } + + +} \ No newline at end of file From c3c0244b60024e5756b3832f68d1726d48f2a399 Mon Sep 17 00:00:00 2001 From: baitcode Date: Wed, 11 Dec 2024 22:26:14 +0300 Subject: [PATCH 08/40] local settings --- .gitignore | 4 ++++ declare-all.sh | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 116566e..aee9b44 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +.env +.vscode +.starkli + ####### Scarb ####### diff --git a/declare-all.sh b/declare-all.sh index f098548..4cb9642 100755 --- a/declare-all.sh +++ b/declare-all.sh @@ -37,7 +37,11 @@ scarb build declare_class_hash() { local class_name=$1 - starkli declare --watch --network "$NETWORK" --keystore-password "$STARKNET_KEYSTORE_PASSWORD" --casm-file "target/dev/governance_${class_name}.compiled_contract_class.json" "target/dev/governance_${class_name}.contract_class.json" + starkli declare --watch \ + --network "$NETWORK" \ + --keystore-password "$STARKNET_KEYSTORE_PASSWORD" \ + --casm-file "target/dev/governance_${class_name}.compiled_contract_class.json" \ + "target/dev/governance_${class_name}.contract_class.json" } echo "Declaring AirdropClaimCheck" From 692c36abfa01ed3a66fa82baedc74243481c9bca Mon Sep 17 00:00:00 2001 From: baitcode Date: Wed, 11 Dec 2024 22:50:03 +0300 Subject: [PATCH 09/40] revert upgrade code for now --- src/staker.cairo | 37 ++++++++++++++++++------------------- src/staker_test.cairo | 11 +++++++++-- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/staker.cairo b/src/staker.cairo index 729251a..68762d9 100644 --- a/src/staker.cairo +++ b/src/staker.cairo @@ -57,18 +57,17 @@ pub trait IStaker { fn get_staked_seconds_at(self: @TContractState, owner: ContractAddress, timestamp: u64) -> u128; // Replaces the code at this address. This must be self-called via a governor proposal. - fn upgrade(ref self: TContractState, class_hash: ClassHash); + // fn upgrade(ref self: TContractState, class_hash: ClassHash); } #[starknet::contract] pub mod Staker { - use starknet::storage::MutableVecTrait; use core::num::traits::zero::{Zero}; use governance::interfaces::erc20::{IERC20Dispatcher, IERC20DispatcherTrait}; use starknet::storage::{ Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess, - Vec, VecTrait, + Vec, VecTrait, MutableVecTrait, }; use starknet::{ @@ -115,20 +114,20 @@ pub mod Staker { #[storage] struct Storage { token: IERC20Dispatcher, - governor: ContractAddress, + // governor: ContractAddress, // owner, delegate => amount staked: Map<(ContractAddress, ContractAddress), u128>, amount_delegated: Map, delegated_cumulative_num_snapshots: Map, delegated_cumulative_snapshot: Map>, - - staking_log: Map> + staking_log: Map>, } #[constructor] - fn constructor(ref self: ContractState, token: IERC20Dispatcher, governor: ContractAddress) { + fn constructor(ref self: ContractState, token: IERC20Dispatcher) { + // , governor: ContractAddress self.token.write(token); - self.governor.write(governor); + // self.governor.write(governor); } #[derive(starknet::Event, PartialEq, Debug, Drop)] @@ -285,11 +284,11 @@ pub mod Staker { } } - fn check_governor_call(self: @ContractState) { - assert(self.governor.read().is_non_zero(), 'GOVERNOR_UNDEFINED'); - assert(get_caller_address().is_non_zero(), 'GOVERNOR_ONLY'); - assert(get_caller_address() == self.governor.read(), 'GOVERNOR_ONLY'); - } + // fn check_governor_call(self: @ContractState) { + // assert(self.governor.read().is_non_zero(), 'GOVERNOR_UNDEFINED'); + // assert(get_caller_address().is_non_zero(), 'GOVERNOR_ONLY'); + // assert(get_caller_address() == self.governor.read(), 'GOVERNOR_ONLY'); + // } fn find_in_change_log(self: @ContractState, from: ContractAddress, timestamp: u64) -> Option { // Find first log record in an array whos timestamp is less or equal to timestamp. @@ -463,12 +462,12 @@ pub mod Staker { } } - fn upgrade(ref self: ContractState, class_hash: ClassHash) { - assert(class_hash.is_non_zero(), 'INVALID_CLASS_HASH'); - self.check_governor_call(); - replace_class_syscall(class_hash).unwrap(); + // fn upgrade(ref self: ContractState, class_hash: ClassHash) { + // assert(class_hash.is_non_zero(), 'INVALID_CLASS_HASH'); + // self.check_governor_call(); + // replace_class_syscall(class_hash).unwrap(); - // TODO(baitcode): Ideally we should emit an event here. - } + // // TODO(baitcode): Ideally we should emit an event here. + // } } } diff --git a/src/staker_test.cairo b/src/staker_test.cairo index c6e1e4a..aabbdae 100644 --- a/src/staker_test.cairo +++ b/src/staker_test.cairo @@ -7,13 +7,20 @@ use governance::staker::{ }; use governance::test::test_token::{TestToken, deploy as deploy_token}; use starknet::testing::{pop_log, set_block_timestamp}; -use starknet::{contract_address_const, get_contract_address, syscalls::deploy_syscall}; +use starknet::{ + ClassHash, + contract_address_const, + get_contract_address, + syscalls::deploy_syscall +}; pub(crate) fn setup(amount: u256) -> (IStakerDispatcher, IERC20Dispatcher) { let token = deploy_token(get_contract_address(), amount); + let classHash: ClassHash = Staker::TEST_CLASS_HASH.try_into().unwrap(); + let (staker_address, _) = deploy_syscall( - Staker::TEST_CLASS_HASH.try_into().unwrap(), + classHash, 0, array![token.contract_address.into()].span(), true, From 349919eccb8195c3b610d21b45395f434f77aaf9 Mon Sep 17 00:00:00 2001 From: baitcode Date: Wed, 11 Dec 2024 22:54:16 +0300 Subject: [PATCH 10/40] removed upgrade due to cyclic dependency with governor --- src/staker.cairo | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/src/staker.cairo b/src/staker.cairo index 68762d9..0df0228 100644 --- a/src/staker.cairo +++ b/src/staker.cairo @@ -56,8 +56,6 @@ pub trait IStaker { // Gets the cumulative staked amount * per second staked for the given timestamp and account. fn get_staked_seconds_at(self: @TContractState, owner: ContractAddress, timestamp: u64) -> u128; - // Replaces the code at this address. This must be self-called via a governor proposal. - // fn upgrade(ref self: TContractState, class_hash: ClassHash); } #[starknet::contract] @@ -114,7 +112,6 @@ pub mod Staker { #[storage] struct Storage { token: IERC20Dispatcher, - // governor: ContractAddress, // owner, delegate => amount staked: Map<(ContractAddress, ContractAddress), u128>, amount_delegated: Map, @@ -125,9 +122,7 @@ pub mod Staker { #[constructor] fn constructor(ref self: ContractState, token: IERC20Dispatcher) { - // , governor: ContractAddress self.token.write(token); - // self.governor.write(governor); } #[derive(starknet::Event, PartialEq, Debug, Drop)] @@ -364,7 +359,7 @@ pub mod Staker { .amount_delegated .write(delegate, self.insert_snapshot(delegate, get_block_timestamp()) + amount); - self.log_change(delegate, amount, true); + // self.log_change(delegate, amount, true); self.emit(Staked { from, delegate, amount }); } @@ -394,7 +389,7 @@ pub mod Staker { .write(delegate, self.insert_snapshot(delegate, get_block_timestamp()) - amount); assert(self.token.read().transfer(recipient, amount.into()), 'TRANSFER_FAILED'); - self.log_change(delegate, amount, false); + // self.log_change(delegate, amount, false); self.emit(Withdrawn { from, delegate, to: recipient, amount }); } @@ -461,13 +456,5 @@ pub mod Staker { return 0; } } - - // fn upgrade(ref self: ContractState, class_hash: ClassHash) { - // assert(class_hash.is_non_zero(), 'INVALID_CLASS_HASH'); - // self.check_governor_call(); - // replace_class_syscall(class_hash).unwrap(); - - // // TODO(baitcode): Ideally we should emit an event here. - // } } } From d6504e9ec2f99b48832d074d988e05ef42f1e722 Mon Sep 17 00:00:00 2001 From: baitcode Date: Thu, 12 Dec 2024 00:25:08 +0300 Subject: [PATCH 11/40] finalise --- src/staker.cairo | 114 +++++++++++++++++++++--------------------- src/staker_test.cairo | 84 +++++++++++++++++++++++++++++-- 2 files changed, 136 insertions(+), 62 deletions(-) diff --git a/src/staker.cairo b/src/staker.cairo index 0df0228..5dfbd5d 100644 --- a/src/staker.cairo +++ b/src/staker.cairo @@ -1,4 +1,4 @@ -use starknet::{ContractAddress, ClassHash}; +use starknet::{ContractAddress}; #[starknet::interface] pub trait IStaker { @@ -54,7 +54,7 @@ pub trait IStaker { ) -> u128; // Gets the cumulative staked amount * per second staked for the given timestamp and account. - fn get_staked_seconds_at(self: @TContractState, owner: ContractAddress, timestamp: u64) -> u128; + fn get_staked_seconds_at(self: @TContractState, staker: ContractAddress, timestamp: u64) -> u128; } @@ -70,7 +70,7 @@ pub mod Staker { use starknet::{ get_block_timestamp, get_caller_address, get_contract_address, - syscalls::{replace_class_syscall}, storage_access::{StorePacking}, ClassHash, + storage_access::{StorePacking}, }; use super::{ContractAddress, IStaker}; @@ -227,45 +227,10 @@ pub mod Staker { } } - fn log_change(ref self: ContractState, delegate: ContractAddress, amount: u128, is_add: bool) { - let from = get_caller_address(); - let log = self.staking_log.entry(from); - - if let Option::Some(last_record_ptr) = log.get(log.len() - 1) { - let mut last_record = last_record_ptr.read(); - - let mut record = if last_record.timestamp == get_block_timestamp() { - // update record - last_record_ptr - } else { - // create new record - log.append() - }; - - // Might be zero - let time_diff = get_block_timestamp() - last_record.timestamp; - - let staked_seconds = last_record.total_staked * time_diff.into() / 1000; // staked seconds - - let total_staked = if is_add { - // overflow check - assert(last_record.total_staked + amount > last_record.total_staked, 'BAD AMOUNT'); - last_record.total_staked + amount - } else { - // underflow check - assert(last_record.total_staked > amount, 'BAD AMOUNT'); - last_record.total_staked - amount - }; + fn log_change(ref self: ContractState, staker: ContractAddress, amount: u128, is_add: bool) { + let log = self.staking_log.entry(staker); - // Add a new record. - record.write( - StakingLogRecord { - timestamp: get_block_timestamp(), - total_staked: total_staked, - cumulative_staked_seconds: last_record.cumulative_staked_seconds + staked_seconds, - } - ); - } else { + if log.len() == 0 { // Add the first record. If withdrawal, then it's underflow. assert(is_add, 'BAD AMOUNT'); @@ -276,20 +241,56 @@ pub mod Staker { cumulative_staked_seconds: 0, } ); + + return; } - } - // fn check_governor_call(self: @ContractState) { - // assert(self.governor.read().is_non_zero(), 'GOVERNOR_UNDEFINED'); - // assert(get_caller_address().is_non_zero(), 'GOVERNOR_ONLY'); - // assert(get_caller_address() == self.governor.read(), 'GOVERNOR_ONLY'); - // } + let last_record_ptr = log.at(log.len() - 1); + + let mut last_record = last_record_ptr.read(); - fn find_in_change_log(self: @ContractState, from: ContractAddress, timestamp: u64) -> Option { + let mut record = if last_record.timestamp == get_block_timestamp() { + // update record + last_record_ptr + } else { + // create new record + log.append() + }; + + // Might be zero + let seconds_diff = (get_block_timestamp() - last_record.timestamp) / 1000; + + let staked_seconds = last_record.total_staked * seconds_diff.into(); // staked seconds + + let total_staked = if is_add { + // overflow check + assert(last_record.total_staked + amount >= last_record.total_staked, 'BAD AMOUNT'); + last_record.total_staked + amount + } else { + // underflow check + assert(last_record.total_staked >= amount, 'BAD AMOUNT'); + last_record.total_staked - amount + }; + + // Add a new record. + record.write( + StakingLogRecord { + timestamp: get_block_timestamp(), + total_staked: total_staked, + cumulative_staked_seconds: last_record.cumulative_staked_seconds + staked_seconds, + } + ); + } + + fn find_in_change_log(self: @ContractState, staker: ContractAddress, timestamp: u64) -> Option { // Find first log record in an array whos timestamp is less or equal to timestamp. // Uses binary search. - let log = self.staking_log.entry(from); + let log = self.staking_log.entry(staker); + + if log.len() == 0 { + return Option::None; + } let mut left = 0; let mut right = log.len() - 1; @@ -298,7 +299,7 @@ pub mod Staker { let mut result_ptr = Option::None; while left <= right { - let center = (right - left) / 2; + let center = (right + left) / 2; let record = log.at(center); if record.timestamp.read() <= timestamp { @@ -318,7 +319,6 @@ pub mod Staker { } - #[abi(embed_v0)] impl StakerImpl of IStaker { fn get_token(self: @ContractState) -> ContractAddress { @@ -359,7 +359,7 @@ pub mod Staker { .amount_delegated .write(delegate, self.insert_snapshot(delegate, get_block_timestamp()) + amount); - // self.log_change(delegate, amount, true); + self.log_change(from, amount, true); self.emit(Staked { from, delegate, amount }); } @@ -389,7 +389,7 @@ pub mod Staker { .write(delegate, self.insert_snapshot(delegate, get_block_timestamp()) - amount); assert(self.token.read().transfer(recipient, amount.into()), 'TRANSFER_FAILED'); - // self.log_change(delegate, amount, false); + self.log_change(from, amount, false); self.emit(Withdrawn { from, delegate, to: recipient, amount }); } @@ -446,11 +446,11 @@ pub mod Staker { } fn get_staked_seconds_at( - self: @ContractState, owner: ContractAddress, timestamp: u64, + self: @ContractState, staker: ContractAddress, timestamp: u64, ) -> u128 { - if let Option::Some(log_record) = self.find_in_change_log(owner, timestamp) { - let time_diff = timestamp - log_record.timestamp; - let staked_seconds = log_record.total_staked * time_diff.into() / 1000; // staked seconds + if let Option::Some(log_record) = self.find_in_change_log(staker, timestamp) { + let seconds_diff = (timestamp - log_record.timestamp) / 1000; + let staked_seconds = log_record.total_staked * seconds_diff.into(); // staked seconds return log_record.cumulative_staked_seconds + staked_seconds; } else { return 0; diff --git a/src/staker_test.cairo b/src/staker_test.cairo index aabbdae..b9cbff9 100644 --- a/src/staker_test.cairo +++ b/src/staker_test.cairo @@ -412,14 +412,22 @@ mod staker_staked_seconds_calculation { IERC20DispatcherTrait, IStakerDispatcherTrait }; + #[test] + fn test_should_return_0_if_no_data_found() { + let (staker, _) = setup(10000); + let token_owner = get_caller_address(); + assert(staker.get_staked_seconds_at(token_owner, 0) == 0, 'At 0 should be 0'); + assert(staker.get_staked_seconds_at(token_owner, 1000) == 0, 'At 1000 should be 0'); + } + #[test] fn test_should_stake_10000_tokens_for_5_seconds_adding_10000_every_second_to_staked_seconds() { - let (staker, token) = setup(1000); + let (staker, token) = setup(10000); // Caller is token owner let token_owner = get_caller_address(); - // Allow staker to spend 10000 tokens from owner account + // Allow staker contract to spend 10000 tokens from owner account token.approve(staker.contract_address, 10000); // Adress to delegate tokens to @@ -429,6 +437,9 @@ mod staker_staked_seconds_calculation { staker.stake(delegatee); // Will transfer 10000 tokens to contract account and setup delegatee set_block_timestamp(5000); // 5 seconds passed + + assert(staker.get_staked(token_owner, delegatee) == 10000, 'Something went wrong'); + staker.withdraw(delegatee, token_owner); // Will withdraw all 10000 tokens back to owner set_block_timestamp(10000); @@ -448,9 +459,73 @@ mod staker_staked_seconds_calculation { } #[test] - #[should_panic(expected: ('FUTURE', 'ENTRYPOINT_FAILED'))] + fn test_should_stake_1_tokens_for_5_seconds_adding_1_every_second_rounding_down() { + let (staker, token) = setup(1); + + // Caller is token owner + let token_owner = get_caller_address(); + + // Allow staker contract to spend 1 tokens from owner account + token.approve(staker.contract_address, 1); + + // Adress to delegate tokens to + let delegatee = contract_address_const::<1234567890>(); + + set_block_timestamp(0); + staker.stake(delegatee); // Will transfer 10000 tokens to contract account and setup delegatee + + set_block_timestamp(5000); // 5 seconds passed + + assert(staker.get_staked(token_owner, delegatee) == 1, 'Something went wrong'); + + staker.withdraw(delegatee, token_owner); // Will withdraw all 10000 tokens back to owner + + set_block_timestamp(10000); + assert(staker.get_staked(delegatee, token_owner) == 0, 'Not all tokens were withdrawn'); + + assert(staker.get_staked_seconds_at(token_owner, 0) == 0, 'At 0 should be 0'); + assert(staker.get_staked_seconds_at(token_owner, 1000) == 1, 'At 1s should be 1 tok*sec'); + assert(staker.get_staked_seconds_at(token_owner, 1100) == 1, 'At 1.1s should be 1 tok*sec'); + assert(staker.get_staked_seconds_at(token_owner, 1500) == 1, 'At 1.5s should be 1 tok*sec'); + assert(staker.get_staked_seconds_at(token_owner, 1900) == 1, 'At 1.9s should be 1 tok*sec'); + assert(staker.get_staked_seconds_at(token_owner, 2000) == 2, 'At 2s should be 2 tok*sec'); + assert(staker.get_staked_seconds_at(token_owner, 3000) == 3, 'At 3s should be 3 tok*sec'); + assert(staker.get_staked_seconds_at(token_owner, 4000) == 4, 'At 4s should be 4 tok*sec'); + assert(staker.get_staked_seconds_at(token_owner, 5000) == 5, 'At 5s should be 5 tok*sec'); + assert(staker.get_staked_seconds_at(token_owner, 6000) == 5, 'At 6s should be 5 tok*sec'); + assert(staker.get_staked_seconds_at(token_owner, 7000) == 5, 'At 7s should be 5 tok*sec'); + } + + #[test] + #[should_panic(expected: ('INSUFFICIENT_AMOUNT_STAKED', 'ENTRYPOINT_FAILED'))] fn test_raises_error_if_no_history_exists_and_withdrawal_happens() { - let (staker, token) = setup(1000); + // TODO(biatcode): This test accidentally tests other + // functionality and should be refactored + + let (staker, token) = setup(10000); + + // Caller is token owner + let token_owner = get_caller_address(); + + // Adress to delegate tokens to + let delegatee = contract_address_const::<1234567890>(); + + token.approve(staker.contract_address, 10000); + + set_block_timestamp(1000); + staker.stake_amount(delegatee, 1000); + set_block_timestamp(2000); + staker.withdraw_amount(delegatee, token_owner, 500); + set_block_timestamp(3000); + staker.stake_amount(delegatee, 1000); + set_block_timestamp(4000); + staker.withdraw_amount(delegatee, token_owner, 2000); + } + + #[test] + #[should_panic(expected: ('BAD AMOUNT', 'ENTRYPOINT_FAILED'))] + fn test_raises_error_if_withdrawn_more_than_staked() { + let (staker, token) = setup(10000); // Caller is token owner let token_owner = get_caller_address(); @@ -465,5 +540,4 @@ mod staker_staked_seconds_calculation { staker.withdraw(delegatee, token_owner); // Will withdraw all 10000 tokens back to owner } - } \ No newline at end of file From 5f323266d56318af6c33b85ee1445899085f16fd Mon Sep 17 00:00:00 2001 From: baitcode Date: Thu, 12 Dec 2024 00:33:19 +0300 Subject: [PATCH 12/40] typo --- src/staker_test.cairo | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/staker_test.cairo b/src/staker_test.cairo index b9cbff9..6a61d2a 100644 --- a/src/staker_test.cairo +++ b/src/staker_test.cairo @@ -17,10 +17,10 @@ use starknet::{ pub(crate) fn setup(amount: u256) -> (IStakerDispatcher, IERC20Dispatcher) { let token = deploy_token(get_contract_address(), amount); - let classHash: ClassHash = Staker::TEST_CLASS_HASH.try_into().unwrap(); + let class_hash: ClassHash = Staker::TEST_CLASS_HASH.try_into().unwrap(); let (staker_address, _) = deploy_syscall( - classHash, + class_hash, 0, array![token.contract_address.into()].span(), true, From b5ac50b0fcf2a18aa9547c6e46552495f21c6b8d Mon Sep 17 00:00:00 2001 From: baitcode Date: Fri, 13 Dec 2024 21:55:04 +0300 Subject: [PATCH 13/40] Added UFixedPoint type for unsigned FixedPoint operations. Currently support Q128.128 FP. Added cumulative seconds per total staked history value, and getter for that value Added method that allows to calculate how much staked seconds would staked token amount generate in between 2 timestamps: `calculate_staked_seconds_for_amount_between` Added tiny amount of tests --- src/lib.cairo | 3 + src/staker.cairo | 82 ++++++++++++------ src/utils/fp.cairo | 186 ++++++++++++++++++++++++++++++++++++++++ src/utils/fp_test.cairo | 45 ++++++++++ 4 files changed, 290 insertions(+), 26 deletions(-) create mode 100644 src/utils/fp.cairo create mode 100644 src/utils/fp_test.cairo diff --git a/src/lib.cairo b/src/lib.cairo index 5c79614..e73c083 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -24,6 +24,9 @@ mod interfaces { } mod utils { pub(crate) mod exp2; + pub(crate) mod fp; + #[cfg(test)] + pub(crate) mod fp_test; } #[cfg(test)] diff --git a/src/staker.cairo b/src/staker.cairo index 5dfbd5d..d9739af 100644 --- a/src/staker.cairo +++ b/src/staker.cairo @@ -1,4 +1,6 @@ use starknet::{ContractAddress}; +use crate::utils::fp::{UFixedPoint}; + #[starknet::interface] pub trait IStaker { @@ -54,25 +56,31 @@ pub trait IStaker { ) -> u128; // Gets the cumulative staked amount * per second staked for the given timestamp and account. - fn get_staked_seconds_at(self: @TContractState, staker: ContractAddress, timestamp: u64) -> u128; + fn get_cumulative_seconds_per_total_staked_at(self: @TContractState, timestamp: u64) -> UFixedPoint; + // Gets the cumulative staked amount * per second staked for the given timestamp and account. + fn calculate_staked_seconds_for_amount_between(self: @TContractState, token_amount: u128, start_at: u64, end_at: u64) -> u128; } + #[starknet::contract] pub mod Staker { + use starknet::storage::StorageAsPath; +use super::super::utils::fp::UFixedPointTrait; use core::num::traits::zero::{Zero}; use governance::interfaces::erc20::{IERC20Dispatcher, IERC20DispatcherTrait}; use starknet::storage::{ - Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePathEntry, + Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePathEntry, StoragePath, StoragePointerReadAccess, StoragePointerWriteAccess, Vec, VecTrait, MutableVecTrait, }; + use crate::utils::fp::{UFixedPoint, UFixedPointZero}; use starknet::{ get_block_timestamp, get_caller_address, get_contract_address, - storage_access::{StorePacking}, + storage_access::{StorePacking}, ContractAddress, }; - use super::{ContractAddress, IStaker}; + use super::{IStaker}; #[derive(Copy, Drop, PartialEq, Debug)] @@ -106,7 +114,7 @@ pub mod Staker { struct StakingLogRecord { timestamp: u64, total_staked: u128, - cumulative_staked_seconds: u128 + cumulative_seconds_per_total_staked: UFixedPoint, } #[storage] @@ -117,7 +125,8 @@ pub mod Staker { amount_delegated: Map, delegated_cumulative_num_snapshots: Map, delegated_cumulative_snapshot: Map>, - staking_log: Map>, + + staking_log: Vec, } #[constructor] @@ -227,8 +236,8 @@ pub mod Staker { } } - fn log_change(ref self: ContractState, staker: ContractAddress, amount: u128, is_add: bool) { - let log = self.staking_log.entry(staker); + fn log_change(ref self: ContractState, amount: u128, is_add: bool) { + let log = self.staking_log.as_path(); if log.len() == 0 { // Add the first record. If withdrawal, then it's underflow. @@ -238,7 +247,7 @@ pub mod Staker { StakingLogRecord { timestamp: get_block_timestamp(), total_staked: amount, - cumulative_staked_seconds: 0, + cumulative_seconds_per_total_staked: 0_u64.into(), } ); @@ -260,7 +269,7 @@ pub mod Staker { // Might be zero let seconds_diff = (get_block_timestamp() - last_record.timestamp) / 1000; - let staked_seconds = last_record.total_staked * seconds_diff.into(); // staked seconds + let staked_seconds: UFixedPoint = seconds_diff.into() / last_record.total_staked.into(); // staked seconds let total_staked = if is_add { // overflow check @@ -277,16 +286,17 @@ pub mod Staker { StakingLogRecord { timestamp: get_block_timestamp(), total_staked: total_staked, - cumulative_staked_seconds: last_record.cumulative_staked_seconds + staked_seconds, + cumulative_seconds_per_total_staked: ( + last_record.cumulative_seconds_per_total_staked + staked_seconds + ), } ); } - fn find_in_change_log(self: @ContractState, staker: ContractAddress, timestamp: u64) -> Option { + fn find_in_change_log(self: @ContractState, timestamp: u64) -> Option { // Find first log record in an array whos timestamp is less or equal to timestamp. // Uses binary search. - - let log = self.staking_log.entry(staker); + let log = self.staking_log.as_path(); if log.len() == 0 { return Option::None; @@ -296,9 +306,9 @@ pub mod Staker { let mut right = log.len() - 1; // To avoid reading from the storage multiple times. - let mut result_ptr = Option::None; + let mut result_ptr: Option> = Option::None; - while left <= right { + while (left <= right) { let center = (right + left) / 2; let record = log.at(center); @@ -307,7 +317,7 @@ pub mod Staker { left = center + 1; } else { right = center - 1; - } + }; }; if let Option::Some(result) = result_ptr { @@ -359,11 +369,33 @@ pub mod Staker { .amount_delegated .write(delegate, self.insert_snapshot(delegate, get_block_timestamp()) + amount); - self.log_change(from, amount, true); + self.log_change(amount, true); self.emit(Staked { from, delegate, amount }); } + fn calculate_staked_seconds_for_amount_between( + self: @ContractState, + token_amount: u128, + start_at: u64, end_at: u64 + ) -> u128 { + let user_cumulative_at_start: UFixedPoint = if let Option::Some(log_record) = self.find_in_change_log(start_at) { + log_record.cumulative_seconds_per_total_staked + } else { + Zero::zero() + }; + + let user_cumulative_at_end: UFixedPoint = if let Option::Some(log_record) = self.find_in_change_log(start_at) { + log_record.cumulative_seconds_per_total_staked + } else { + Zero::zero() + }; + + let res = (user_cumulative_at_end - user_cumulative_at_start) * token_amount.into(); + + return res.get_integer(); + } + fn withdraw( ref self: ContractState, delegate: ContractAddress, recipient: ContractAddress, ) { @@ -389,7 +421,7 @@ pub mod Staker { .write(delegate, self.insert_snapshot(delegate, get_block_timestamp()) - amount); assert(self.token.read().transfer(recipient, amount.into()), 'TRANSFER_FAILED'); - self.log_change(from, amount, false); + self.log_change(amount, false); self.emit(Withdrawn { from, delegate, to: recipient, amount }); } @@ -445,15 +477,13 @@ pub mod Staker { self.get_average_delegated(delegate, now - period, now) } - fn get_staked_seconds_at( - self: @ContractState, staker: ContractAddress, timestamp: u64, - ) -> u128 { - if let Option::Some(log_record) = self.find_in_change_log(staker, timestamp) { + fn get_cumulative_seconds_per_total_staked_at(self: @ContractState, timestamp: u64) -> UFixedPoint { + if let Option::Some(log_record) = self.find_in_change_log(timestamp) { let seconds_diff = (timestamp - log_record.timestamp) / 1000; - let staked_seconds = log_record.total_staked * seconds_diff.into(); // staked seconds - return log_record.cumulative_staked_seconds + staked_seconds; + let staked_seconds: UFixedPoint = seconds_diff.into() / log_record.total_staked.into(); // staked seconds + return log_record.cumulative_seconds_per_total_staked + staked_seconds; } else { - return 0; + return 0_u64.into(); } } } diff --git a/src/utils/fp.cairo b/src/utils/fp.cairo new file mode 100644 index 0000000..da9518a --- /dev/null +++ b/src/utils/fp.cairo @@ -0,0 +1,186 @@ +use starknet::storage_access::{StorePacking}; +use core::num::traits::{WideMul, Zero}; +use core::integer::{u512, u512_safe_div_rem_by_u256}; + +// 128.128 +#[derive(Drop, Copy, PartialEq)] +pub struct UFixedPoint { + pub(crate) value: u512 +} + +pub impl UFixedPointStorePacking of StorePacking { + fn pack(value: UFixedPoint) -> u256 { + value.into() + } + + fn unpack(value: u256) -> UFixedPoint { + value.into() + } +} + +pub impl UFixedPointZero of Zero { + fn zero() -> UFixedPoint { + UFixedPoint { + value: u512 { + limb0: 0, + limb1: 0, + limb2: 0, + limb3: 0, + } + } + } + + fn is_zero(self: @UFixedPoint) -> bool { + self.value.limb0 == @0 && + self.value.limb1 == @0 && + self.value.limb2 == @0 && + self.value.limb3 == @0 + } + + fn is_non_zero(self: @UFixedPoint) -> bool { !self.is_zero() } +} + +impl UFixedPointSerde of core::serde::Serde { + fn serialize(self: @UFixedPoint, ref output: Array) { + let value: u256 = (*self).try_into().unwrap(); + Serde::serialize(@value, ref output) + } + + fn deserialize(ref serialized: Span) -> Option { + let value: u256 = Serde::deserialize(ref serialized)?; + Option::Some(value.into()) + } +} + +pub(crate) impl U64IntoUFixedPoint of Into { + fn into(self: u64) -> UFixedPoint { + UFixedPoint { + value: u512 { + limb0: 0, // fractional + limb1: self.into(), // integer + limb2: 0, + limb3: 0, + } + } + } +} + +pub(crate) impl U128IntoUFixedPoint of Into { + fn into(self: u128) -> UFixedPoint { + UFixedPoint { + value: u512 { + limb0: 0, // fractional + limb1: self.into(), // integer + limb2: 0, + limb3: 0, + } + } + } +} + +pub(crate) impl U256IntoUFixedPoint of Into { + fn into(self: u256) -> UFixedPoint { + UFixedPoint { + value: u512 { + limb0: self.low, // fractional + limb1: self.high, // integer + limb2: 0, + limb3: 0, + } + } + } +} + +#[generate_trait] +pub impl UFixedPointImpl of UFixedPointTrait { + fn get_integer(self: UFixedPoint) -> u128 { + self.value.limb1 + } + + fn get_fractional(self: UFixedPoint) -> u128 { + self.value.limb0 + } +} + +#[generate_trait] +impl UFixedPointShiftImpl of BitShiftImpl { + + fn bitshift_128_up(self: UFixedPoint) -> UFixedPoint { + UFixedPoint { + value: u512 { + limb0: 0, + limb1: self.value.limb0, + limb2: self.value.limb1, + limb3: self.value.limb2, + } + } + } + + fn bitshift_128_down(self: UFixedPoint) -> UFixedPoint { + UFixedPoint { + value: u512 { + limb0: self.value.limb1, + limb1: self.value.limb2, + limb2: self.value.limb3, + limb3: 0, + } + } + } +} + +pub(crate) impl FixedPointIntoU256 of Into { + fn into(self: UFixedPoint) -> u256 { self.value.try_into().unwrap() } +} + +pub impl UFpImplAdd of Add { + fn add(lhs: UFixedPoint, rhs: UFixedPoint) -> UFixedPoint { + let sum: u256 = rhs.into() + lhs.into(); + UFixedPoint { + value: u512 { + limb0: sum.low, + limb1: sum.high, + limb2: 0, + limb3: 0 + } + } + } +} + +pub impl UFpImplSub of Sub { + fn sub(lhs: UFixedPoint, rhs: UFixedPoint) -> UFixedPoint { + let sum: u256 = rhs.into() - lhs.into(); + UFixedPoint { + value: u512 { + limb0: sum.low, + limb1: sum.high, + limb2: 0, + limb3: 0 + } + } + } +} + +// 20100 +pub impl UFpImplMul of Mul { + fn mul(lhs: UFixedPoint, rhs: UFixedPoint) -> UFixedPoint { + let left: u256 = lhs.into(); + let right: u256 = rhs.into(); + + let z = left.wide_mul(right); + + UFixedPoint { value: z }.bitshift_128_down() + } +} + +pub impl UFpImplDiv of Div { + fn div(lhs: UFixedPoint, rhs: UFixedPoint) -> UFixedPoint { + let rhs: u256 = rhs.into(); + + let (result, _) = u512_safe_div_rem_by_u256( + lhs.bitshift_128_up().value, + rhs.try_into().unwrap(), + ); + + UFixedPoint { value: result } + } +} \ No newline at end of file diff --git a/src/utils/fp_test.cairo b/src/utils/fp_test.cairo new file mode 100644 index 0000000..3f296a3 --- /dev/null +++ b/src/utils/fp_test.cairo @@ -0,0 +1,45 @@ +use crate::utils::fp::{UFixedPoint}; + + +#[test] +fn test_add() { + let f1 : UFixedPoint = 0xFFFFFFFFFFFFFFFF_u64.into(); + let f2 : UFixedPoint = 1_u64.into(); + let res = f1 + f2; + let z: u256 = res.into(); + assert(z.low == 0, 'low 0'); + assert(z.high == 18446744073709551616, 'high 18446744073709551616'); +} + +#[test] +fn test_mul() { + let f1 : UFixedPoint = 7_u64.into(); + let f2 : UFixedPoint = 7_u64.into(); + let res = f1 * f2; + let z: u256 = res.into(); + assert(z.low == 0, 'low 0'); + assert(z.high == 49, 'high 49'); +} + +#[test] +fn test_div() { + let f1 : UFixedPoint = 7_u64.into(); + let f2 : UFixedPoint = 56_u64.into(); + let res: u256 = (f2 / f1).into(); + assert(res.high == 8, 'high 8'); + assert(res.low == 0, 'low 0'); +} + +#[test] +fn test_comlex() { + let f2 : UFixedPoint = 2_u64.into(); + let f05: UFixedPoint = 1_u64.into() / f2; + let uf05: u256 = f05.into(); + let f7 : UFixedPoint = 7_u64.into(); + let f175 : UFixedPoint = 17_u64.into() + f05; + let res: u256 = (f175 / f7).into(); + assert(res.high == 2, 'high 2'); + assert(res.low == uf05.low, 'low 0.5'); +} + +// TODO(baitcode): more tests needed \ No newline at end of file From 2f9516aadc86f4d2014f8e9026929bf733aad8a8 Mon Sep 17 00:00:00 2001 From: baitcode Date: Fri, 13 Dec 2024 21:55:46 +0300 Subject: [PATCH 14/40] removed old tests --- src/staker_test.cairo | 238 +++++++++++++++++++++--------------------- 1 file changed, 119 insertions(+), 119 deletions(-) diff --git a/src/staker_test.cairo b/src/staker_test.cairo index 6a61d2a..48d52e4 100644 --- a/src/staker_test.cairo +++ b/src/staker_test.cairo @@ -403,141 +403,141 @@ fn test_delegate_undelegate() { assert(staker.get_delegated_at(delegatee, timestamp: 6) == 0, 'at 6'); } -mod staker_staked_seconds_calculation { - use starknet::{ - get_caller_address - }; - use super::{ - setup, contract_address_const, set_block_timestamp, - IERC20DispatcherTrait, IStakerDispatcherTrait - }; - - #[test] - fn test_should_return_0_if_no_data_found() { - let (staker, _) = setup(10000); - let token_owner = get_caller_address(); - assert(staker.get_staked_seconds_at(token_owner, 0) == 0, 'At 0 should be 0'); - assert(staker.get_staked_seconds_at(token_owner, 1000) == 0, 'At 1000 should be 0'); - } - - #[test] - fn test_should_stake_10000_tokens_for_5_seconds_adding_10000_every_second_to_staked_seconds() { - let (staker, token) = setup(10000); - - // Caller is token owner - let token_owner = get_caller_address(); - - // Allow staker contract to spend 10000 tokens from owner account - token.approve(staker.contract_address, 10000); - - // Adress to delegate tokens to - let delegatee = contract_address_const::<1234567890>(); +// mod staker_staked_seconds_calculation { +// use starknet::{ +// get_caller_address +// }; +// use super::{ +// setup, contract_address_const, set_block_timestamp, +// IERC20DispatcherTrait, IStakerDispatcherTrait +// }; + +// #[test] +// fn test_should_return_0_if_no_data_found() { +// let (staker, _) = setup(10000); +// let token_owner = get_caller_address(); +// assert(staker.get_staked_seconds_at(token_owner, 0) == 0, 'At 0 should be 0'); +// assert(staker.get_staked_seconds_at(token_owner, 1000) == 0, 'At 1000 should be 0'); +// } + +// #[test] +// fn test_should_stake_10000_tokens_for_5_seconds_adding_10000_every_second_to_staked_seconds() { +// let (staker, token) = setup(10000); + +// // Caller is token owner +// let token_owner = get_caller_address(); + +// // Allow staker contract to spend 10000 tokens from owner account +// token.approve(staker.contract_address, 10000); + +// // Adress to delegate tokens to +// let delegatee = contract_address_const::<1234567890>(); - set_block_timestamp(0); - staker.stake(delegatee); // Will transfer 10000 tokens to contract account and setup delegatee +// set_block_timestamp(0); +// staker.stake(delegatee); // Will transfer 10000 tokens to contract account and setup delegatee - set_block_timestamp(5000); // 5 seconds passed +// set_block_timestamp(5000); // 5 seconds passed - assert(staker.get_staked(token_owner, delegatee) == 10000, 'Something went wrong'); +// assert(staker.get_staked(token_owner, delegatee) == 10000, 'Something went wrong'); - staker.withdraw(delegatee, token_owner); // Will withdraw all 10000 tokens back to owner +// staker.withdraw(delegatee, token_owner); // Will withdraw all 10000 tokens back to owner - set_block_timestamp(10000); - assert(staker.get_staked(delegatee, token_owner) == 0, 'Not all tokens were withdrawn'); +// set_block_timestamp(10000); +// assert(staker.get_staked(delegatee, token_owner) == 0, 'Not all tokens were withdrawn'); - assert(staker.get_staked_seconds_at(token_owner, 0) == 0, 'At 0 should be 0'); - assert(staker.get_staked_seconds_at(token_owner, 1000) == 10000, 'At 1s should be 10000 tok*sec'); - assert(staker.get_staked_seconds_at(token_owner, 1100) == 10000, 'At 1.1s should be 10000 tok*sec'); - assert(staker.get_staked_seconds_at(token_owner, 1500) == 10000, 'At 1.5s should be 10000 tok*sec'); - assert(staker.get_staked_seconds_at(token_owner, 1900) == 10000, 'At 1.9s should be 10000 tok*sec'); - assert(staker.get_staked_seconds_at(token_owner, 2000) == 20000, 'At 2s should be 20000 tok*sec'); - assert(staker.get_staked_seconds_at(token_owner, 3000) == 30000, 'At 3s should be 30000 tok*sec'); - assert(staker.get_staked_seconds_at(token_owner, 4000) == 40000, 'At 4s should be 40000 tok*sec'); - assert(staker.get_staked_seconds_at(token_owner, 5000) == 50000, 'At 5s should be 50000 tok*sec'); - assert(staker.get_staked_seconds_at(token_owner, 6000) == 50000, 'At 6s should be 50000 tok*sec'); - assert(staker.get_staked_seconds_at(token_owner, 7000) == 50000, 'At 7s should be 50000 tok*sec'); - } - - #[test] - fn test_should_stake_1_tokens_for_5_seconds_adding_1_every_second_rounding_down() { - let (staker, token) = setup(1); - - // Caller is token owner - let token_owner = get_caller_address(); - - // Allow staker contract to spend 1 tokens from owner account - token.approve(staker.contract_address, 1); - - // Adress to delegate tokens to - let delegatee = contract_address_const::<1234567890>(); +// assert(staker.get_staked_seconds_at(token_owner, 0) == 0, 'At 0 should be 0'); +// assert(staker.get_staked_seconds_at(token_owner, 1000) == 10000, 'At 1s should be 10000 tok*sec'); +// assert(staker.get_staked_seconds_at(token_owner, 1100) == 10000, 'At 1.1s should be 10000 tok*sec'); +// assert(staker.get_staked_seconds_at(token_owner, 1500) == 10000, 'At 1.5s should be 10000 tok*sec'); +// assert(staker.get_staked_seconds_at(token_owner, 1900) == 10000, 'At 1.9s should be 10000 tok*sec'); +// assert(staker.get_staked_seconds_at(token_owner, 2000) == 20000, 'At 2s should be 20000 tok*sec'); +// assert(staker.get_staked_seconds_at(token_owner, 3000) == 30000, 'At 3s should be 30000 tok*sec'); +// assert(staker.get_staked_seconds_at(token_owner, 4000) == 40000, 'At 4s should be 40000 tok*sec'); +// assert(staker.get_staked_seconds_at(token_owner, 5000) == 50000, 'At 5s should be 50000 tok*sec'); +// assert(staker.get_staked_seconds_at(token_owner, 6000) == 50000, 'At 6s should be 50000 tok*sec'); +// assert(staker.get_staked_seconds_at(token_owner, 7000) == 50000, 'At 7s should be 50000 tok*sec'); +// } + +// #[test] +// fn test_should_stake_1_tokens_for_5_seconds_adding_1_every_second_rounding_down() { +// let (staker, token) = setup(1); + +// // Caller is token owner +// let token_owner = get_caller_address(); + +// // Allow staker contract to spend 1 tokens from owner account +// token.approve(staker.contract_address, 1); + +// // Adress to delegate tokens to +// let delegatee = contract_address_const::<1234567890>(); - set_block_timestamp(0); - staker.stake(delegatee); // Will transfer 10000 tokens to contract account and setup delegatee +// set_block_timestamp(0); +// staker.stake(delegatee); // Will transfer 10000 tokens to contract account and setup delegatee - set_block_timestamp(5000); // 5 seconds passed +// set_block_timestamp(5000); // 5 seconds passed - assert(staker.get_staked(token_owner, delegatee) == 1, 'Something went wrong'); +// assert(staker.get_staked(token_owner, delegatee) == 1, 'Something went wrong'); - staker.withdraw(delegatee, token_owner); // Will withdraw all 10000 tokens back to owner +// staker.withdraw(delegatee, token_owner); // Will withdraw all 10000 tokens back to owner - set_block_timestamp(10000); - assert(staker.get_staked(delegatee, token_owner) == 0, 'Not all tokens were withdrawn'); +// set_block_timestamp(10000); +// assert(staker.get_staked(delegatee, token_owner) == 0, 'Not all tokens were withdrawn'); - assert(staker.get_staked_seconds_at(token_owner, 0) == 0, 'At 0 should be 0'); - assert(staker.get_staked_seconds_at(token_owner, 1000) == 1, 'At 1s should be 1 tok*sec'); - assert(staker.get_staked_seconds_at(token_owner, 1100) == 1, 'At 1.1s should be 1 tok*sec'); - assert(staker.get_staked_seconds_at(token_owner, 1500) == 1, 'At 1.5s should be 1 tok*sec'); - assert(staker.get_staked_seconds_at(token_owner, 1900) == 1, 'At 1.9s should be 1 tok*sec'); - assert(staker.get_staked_seconds_at(token_owner, 2000) == 2, 'At 2s should be 2 tok*sec'); - assert(staker.get_staked_seconds_at(token_owner, 3000) == 3, 'At 3s should be 3 tok*sec'); - assert(staker.get_staked_seconds_at(token_owner, 4000) == 4, 'At 4s should be 4 tok*sec'); - assert(staker.get_staked_seconds_at(token_owner, 5000) == 5, 'At 5s should be 5 tok*sec'); - assert(staker.get_staked_seconds_at(token_owner, 6000) == 5, 'At 6s should be 5 tok*sec'); - assert(staker.get_staked_seconds_at(token_owner, 7000) == 5, 'At 7s should be 5 tok*sec'); - } - - #[test] - #[should_panic(expected: ('INSUFFICIENT_AMOUNT_STAKED', 'ENTRYPOINT_FAILED'))] - fn test_raises_error_if_no_history_exists_and_withdrawal_happens() { - // TODO(biatcode): This test accidentally tests other - // functionality and should be refactored +// assert(staker.get_staked_seconds_at(token_owner, 0) == 0, 'At 0 should be 0'); +// assert(staker.get_staked_seconds_at(token_owner, 1000) == 1, 'At 1s should be 1 tok*sec'); +// assert(staker.get_staked_seconds_at(token_owner, 1100) == 1, 'At 1.1s should be 1 tok*sec'); +// assert(staker.get_staked_seconds_at(token_owner, 1500) == 1, 'At 1.5s should be 1 tok*sec'); +// assert(staker.get_staked_seconds_at(token_owner, 1900) == 1, 'At 1.9s should be 1 tok*sec'); +// assert(staker.get_staked_seconds_at(token_owner, 2000) == 2, 'At 2s should be 2 tok*sec'); +// assert(staker.get_staked_seconds_at(token_owner, 3000) == 3, 'At 3s should be 3 tok*sec'); +// assert(staker.get_staked_seconds_at(token_owner, 4000) == 4, 'At 4s should be 4 tok*sec'); +// assert(staker.get_staked_seconds_at(token_owner, 5000) == 5, 'At 5s should be 5 tok*sec'); +// assert(staker.get_staked_seconds_at(token_owner, 6000) == 5, 'At 6s should be 5 tok*sec'); +// assert(staker.get_staked_seconds_at(token_owner, 7000) == 5, 'At 7s should be 5 tok*sec'); +// } + +// #[test] +// #[should_panic(expected: ('INSUFFICIENT_AMOUNT_STAKED', 'ENTRYPOINT_FAILED'))] +// fn test_raises_error_if_no_history_exists_and_withdrawal_happens() { +// // TODO(biatcode): This test accidentally tests other +// // functionality and should be refactored - let (staker, token) = setup(10000); +// let (staker, token) = setup(10000); - // Caller is token owner - let token_owner = get_caller_address(); +// // Caller is token owner +// let token_owner = get_caller_address(); - // Adress to delegate tokens to - let delegatee = contract_address_const::<1234567890>(); +// // Adress to delegate tokens to +// let delegatee = contract_address_const::<1234567890>(); - token.approve(staker.contract_address, 10000); +// token.approve(staker.contract_address, 10000); - set_block_timestamp(1000); - staker.stake_amount(delegatee, 1000); - set_block_timestamp(2000); - staker.withdraw_amount(delegatee, token_owner, 500); - set_block_timestamp(3000); - staker.stake_amount(delegatee, 1000); - set_block_timestamp(4000); - staker.withdraw_amount(delegatee, token_owner, 2000); - } - - #[test] - #[should_panic(expected: ('BAD AMOUNT', 'ENTRYPOINT_FAILED'))] - fn test_raises_error_if_withdrawn_more_than_staked() { - let (staker, token) = setup(10000); - - // Caller is token owner - let token_owner = get_caller_address(); - - // Allow staker to spend 10000 tokens from owner account - token.approve(staker.contract_address, 10000); - - // Adress to delegate tokens to - let delegatee = contract_address_const::<1234567890>(); +// set_block_timestamp(1000); +// staker.stake_amount(delegatee, 1000); +// set_block_timestamp(2000); +// staker.withdraw_amount(delegatee, token_owner, 500); +// set_block_timestamp(3000); +// staker.stake_amount(delegatee, 1000); +// set_block_timestamp(4000); +// staker.withdraw_amount(delegatee, token_owner, 2000); +// } + +// #[test] +// #[should_panic(expected: ('BAD AMOUNT', 'ENTRYPOINT_FAILED'))] +// fn test_raises_error_if_withdrawn_more_than_staked() { +// let (staker, token) = setup(10000); + +// // Caller is token owner +// let token_owner = get_caller_address(); + +// // Allow staker to spend 10000 tokens from owner account +// token.approve(staker.contract_address, 10000); + +// // Adress to delegate tokens to +// let delegatee = contract_address_const::<1234567890>(); - set_block_timestamp(5000); // 5 seconds passed - staker.withdraw(delegatee, token_owner); // Will withdraw all 10000 tokens back to owner - } +// set_block_timestamp(5000); // 5 seconds passed +// staker.withdraw(delegatee, token_owner); // Will withdraw all 10000 tokens back to owner +// } -} \ No newline at end of file +// } \ No newline at end of file From bb4f99a34fcdb551371920b57415836318b4519e Mon Sep 17 00:00:00 2001 From: baitcode Date: Sun, 15 Dec 2024 01:52:52 +0300 Subject: [PATCH 15/40] * debugged fixed point math. added tests * removed staked seconds calculation * added seconds per total staked test and added logic for calculation of that value when nothing is staked --- src/staker.cairo | 40 +++---- src/staker_test.cairo | 224 ++++++++++++++++++---------------------- src/utils/fp.cairo | 37 ++++++- src/utils/fp_test.cairo | 216 ++++++++++++++++++++++++++++++++++---- 4 files changed, 342 insertions(+), 175 deletions(-) diff --git a/src/staker.cairo b/src/staker.cairo index d9739af..76dc396 100644 --- a/src/staker.cairo +++ b/src/staker.cairo @@ -58,15 +58,12 @@ pub trait IStaker { // Gets the cumulative staked amount * per second staked for the given timestamp and account. fn get_cumulative_seconds_per_total_staked_at(self: @TContractState, timestamp: u64) -> UFixedPoint; - // Gets the cumulative staked amount * per second staked for the given timestamp and account. - fn calculate_staked_seconds_for_amount_between(self: @TContractState, token_amount: u128, start_at: u64, end_at: u64) -> u128; } #[starknet::contract] pub mod Staker { use starknet::storage::StorageAsPath; -use super::super::utils::fp::UFixedPointTrait; use core::num::traits::zero::{Zero}; use governance::interfaces::erc20::{IERC20Dispatcher, IERC20DispatcherTrait}; use starknet::storage::{ @@ -269,7 +266,11 @@ use super::super::utils::fp::UFixedPointTrait; // Might be zero let seconds_diff = (get_block_timestamp() - last_record.timestamp) / 1000; - let staked_seconds: UFixedPoint = seconds_diff.into() / last_record.total_staked.into(); // staked seconds + let staked_seconds: UFixedPoint = if last_record.total_staked == 0 { + 0_u64.into() + } else { + seconds_diff.into() / last_record.total_staked.into() + }; let total_staked = if is_add { // overflow check @@ -355,6 +356,7 @@ use super::super::utils::fp::UFixedPointTrait; } fn stake_amount(ref self: ContractState, delegate: ContractAddress, amount: u128) { + assert(amount != 0, 'PFFFFF'); let from = get_caller_address(); let token = self.token.read(); @@ -374,28 +376,6 @@ use super::super::utils::fp::UFixedPointTrait; self.emit(Staked { from, delegate, amount }); } - fn calculate_staked_seconds_for_amount_between( - self: @ContractState, - token_amount: u128, - start_at: u64, end_at: u64 - ) -> u128 { - let user_cumulative_at_start: UFixedPoint = if let Option::Some(log_record) = self.find_in_change_log(start_at) { - log_record.cumulative_seconds_per_total_staked - } else { - Zero::zero() - }; - - let user_cumulative_at_end: UFixedPoint = if let Option::Some(log_record) = self.find_in_change_log(start_at) { - log_record.cumulative_seconds_per_total_staked - } else { - Zero::zero() - }; - - let res = (user_cumulative_at_end - user_cumulative_at_start) * token_amount.into(); - - return res.get_integer(); - } - fn withdraw( ref self: ContractState, delegate: ContractAddress, recipient: ContractAddress, ) { @@ -480,7 +460,13 @@ use super::super::utils::fp::UFixedPointTrait; fn get_cumulative_seconds_per_total_staked_at(self: @ContractState, timestamp: u64) -> UFixedPoint { if let Option::Some(log_record) = self.find_in_change_log(timestamp) { let seconds_diff = (timestamp - log_record.timestamp) / 1000; - let staked_seconds: UFixedPoint = seconds_diff.into() / log_record.total_staked.into(); // staked seconds + + let staked_seconds: UFixedPoint = if log_record.total_staked == 0 { + 0_u64.into() + } else { + seconds_diff.into() / log_record.total_staked.into() + }; + return log_record.cumulative_seconds_per_total_staked + staked_seconds; } else { return 0_u64.into(); diff --git a/src/staker_test.cairo b/src/staker_test.cairo index 48d52e4..b53ed7e 100644 --- a/src/staker_test.cairo +++ b/src/staker_test.cairo @@ -403,141 +403,121 @@ fn test_delegate_undelegate() { assert(staker.get_delegated_at(delegatee, timestamp: 6) == 0, 'at 6'); } -// mod staker_staked_seconds_calculation { -// use starknet::{ -// get_caller_address -// }; -// use super::{ -// setup, contract_address_const, set_block_timestamp, -// IERC20DispatcherTrait, IStakerDispatcherTrait -// }; - -// #[test] -// fn test_should_return_0_if_no_data_found() { -// let (staker, _) = setup(10000); -// let token_owner = get_caller_address(); -// assert(staker.get_staked_seconds_at(token_owner, 0) == 0, 'At 0 should be 0'); -// assert(staker.get_staked_seconds_at(token_owner, 1000) == 0, 'At 1000 should be 0'); -// } - -// #[test] -// fn test_should_stake_10000_tokens_for_5_seconds_adding_10000_every_second_to_staked_seconds() { -// let (staker, token) = setup(10000); - -// // Caller is token owner -// let token_owner = get_caller_address(); - -// // Allow staker contract to spend 10000 tokens from owner account -// token.approve(staker.contract_address, 10000); - -// // Adress to delegate tokens to -// let delegatee = contract_address_const::<1234567890>(); - -// set_block_timestamp(0); -// staker.stake(delegatee); // Will transfer 10000 tokens to contract account and setup delegatee +mod staker_staked_seconds_per_total_staked_calculation { + use starknet::{get_caller_address}; + use crate::utils::fp::{UFixedPoint, UFixedPointImpl}; + use super::{ + setup, contract_address_const, set_block_timestamp, + IERC20DispatcherTrait, IStakerDispatcherTrait, + }; -// set_block_timestamp(5000); // 5 seconds passed -// assert(staker.get_staked(token_owner, delegatee) == 10000, 'Something went wrong'); + #[test] + fn test_should_return_0_if_no_data_found() { + let (staker, _) = setup(10000); + assert(staker.get_cumulative_seconds_per_total_staked_at(0) == 0_u64.into(), 'At 0 should be 0'); + assert(staker.get_cumulative_seconds_per_total_staked_at(1000) == 0_u64.into(), 'At 1000 should be 0'); + } -// staker.withdraw(delegatee, token_owner); // Will withdraw all 10000 tokens back to owner -// set_block_timestamp(10000); -// assert(staker.get_staked(delegatee, token_owner) == 0, 'Not all tokens were withdrawn'); - -// assert(staker.get_staked_seconds_at(token_owner, 0) == 0, 'At 0 should be 0'); -// assert(staker.get_staked_seconds_at(token_owner, 1000) == 10000, 'At 1s should be 10000 tok*sec'); -// assert(staker.get_staked_seconds_at(token_owner, 1100) == 10000, 'At 1.1s should be 10000 tok*sec'); -// assert(staker.get_staked_seconds_at(token_owner, 1500) == 10000, 'At 1.5s should be 10000 tok*sec'); -// assert(staker.get_staked_seconds_at(token_owner, 1900) == 10000, 'At 1.9s should be 10000 tok*sec'); -// assert(staker.get_staked_seconds_at(token_owner, 2000) == 20000, 'At 2s should be 20000 tok*sec'); -// assert(staker.get_staked_seconds_at(token_owner, 3000) == 30000, 'At 3s should be 30000 tok*sec'); -// assert(staker.get_staked_seconds_at(token_owner, 4000) == 40000, 'At 4s should be 40000 tok*sec'); -// assert(staker.get_staked_seconds_at(token_owner, 5000) == 50000, 'At 5s should be 50000 tok*sec'); -// assert(staker.get_staked_seconds_at(token_owner, 6000) == 50000, 'At 6s should be 50000 tok*sec'); -// assert(staker.get_staked_seconds_at(token_owner, 7000) == 50000, 'At 7s should be 50000 tok*sec'); -// } - -// #[test] -// fn test_should_stake_1_tokens_for_5_seconds_adding_1_every_second_rounding_down() { -// let (staker, token) = setup(1); - -// // Caller is token owner -// let token_owner = get_caller_address(); - -// // Allow staker contract to spend 1 tokens from owner account -// token.approve(staker.contract_address, 1); - -// // Adress to delegate tokens to -// let delegatee = contract_address_const::<1234567890>(); - -// set_block_timestamp(0); -// staker.stake(delegatee); // Will transfer 10000 tokens to contract account and setup delegatee + #[test] + #[should_panic(expected: ('BAD AMOUNT', 'ENTRYPOINT_FAILED'))] + fn test_raises_error_if_withdrawn_more_than_staked() { + let (staker, token) = setup(10000); -// set_block_timestamp(5000); // 5 seconds passed + // Caller is token owner + let token_owner = get_caller_address(); -// assert(staker.get_staked(token_owner, delegatee) == 1, 'Something went wrong'); + // Allow staker to spend 10000 tokens from owner account + token.approve(staker.contract_address, 10000); + + // Adress to delegate tokens to + let delegatee = contract_address_const::<1234567890>(); + + set_block_timestamp(5000); // 5 seconds passed + staker.withdraw(delegatee, token_owner); // Will withdraw all 10000 tokens back to owner + } -// staker.withdraw(delegatee, token_owner); // Will withdraw all 10000 tokens back to owner -// set_block_timestamp(10000); -// assert(staker.get_staked(delegatee, token_owner) == 0, 'Not all tokens were withdrawn'); + #[test] + #[should_panic(expected: ('INSUFFICIENT_AMOUNT_STAKED', 'ENTRYPOINT_FAILED'))] + fn test_raises_error_if_no_history_exists_and_withdrawal_happens() { + // TODO(biatcode): This test accidentally tests other + // functionality and should be refactored -// assert(staker.get_staked_seconds_at(token_owner, 0) == 0, 'At 0 should be 0'); -// assert(staker.get_staked_seconds_at(token_owner, 1000) == 1, 'At 1s should be 1 tok*sec'); -// assert(staker.get_staked_seconds_at(token_owner, 1100) == 1, 'At 1.1s should be 1 tok*sec'); -// assert(staker.get_staked_seconds_at(token_owner, 1500) == 1, 'At 1.5s should be 1 tok*sec'); -// assert(staker.get_staked_seconds_at(token_owner, 1900) == 1, 'At 1.9s should be 1 tok*sec'); -// assert(staker.get_staked_seconds_at(token_owner, 2000) == 2, 'At 2s should be 2 tok*sec'); -// assert(staker.get_staked_seconds_at(token_owner, 3000) == 3, 'At 3s should be 3 tok*sec'); -// assert(staker.get_staked_seconds_at(token_owner, 4000) == 4, 'At 4s should be 4 tok*sec'); -// assert(staker.get_staked_seconds_at(token_owner, 5000) == 5, 'At 5s should be 5 tok*sec'); -// assert(staker.get_staked_seconds_at(token_owner, 6000) == 5, 'At 6s should be 5 tok*sec'); -// assert(staker.get_staked_seconds_at(token_owner, 7000) == 5, 'At 7s should be 5 tok*sec'); -// } - -// #[test] -// #[should_panic(expected: ('INSUFFICIENT_AMOUNT_STAKED', 'ENTRYPOINT_FAILED'))] -// fn test_raises_error_if_no_history_exists_and_withdrawal_happens() { -// // TODO(biatcode): This test accidentally tests other -// // functionality and should be refactored + let (staker, token) = setup(10000); + + // Caller is token owner + let token_owner = get_caller_address(); -// let (staker, token) = setup(10000); + // Adress to delegate tokens to + let delegatee = contract_address_const::<1234567890>(); -// // Caller is token owner -// let token_owner = get_caller_address(); + token.approve(staker.contract_address, 10000); -// // Adress to delegate tokens to -// let delegatee = contract_address_const::<1234567890>(); + set_block_timestamp(1000); + staker.stake_amount(delegatee, 1000); + set_block_timestamp(2000); + staker.withdraw_amount(delegatee, token_owner, 500); + set_block_timestamp(3000); + staker.stake_amount(delegatee, 1000); + set_block_timestamp(4000); + staker.withdraw_amount(delegatee, token_owner, 2000); + } + -// token.approve(staker.contract_address, 10000); + fn assert_fp(value: UFixedPoint, integer: u128, fractional: u128) { + assert(value.get_integer() == integer, 'Integer part is not correct'); + assert(value.get_fractional() == fractional, 'Fractional part is not correct'); + } + + + #[test] + fn test_should_stake_10000_tokens_for_5_seconds_adding_10000_every_second_to_staked_seconds() { + let (staker, token) = setup(1000); + + // Caller is token owner + let token_owner = get_caller_address(); + + // Allow staker contract to spend 10000 tokens from owner account + token.approve(staker.contract_address, 2); + + // Adress to delegate tokens to + let delegatee = contract_address_const::<1234567890>(); -// set_block_timestamp(1000); -// staker.stake_amount(delegatee, 1000); -// set_block_timestamp(2000); -// staker.withdraw_amount(delegatee, token_owner, 500); -// set_block_timestamp(3000); -// staker.stake_amount(delegatee, 1000); -// set_block_timestamp(4000); -// staker.withdraw_amount(delegatee, token_owner, 2000); -// } - -// #[test] -// #[should_panic(expected: ('BAD AMOUNT', 'ENTRYPOINT_FAILED'))] -// fn test_raises_error_if_withdrawn_more_than_staked() { -// let (staker, token) = setup(10000); - -// // Caller is token owner -// let token_owner = get_caller_address(); - -// // Allow staker to spend 10000 tokens from owner account -// token.approve(staker.contract_address, 10000); - -// // Adress to delegate tokens to -// let delegatee = contract_address_const::<1234567890>(); + set_block_timestamp(0); + staker.stake(delegatee); // Will transfer 10 token to contract account and setup delegatee + + set_block_timestamp(5000); // 5 seconds passed + + assert(staker.get_staked(token_owner, delegatee) == 2, 'Something went wrong'); + + staker.withdraw(delegatee, token_owner); // Will withdraw all 10 tokens back to owner + assert(staker.get_staked(delegatee, token_owner) == 0, 'Not all tokens were withdrawn'); + + set_block_timestamp(10000); + token.approve(staker.contract_address, 7); + staker.stake(delegatee); // Will transfer 7 token to contract account and setup delegatee -// set_block_timestamp(5000); // 5 seconds passed -// staker.withdraw(delegatee, token_owner); // Will withdraw all 10000 tokens back to owner -// } + assert(staker.get_cumulative_seconds_per_total_staked_at(0) == 0_u64.into(), 'At 0 should be 0'); + assert(staker.get_cumulative_seconds_per_total_staked_at(500) == 0_u64.into(), 'At 500 should be 0'); + assert(staker.get_cumulative_seconds_per_total_staked_at(999) == 0_u64.into(), 'At 999 should be 0'); + + assert_fp(staker.get_cumulative_seconds_per_total_staked_at(1000), 0, 0x80000000000000000000000000000000_u128); + assert_fp(staker.get_cumulative_seconds_per_total_staked_at(2000), 1, 0_u128); + assert_fp(staker.get_cumulative_seconds_per_total_staked_at(3000), 1, 0x80000000000000000000000000000000_u128); + assert_fp(staker.get_cumulative_seconds_per_total_staked_at(4000), 2, 0_u128); + assert_fp(staker.get_cumulative_seconds_per_total_staked_at(5000), 2, 0x80000000000000000000000000000000_u128); + + // NOTE: After 5s value stops changing as nothing is staked. @Moody is that a desired behaviour? + assert_fp(staker.get_cumulative_seconds_per_total_staked_at(6000), 2, 0x80000000000000000000000000000000_u128); + assert_fp(staker.get_cumulative_seconds_per_total_staked_at(7000), 2, 0x80000000000000000000000000000000_u128); + assert_fp(staker.get_cumulative_seconds_per_total_staked_at(8000), 2, 0x80000000000000000000000000000000_u128); + assert_fp(staker.get_cumulative_seconds_per_total_staked_at(9000), 2, 0x80000000000000000000000000000000_u128); + assert_fp(staker.get_cumulative_seconds_per_total_staked_at(10000), 2, 0x80000000000000000000000000000000_u128); + // 7 were staked here + assert_fp(staker.get_cumulative_seconds_per_total_staked_at(17000), 3, 0x80000000000000000000000000000000_u128); + assert_fp(staker.get_cumulative_seconds_per_total_staked_at(24000), 4, 0x80000000000000000000000000000000_u128); + } -// } \ No newline at end of file +} \ No newline at end of file diff --git a/src/utils/fp.cairo b/src/utils/fp.cairo index da9518a..628f5c8 100644 --- a/src/utils/fp.cairo +++ b/src/utils/fp.cairo @@ -2,8 +2,10 @@ use starknet::storage_access::{StorePacking}; use core::num::traits::{WideMul, Zero}; use core::integer::{u512, u512_safe_div_rem_by_u256}; +pub const EPSILON: u256 = 0x10_u256; + // 128.128 -#[derive(Drop, Copy, PartialEq)] +#[derive(Drop, Copy)] pub struct UFixedPoint { pub(crate) value: u512 } @@ -18,6 +20,19 @@ pub impl UFixedPointStorePacking of StorePacking { } } +pub impl UFixedPointPartialEq of PartialEq { + fn eq(lhs: @UFixedPoint, rhs: @UFixedPoint) -> bool { + let left: u256 = (*lhs).into(); + let right: u256 = (*rhs).into(); + let diff = if left > right { + left - right + } else { + right - left + }; + diff < EPSILON + } +} + pub impl UFixedPointZero of Zero { fn zero() -> UFixedPoint { UFixedPoint { @@ -70,7 +85,7 @@ pub(crate) impl U128IntoUFixedPoint of Into { UFixedPoint { value: u512 { limb0: 0, // fractional - limb1: self.into(), // integer + limb1: self, // integer limb2: 0, limb3: 0, } @@ -103,7 +118,7 @@ pub impl UFixedPointImpl of UFixedPointTrait { } #[generate_trait] -impl UFixedPointShiftImpl of BitShiftImpl { +pub impl UFixedPointShiftImpl of BitShiftImpl { fn bitshift_128_up(self: UFixedPoint) -> UFixedPoint { UFixedPoint { @@ -126,6 +141,17 @@ impl UFixedPointShiftImpl of BitShiftImpl { } } } + + fn bitshift_256_down(self: UFixedPoint) -> UFixedPoint { + UFixedPoint { + value: u512 { + limb0: self.value.limb2, + limb1: self.value.limb3, + limb2: 0, + limb3: 0, + } + } + } } pub(crate) impl FixedPointIntoU256 of Into { @@ -166,15 +192,16 @@ pub impl UFpImplMul of Mul { let left: u256 = lhs.into(); let right: u256 = rhs.into(); - let z = left.wide_mul(right); + let res = left.wide_mul(right); - UFixedPoint { value: z }.bitshift_128_down() + UFixedPoint { value: res }.bitshift_128_down() } } pub impl UFpImplDiv of Div { fn div(lhs: UFixedPoint, rhs: UFixedPoint) -> UFixedPoint { let rhs: u256 = rhs.into(); + assert(rhs != 0, 'DIVISION_BY_ZERO'); let (result, _) = u512_safe_div_rem_by_u256( lhs.bitshift_128_up().value, diff --git a/src/utils/fp_test.cairo b/src/utils/fp_test.cairo index 3f296a3..d9a1cae 100644 --- a/src/utils/fp_test.cairo +++ b/src/utils/fp_test.cairo @@ -1,5 +1,10 @@ -use crate::utils::fp::{UFixedPoint}; +use core::num::traits::WideMul; +use super::fp::BitShiftImpl; +use super::fp::UFixedPointTrait; +use crate::utils::fp::{UFixedPoint, UFixedPointShiftImpl}; + +const SCALE_FACTOR: u256 = 0x100000000000000000000000000000000; #[test] fn test_add() { @@ -12,34 +17,203 @@ fn test_add() { } #[test] -fn test_mul() { +fn test_fp_value_mapping() { let f1 : UFixedPoint = 7_u64.into(); - let f2 : UFixedPoint = 7_u64.into(); - let res = f1 * f2; - let z: u256 = res.into(); - assert(z.low == 0, 'low 0'); - assert(z.high == 49, 'high 49'); + assert(f1.value.limb0 == 0x0, 'limb0 == 0'); + assert(f1.value.limb1 == 0x7, 'limb1 == 7'); + + let val: u256 = f1.into(); + assert(val == 7_u256*0x100000000000000000000000000000000, 'val has to be 128 bit shifted'); } + #[test] -fn test_div() { +fn test_mul() { let f1 : UFixedPoint = 7_u64.into(); - let f2 : UFixedPoint = 56_u64.into(); - let res: u256 = (f2 / f1).into(); - assert(res.high == 8, 'high 8'); + let f2 : UFixedPoint = 7_u64.into(); + + let expected = (7_u256*SCALE_FACTOR).wide_mul(7_u256*SCALE_FACTOR); + + assert(expected.limb0 == 0, 'limb0==0'); + assert(expected.limb1 == 0, 'limb1==0'); + assert(expected.limb2 == 49, 'limb2==0'); + assert(expected.limb3 == 0, 'limb3==0'); + + let res: u256 = (f1 * f2).into(); + assert(res.high == 49, 'high 49'); assert(res.low == 0, 'low 0'); } #[test] -fn test_comlex() { - let f2 : UFixedPoint = 2_u64.into(); - let f05: UFixedPoint = 1_u64.into() / f2; - let uf05: u256 = f05.into(); - let f7 : UFixedPoint = 7_u64.into(); - let f175 : UFixedPoint = 17_u64.into() + f05; - let res: u256 = (f175 / f7).into(); - assert(res.high == 2, 'high 2'); - assert(res.low == uf05.low, 'low 0.5'); +fn test_multiplication() { + let f1 : UFixedPoint = 9223372036854775808_u128.into(); + assert(f1.value.limb0 == 0, 'f1.limb0 0= 0'); + assert(f1.value.limb1 == 9223372036854775808_u128, 'f1.limb1 != 0'); + assert(f1.value.limb2 == 0, 'f1.limb2 == 0'); + assert(f1.value.limb3 == 0, 'f1.limb3 == 0'); + + let res = f1 * f1; + + assert(res.value.limb0 == 0, 'res.limb0 != 0'); + assert(res.value.limb1 == 0x40000000000000000000000000000000, 'res.limb1 != 0'); + assert(res.value.limb2 == 0, 'res.limb2 == 0'); + assert(res.value.limb3 == 0, 'res.limb3 == 0'); + + let expected = 9223372036854775808_u128.wide_mul(9223372036854775808_u128) * SCALE_FACTOR; + + assert(expected.low == 0, 'low == 0'); + assert(expected.high == 0x40000000000000000000000000000000, 'high != 0'); + + let result: u256 = res.into(); + assert(result == expected, 'unexpected mult result'); +} + +#[test] +fn test_u256_conversion() { + let f: u256 = 0x0123456789ABCDEFFEDCBA987654321000112233445566778899AABBCCDDEEFF_u256; + + assert(f.low == 0x00112233445566778899AABBCCDDEEFF, 'low'); + assert(f.high == 0x0123456789ABCDEFFEDCBA9876543210, 'high'); + + // BITSHIFT DOWN + let fp: UFixedPoint = f.into(); + assert(fp.get_integer() == f.high, 'integer == f.high'); + assert(fp.get_fractional() == f.low, 'fractional == f.low'); + + let fp = fp.bitshift_128_down(); + assert(fp.get_integer() == 0, 'integer==0 bs_down'); + assert(fp.get_fractional() == f.high, 'fractional == f.low bs_down'); + + let fp = fp.bitshift_128_down(); + assert(fp.get_integer() == 0, 'integer==0 bs_down 2'); + assert(fp.get_fractional() == 0, 'fractional == 0 bs_down 2'); + + // BITSHIFT UP + let fp: UFixedPoint = f.into(); + assert(fp.get_integer() == f.high, 'integer == f.high'); + assert(fp.get_fractional() == f.low, 'fractional == f.low'); + + let fp = fp.bitshift_128_up(); + assert(fp.get_integer() == f.low, 'integer == f.high bs_up'); + assert(fp.get_fractional() == 0, 'fractional == f.low bs_up'); + + let fp = fp.bitshift_128_up(); + assert(fp.get_integer() == 0, 'integer == f.high bs_up'); + assert(fp.get_fractional() == 0, 'fractional == f.low bs_up'); +} + +fn run_division_test(left: u128, right: u128, expected_int: u128, expected_frac: u128) { + let f1 : UFixedPoint = left.into(); + let f2 : UFixedPoint = right.into(); + let res = f1 / f2; + assert(res.get_integer() == expected_int, 'integer'); + assert(res.get_fractional() == expected_frac, 'fractional'); +} + +fn run_division_and_multiplication_test(numenator: u128, divisor: u128, mult: u128, expected_int: u128, expected_frac: u128) { + let f1 : UFixedPoint = numenator.into(); + let f2 : UFixedPoint = divisor.into(); + let f3 : UFixedPoint = mult.into(); + let res = f1 / f2 * f3; + assert(res.get_integer() == expected_int, 'integer'); + assert(res.get_fractional() == expected_frac, 'fractional'); +} + + +#[test] +fn test_division() { + run_division_test(1, 1000, 0, 0x4189374bc6a7ef9db22d0e56041893); + run_division_test(56, 7, 8, 0); + run_division_test(0, 7, 0, 0); + + run_division_test(0x373a1db0ac54ce4e580815828152f, 0x68ee6e5a163cd022760d1bb6eb4a0f7d, 0x0, 0x86bc9e2a429fb89528cb71f24afbb); + run_division_test(0x6420b39c9a627ed6b97f50bed3887, 0xdfc9859d615e2a1423bb70f42426ceeb, 0x0, 0x728a5f7ea534a5858bd21791663d2); + run_division_test(0x54948d04aab7daa0ac42f255df182, 0x5d97ed36740f5e528f154b565d039df6, 0x0, 0xe758c9b672eb559b2a09feb6e5082); + run_division_test(0x3603c70c39769c861d6a9e1c4ab93, 0x1cd625b5488f3b7443f7baf785db9d23, 0x0, 0x1df85f1cabacb5b7af7dbfb4f3eb29); + run_division_test(0x3454ad56a8a03af36c0c682f80ff4, 0x221ff40a3c0917b64cdc72b7c2201553, 0x0, 0x1889426dabe2220f0bdf5e6d91aa22); + run_division_test(0xc5151b7adb4f20fec8affdacdbd0, 0x9b9f9c2893ba96b28460bd6651ef35b4, 0x0, 0x144332932b6b6b845eb0619b6460d); + run_division_test(0x30d4f65bb98392fac119cb0d1a001, 0xe816835b58266378ef4ff98551febaa6, 0x0, 0x35dcf041fc7c566cee3c08524ac3c); + run_division_test(0x5fdcece9a04e6e2ec3b9b97daae29, 0xafc02e6f0de1d5f1a03f856e3ff0ac50, 0x0, 0x8ba28622026c1e149c76ce780d48c); + run_division_test(0x2dbd5a118251261474e78c00d6218, 0xd648b9b997123ec4ad9c28efe41429c0, 0x0, 0x36a4e0f5ccd2c7245ccc92eaa3032); + run_division_test(0x5dd565d8cff2c3910670e7281437, 0xc98fe84476bcde26354201c914b67bc2, 0x0, 0x772d1799f8bce31f7938b243b1a3); + run_division_test(0x395e07b9b8b0101ace9e5d8e81b3, 0x32ab696d44691512dcb0f703652cbc50, 0x0, 0x121d6d65094a60a4e0cacde47959a); + run_division_test(0x23d7fae99ca75b5b3895532d3af7e, 0xfdd9cc91f526a4604e5155f44cca3932, 0x0, 0x2425ab1a41dec9f5cec9309c9f215); + run_division_test(0x17a95c6d826ae28da48bf9c7e1d37, 0xdf1d2b4ffad0429069d1149849420ca, 0x0, 0x1b2630cc7021c0eed8b50b883f27c1); + run_division_test(0x2f4eccfae439a7537c80d4ea48abb, 0x7c783f2d20675729ffa2370389b6041a, 0x0, 0x614c96d92643f1bba9d426a45ca79); + run_division_test(0x50ef73f24f0359bf40f9af1c54503, 0x469d30f299b08c1fc606db348bec82e6, 0x0, 0x1256b1a2feed6ed8a72d08d53fc7e5); + run_division_test(0x1f759ee0b81dcc772682f40ebffb0, 0x167ec3ed79e55af7bd2d80b89c1a2308, 0x0, 0x16603f3888ecab6083fd8f64cd3fea); + run_division_test(0x1942fb52cd7b845ec1f0baf6c4eb1, 0xa32bfb866092db90fe4b43e847d0074f, 0x0, 0x27a209af1d8918a287093736dc0f1); + run_division_test(0x40e38886d28ca946f16aa26424243, 0x3296ac028d8f1ab8b78a0c05299e8c49, 0x0, 0x1485d8aab412f548495df395399e8b); + run_division_test(0x2941a1e28f5541a80859ce2f2fc4f, 0x4c744a392588e6801597ddf8611bbf82, 0x0, 0x8a24a5d0d212e897ddc97671b457d); + run_division_test(0x12ba728d7d93924d73e5292b37627, 0x2432897ea1854505bdae6d810ee658db, 0x0, 0x8473e91137f0d7c56a7fc231ab83e); + run_division_test(0xd3b366a58fe3821a0299458a948e697c, 0xd36969b2718202cecefc06c537e6201f, 0x1, 0x5997bab757f3870d31faee27348ea1); + run_division_test(0x1c5531122412a124204881c422341974, 0x16bdecf1affca09bf42a85af2791b978, 0x1, 0x3eef6a228c9ed60eae1635602d1dd8fb); + run_division_test(0x7662d55984edd61d7e4d6da1c8f7f609, 0xf7c7cfbb07a727f624ed66ea1b822860, 0x0, 0x7a502f6f5d0c2dfaf29130900236627a); + run_division_test(0x394f5c6063a9bf5e78f3e342cccc96e, 0x4b53133fe701640e79961ed2a272ef12, 0x0, 0xc2c67f91abe37777ac2c2be19bb40bc); + run_division_test(0x50d5c44bd44ff124dd8a11ed7f13a12c, 0x725eb08a055bbdc1fe4dcacbb0f9e91a, 0x0, 0xb4efecbd889dee1b3445025fe257e7c0); + run_division_test(0x73c461991f9afba0594ac191776804a, 0x2520a3758144f7e7dd322b111dfbc24d, 0x0, 0x31e3b948bf3b6f8ac8a45cbb8c1ef438); + run_division_test(0x8c4656479123f4eff6703486c19742b5, 0x4fcfca216a1678f42c518cf2e99db0a7, 0x1, 0xc1f0399a9922d8d80872a47d7566e782); + run_division_test(0x648bc4ed42f73926ddfdce290ea950d1, 0xb0df10778bc3e270b670a9726a3a45a2, 0x0, 0x91873866dddaf9454fa9adeb86389398); + run_division_test(0xe2d59629c914cc2a19ec4650788a4655, 0x9f7e128797dce9278d888f512c693456, 0x1, 0x6c16ff2e40d36f87e0dfb54f55f2e7dc); + run_division_test(0xd5ada7bd58e9ce4459e9e75eb51015d2, 0xd7b7da8e50facc3edf649ced43227461, 0x0, 0xfd944a1de820c5fff7d6ca236f4fef5e); + run_division_test(0x27209843a980001e1b4f4f299621b0b0, 0xbad89583a1b2c76f8b7dfcbabf468b9e, 0x0, 0x359bdb754935f619c47597a16f7356c1); + run_division_test(0x545b4f21dbadb9d37e89472c650225e1, 0x247a18df5fa84d3854ad14828029f222, 0x2, 0x5006bbc13f486d49106cf96a3540be0f); + run_division_test(0xaec80ada660da52cbbe451080be32c0f, 0x2268066ba28c4bb236e2b6250a4b2cfd, 0x5, 0x14757c4e1377d290033f17b2ef44f7e6); + run_division_test(0x6cc203648df9ab95dc25b57d2a478bc6, 0x8c70b4964dede00784b7073f220c5e4d, 0x0, 0xc63f83370774258aee728c6c6d73c9c3); + run_division_test(0xa0cf063c914870f20f741551e6a67c27, 0xbc82ef9ab8b00c03e1664b4876f35b76, 0x0, 0xda612163555e568ce463da0fa2ffa736); + run_division_test(0x455ca50ab17ee49d71dfee7982c4c465, 0x425cc9dd112fabccd39f26cf4d219986, 0x1, 0xb92158c612d13d5b000fc3f50accf57); + run_division_test(0xba4657b0e8f75f2c3649977fa5041f, 0xdb82d09bfffc45d103493d5e4041ccba, 0x0, 0xd93d2d1f74f7e989a883825f33d1bc); + run_division_test(0x36c160744ac8b8691dce439885fcd551, 0xea3d9a2cc9fd334d0d907cefc88b7a16, 0x0, 0x3bd77efa7479897df8454f6f2cec26b0); + run_division_test(0x56f641e2d09092a6e64946c369024aa7, 0xdee3d96895f88628e7f9df4d85303d44, 0x0, 0x63e147d8094e54587636cc8555d3f001); + run_division_test(0xba4014de01b2b795afadf8f53904079d, 0xcf284844718f47ddee8f64eec36ceada, 0x0, 0xe629e183a78c1ef971e3ec728a5add51); + run_division_test(0x972e8d62faa9f723704439ccd7ce48d8, 0x2fc13ff50e9c037ad5e6628c955f9, 0x32a7, 0x1194dbb1305b6b8e46f37aac18b3de96); + run_division_test(0x11dec77d7e80a0ae8c536ef94880abef, 0x6224ad9c0aaa94643a94910213ccc, 0x2e9, 0xcff7aed9835aeb72b94e106c1dc5c50a); + run_division_test(0x9be1d94f1f4098955784b57ff8273a22, 0x58a2cac5d356a92f5d0b570e73490, 0x1c23, 0x8df262d4df5f763e23950ead0e91d6d4); + run_division_test(0xba91f625f0295355be949213660a642, 0x5020b063d23456bbe969e1c6b429e, 0x254, 0x12b6b8fbf5ad834d088ea966bc6867c1); + run_division_test(0x7cb2609973cdab1afb62ba9e51d4a1e9, 0x11cc876c900dacb1c610dcfefc583, 0x7017, 0xfa75d3ec798471ba9599724ff3a5dc50); + run_division_test(0xeb176d1db709409c8f58579bcaa08768, 0x5f399216c88317c100b77c87c489f, 0x2780, 0x37dc5fd07995a448d350baed20a9fbcf); + run_division_test(0x4e1beb53e4bf713a2aabfd583cd4961a, 0x48125e52224ac858cdf20ae5410c9, 0x1157, 0x1d224a87b3c7fb3ad145cf875a63253c); + run_division_test(0x8dce5cdd1f89e4ab5d5b5ccab318512, 0x2a6b118868bbabe5b94547cfd1ae3, 0x357, 0xd1d8a24161e31dd4ec62f41030772908); + run_division_test(0x72e39155460d8c438d1b57faa36d45e, 0x2c582908deb8c9887dd38c1e21fc8, 0x297, 0x40ecce8073937cc462ba2e0e1acd525c); + run_division_test(0x385d3e35206e5492c0853df318597708, 0x276219b77cf64eb5a022e4f58208d, 0x16e6, 0x17041dd97bf0d104b1a8941ad435c63d); + run_division_test(0x93878657cc702dd24e3f3b2ead2212, 0x184c9a82ed59a289b4e149974954a, 0x61, 0x244a939131a571cee36457adf293fc87); + run_division_test(0x765f6f501bd0643900e91ee8c59c2de2, 0x22ce6a7573c52148bc69c0c965d5b, 0x366a, 0x164c94fd8da4330553b8d434d43fa546); + run_division_test(0xc2264b74eef484f58a47b24237f777dd, 0x6134cd276ef121755541f782d0dbc, 0x1ff4, 0xebd41213d981bfdbb6f4386ff66c0d07); + run_division_test(0x8ec51ef7dc01cc5b97dcb0aa006148ef, 0x2a1d021bcb2e7111c6c6af35b4848, 0x363e, 0x2d4190e0dd8ce7e5d679eb6f1a59725); + run_division_test(0x40a18d9b527f220c55ed3de006789ecf, 0x41aad898324f93145d3f132f5cb06, 0xfbf, 0x5c5cc0a7efa0b44f213e00a99fac30be); + run_division_test(0xa9877d2e3b18f89b2da40f6d1107f24a, 0x36072d466a5adb9c495bcba3ed505, 0x3234, 0x6f75a473914c0bcef15f264d688b53ef); + run_division_test(0xd51530cc17577384d6af305bc5a5cbc2, 0x33324fd97851afb64721e1f549f7d, 0x4297, 0xc6f3674200c3fba2420c658bb5ff7bbe); + run_division_test(0x8bdf568efc85dd92b1412c6ecf7a4b49, 0x4280cbbe7563e118298d6a182afb5, 0x21a6, 0xe58acdcffe698a1764dc4887fbedd386); + run_division_test(0x57064fdb806bfe1ceb12e5ebf10dc7de, 0x641248868f25b8272b09e3d1d0462, 0xde9, 0xfc689302e34235af64a7f900464cbc05); + run_division_test(0xc366628c8d7a8a8cb88ef5d8b1d9a9e5, 0x1a930a1a6009c101b3cc9d9731029, 0x75a5, 0xab302c9f1cf2cf5261587bfb12d95c3e); } -// TODO(baitcode): more tests needed \ No newline at end of file +#[test] +fn test_division_and_multiplication_by() { + run_division_and_multiplication_test(0xb6926f9c968555c93f44dcb82000, 0x41c4810843aadccbe5f4a3296ffe4bb5, 0x160f06301942d88312821d3a839a4, 0x3d3c3d6fb3ca40352cff8c194, 0x582ae3129825142d2dc28762d63e0f58); + run_division_and_multiplication_test(0x6035c082fa70fbef4b5a85a98e01, 0x7cb3ac8362c55a5be8487272a01fd816, 0x43219b60eb0d907b268bb69c4527c, 0x33cb09fc0bae7b82906c11047, 0x868ff1cfd2764c95997b06eabccc4d40); + run_division_and_multiplication_test(0x335ee4dd963f05e07c48132d3bb65, 0xd5b1a9ac31100b1b2af9047b3d20349c, 0x1d1d6d2ed2dd01e998c269b0b134d, 0x6ffc1a3eeaac6ec613af20a41, 0x4a5a834376064549e4fb70e32c2573fe); + run_division_and_multiplication_test(0x4a9774ef9e7ef7eb2dd37769b2a57, 0x68b7106ce82b8ec285a2b8640f2a6da6, 0x646125b6c41a49096450cdc52924a, 0x4780d49c8c60b6cbc5e94c082e, 0x72e69388d0529ac7e59850af478e5e2c); + run_division_and_multiplication_test(0x554b70e6865711eb212f9d59e1b11, 0xbfbbff82df1475549915c1bd16b303c6, 0x4247801f11f5fc008fd15d67d66dc, 0x1d7c23aa4508018d55a9c117df, 0xfedf54c4a67a241a38f65635e9904f48); + run_division_and_multiplication_test(0x2635985ea60cc205e45baff6a7ee9, 0x90db0a922a99f010e8a609c6bb5d6510, 0x3f13076999b03d83924fdcb600da9, 0x10a330845ab2fa85b1d7e5af32, 0x20db0d441b9a34e81b377a3940946447); + run_division_and_multiplication_test(0x3af0fe8cb844fd197f40920d84aff, 0x3ab67d323393037cb9d7043d2e815039, 0x3c1fb9fe9f771d2577003f15a3def, 0x3c5ba34f048e25f5c13ca1ef38, 0x63aa2ece2991af79ff6dd847a7133a9b); + run_division_and_multiplication_test(0x2fff0e318bc58a255c24c6198e92c, 0xd9db3436c10f394bba16356a6d09a1e8, 0x50d71bba5cf899f5100ee6c02589d, 0x11cf60817aad5321294c5eb21b, 0x7ff7f8b4ea16fa9e0754b4ba40b1be32); + run_division_and_multiplication_test(0x2975a99c4202288eded6c0bbb649e, 0xc649ac895a1508350db5ae616298aac3, 0x478a91ad0851ab464a44a85160c04, 0xef55bd0a66b3db457c56689b6, 0x495a49cda4c4ea265fe0f330e806cdac); + run_division_and_multiplication_test(0x573ac22601c6360b8e5571c77f31f, 0x2fca44a2e6623b942129e06b401721f9, 0xd9b248ab9ec3b82fed8cfdb98121, 0x18d5a6e8e390b79195322bb41d, 0x730fe7a2de9830319bcd48857fd3aa60); + run_division_and_multiplication_test(0xb8236135be634964dbb1d7ca4ee2, 0xbd7a7c0e74eefa07636828c786211c01, 0x493125b307895ecab94bb1e7d7c34, 0x47210f6b9a0f95e89d402f3c8, 0x810a70b06584f1c0ed996072e721beec); + run_division_and_multiplication_test(0x38be1297f1790ca89198af5bda491, 0x7f1d9c3672fcb012b6db3ebf52af8bcb, 0x332893148fac01cfbadfcef52618b, 0x16d61bd8798fecf051afacf972, 0xc64ddb7404ea4cc9d6f09dfcbe160cbf); + run_division_and_multiplication_test(0x412caa16f8f226f6caa898c14cfba, 0x5bb1cadc77ed2d385e13c742726a21c, 0x331ce4f6e3acd5458e7346ec58db0, 0x245475ce928b41b5dfec2d292ab, 0x54e2cedb87fa43542894b2a86d7b7ab0); + run_division_and_multiplication_test(0xe64837dd9dd8ace2b323469ae1eb, 0x5066a7de39e0051d3d16976f66a32545, 0x5592705e6f6ed681a20ba05f213df, 0xf517c207c107dca75dcfaea81, 0xc15b151ca62b2655d2818a72e985604b); + run_division_and_multiplication_test(0x36fd1613995dc80fbe308818c79ba, 0xb0083e398bfea9d03d425e98709eb8a9, 0x4b23cc69d12a5573288ced538eecc, 0x1778d7fe5cfeee213044b5c9de, 0xf3f5f73be7953dddbd16940439016e04); + run_division_and_multiplication_test(0x4a0f4afdd6a44b835c1610f278e64, 0x52ef3818dd603e8578ec93599baacc70, 0x3d8c40ec1c1b1d1afc22f9dbe529a, 0x36f63255a9aa67fe0ef6f34e2e, 0x2b25cc87ac38c591d79bb6c4c23c200e); + run_division_and_multiplication_test(0x58899a525a75cc8c5b1b3c258a7c6, 0x741be914c208c2d318e9502d1e4dd96e, 0x4347a94e0a5669d7e28875f8211df, 0x334db0ff3702401111a61afa3e, 0x9ea1caa92e2c3200c58144408c92239e); + run_division_and_multiplication_test(0x265ef5082e17985e64c81d1045ed6, 0xdc7804734b98298137d81aceb4e3eccb, 0x27b7beb59c34bfbb43abf1e27a3a1, 0x6e99e721d7835a236753ecbe2, 0xdd5a377d4d47ed7827ac355231890e38); + run_division_and_multiplication_test(0x559e8cdbb81c3f968ad4689eb5b16, 0x3b31a36d48ad95a110085130925b10a7, 0x15ba0f54e2d9c97ebdf3382004737, 0x1f6d156ccc2da76a3142fc641e, 0xf23c25325683a107c8088ed98b30cf1c); + run_division_and_multiplication_test(0x384501318ddd8aff4d1852cf90188, 0x8706f6c0efb2265a01df36a9ccf34aed, 0x2cc3c8ab2deb16e9f11dec2bce424, 0x12a79b6e5d2a11e661e5bf9bc4, 0x4bdbce63925867cedc8ccd35d7fe6028); +} + +#[test] +#[should_panic(expected: 'DIVISION_BY_ZERO')] +fn test_division_by_zero() { + run_division_test(56, 0, 0, 0); +} From 1a2d6fce3916ceebbc5d088a280dd69effbc1294 Mon Sep 17 00:00:00 2001 From: baitcode Date: Sat, 4 Jan 2025 02:16:52 +0300 Subject: [PATCH 16/40] some comment fixes --- src/staker.cairo | 27 ++++++++++++++++++++++++- src/utils/fp.cairo | 45 +++++++++++++++++++++++++++-------------- src/utils/fp_test.cairo | 8 ++++---- 3 files changed, 60 insertions(+), 20 deletions(-) diff --git a/src/staker.cairo b/src/staker.cairo index 76dc396..4fcccc6 100644 --- a/src/staker.cairo +++ b/src/staker.cairo @@ -107,13 +107,38 @@ pub mod Staker { } } - #[derive(Drop, Serde, starknet::Store)] + #[derive(Drop, Serde)] struct StakingLogRecord { timestamp: u64, total_staked: u128, cumulative_seconds_per_total_staked: UFixedPoint, } + pub impl StakingLogRecordStorePacking of StorePacking { + fn pack(value: StakingLogRecord) -> (felt252, felt252) { + let first: felt252 = u256 { + high: value.timestamp.into(), + low: value.total_staked, + }.try_into().unwrap(); + + let second: felt252 = value.cumulative_seconds_per_total_staked + .try_into() + .unwrap(); + + (first, second) + } + + fn unpack(value: (felt252, felt252)) -> StakingLogRecord { + let (packed_ts_total_staked, cumulative_seconds_per_total_staked) = value; + let medium: u256 = packed_ts_total_staked.into(); + StakingLogRecord { + timestamp: medium.high.try_into().unwrap(), + total_staked: medium.low, + cumulative_seconds_per_total_staked: cumulative_seconds_per_total_staked.try_into().unwrap(), + } + } + } + #[storage] struct Storage { token: IERC20Dispatcher, diff --git a/src/utils/fp.cairo b/src/utils/fp.cairo index 628f5c8..ec3cfbf 100644 --- a/src/utils/fp.cairo +++ b/src/utils/fp.cairo @@ -10,20 +10,20 @@ pub struct UFixedPoint { pub(crate) value: u512 } -pub impl UFixedPointStorePacking of StorePacking { - fn pack(value: UFixedPoint) -> u256 { - value.into() +pub impl UFixedPointStorePacking of StorePacking { + fn pack(value: UFixedPoint) -> felt252 { + value.try_into().unwrap() } - fn unpack(value: u256) -> UFixedPoint { + fn unpack(value: felt252) -> UFixedPoint { value.into() } } pub impl UFixedPointPartialEq of PartialEq { fn eq(lhs: @UFixedPoint, rhs: @UFixedPoint) -> bool { - let left: u256 = (*lhs).into(); - let right: u256 = (*rhs).into(); + let left: u256 = (*lhs).try_into().unwrap(); + let right: u256 = (*rhs).try_into().unwrap(); let diff = if left > right { left - right } else { @@ -57,12 +57,12 @@ pub impl UFixedPointZero of Zero { impl UFixedPointSerde of core::serde::Serde { fn serialize(self: @UFixedPoint, ref output: Array) { - let value: u256 = (*self).try_into().unwrap(); + let value: felt252 = (*self).try_into().unwrap(); Serde::serialize(@value, ref output) } fn deserialize(ref serialized: Span) -> Option { - let value: u256 = Serde::deserialize(ref serialized)?; + let value: felt252 = Serde::deserialize(ref serialized)?; Option::Some(value.into()) } } @@ -106,6 +106,13 @@ pub(crate) impl U256IntoUFixedPoint of Into { } } +pub(crate) impl Felt252IntoUFixedPoint of Into { + fn into(self: felt252) -> UFixedPoint { + let medium: u256 = self.into(); + medium.into() + } +} + #[generate_trait] pub impl UFixedPointImpl of UFixedPointTrait { fn get_integer(self: UFixedPoint) -> u128 { @@ -154,13 +161,21 @@ pub impl UFixedPointShiftImpl of BitShiftImpl { } } -pub(crate) impl FixedPointIntoU256 of Into { - fn into(self: UFixedPoint) -> u256 { self.value.try_into().unwrap() } +pub(crate) impl FixedPointIntoU256 of TryInto { + fn try_into(self: UFixedPoint) -> Option { self.value.try_into() } } +pub(crate) impl FixedPointIntoFelt252 of TryInto { + fn try_into(self: UFixedPoint) -> Option { + let medium: u256 = self.try_into().unwrap(); + medium.try_into() + } +} + + pub impl UFpImplAdd of Add { fn add(lhs: UFixedPoint, rhs: UFixedPoint) -> UFixedPoint { - let sum: u256 = rhs.into() + lhs.into(); + let sum: u256 = rhs.try_into().unwrap() + lhs.try_into().unwrap(); UFixedPoint { value: u512 { limb0: sum.low, @@ -174,7 +189,7 @@ pub impl UFpImplAdd of Add { pub impl UFpImplSub of Sub { fn sub(lhs: UFixedPoint, rhs: UFixedPoint) -> UFixedPoint { - let sum: u256 = rhs.into() - lhs.into(); + let sum: u256 = rhs.try_into().unwrap() - lhs.try_into().unwrap(); UFixedPoint { value: u512 { limb0: sum.low, @@ -189,8 +204,8 @@ pub impl UFpImplSub of Sub { // 20100 pub impl UFpImplMul of Mul { fn mul(lhs: UFixedPoint, rhs: UFixedPoint) -> UFixedPoint { - let left: u256 = lhs.into(); - let right: u256 = rhs.into(); + let left: u256 = lhs.try_into().unwrap(); + let right: u256 = rhs.try_into().unwrap(); let res = left.wide_mul(right); @@ -200,7 +215,7 @@ pub impl UFpImplMul of Mul { pub impl UFpImplDiv of Div { fn div(lhs: UFixedPoint, rhs: UFixedPoint) -> UFixedPoint { - let rhs: u256 = rhs.into(); + let rhs: u256 = rhs.try_into().unwrap(); assert(rhs != 0, 'DIVISION_BY_ZERO'); let (result, _) = u512_safe_div_rem_by_u256( diff --git a/src/utils/fp_test.cairo b/src/utils/fp_test.cairo index d9a1cae..fb83622 100644 --- a/src/utils/fp_test.cairo +++ b/src/utils/fp_test.cairo @@ -11,7 +11,7 @@ fn test_add() { let f1 : UFixedPoint = 0xFFFFFFFFFFFFFFFF_u64.into(); let f2 : UFixedPoint = 1_u64.into(); let res = f1 + f2; - let z: u256 = res.into(); + let z: u256 = res.try_into().unwrap(); assert(z.low == 0, 'low 0'); assert(z.high == 18446744073709551616, 'high 18446744073709551616'); } @@ -22,7 +22,7 @@ fn test_fp_value_mapping() { assert(f1.value.limb0 == 0x0, 'limb0 == 0'); assert(f1.value.limb1 == 0x7, 'limb1 == 7'); - let val: u256 = f1.into(); + let val: u256 = f1.try_into().unwrap(); assert(val == 7_u256*0x100000000000000000000000000000000, 'val has to be 128 bit shifted'); } @@ -39,7 +39,7 @@ fn test_mul() { assert(expected.limb2 == 49, 'limb2==0'); assert(expected.limb3 == 0, 'limb3==0'); - let res: u256 = (f1 * f2).into(); + let res: u256 = (f1 * f2).try_into().unwrap(); assert(res.high == 49, 'high 49'); assert(res.low == 0, 'low 0'); } @@ -64,7 +64,7 @@ fn test_multiplication() { assert(expected.low == 0, 'low == 0'); assert(expected.high == 0x40000000000000000000000000000000, 'high != 0'); - let result: u256 = res.into(); + let result: u256 = res.try_into().unwrap(); assert(result == expected, 'unexpected mult result'); } From b18d702a5a2ff1e4ba9989e819c58074377748f9 Mon Sep 17 00:00:00 2001 From: baitcode Date: Mon, 6 Jan 2025 21:58:50 +0300 Subject: [PATCH 17/40] Extracted staker storage into separate file due to large amount of boilerplate. Implemented custom packing and custom data SubPointers. --- src/lib.cairo | 1 + src/staker.cairo | 34 +---------- src/staker_storage.cairo | 118 +++++++++++++++++++++++++++++++++++++++ src/staker_test.cairo | 4 +- src/utils/fp_test.cairo | 78 +++++++++++++------------- 5 files changed, 162 insertions(+), 73 deletions(-) create mode 100644 src/staker_storage.cairo diff --git a/src/lib.cairo b/src/lib.cairo index e73c083..3bf2933 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -16,6 +16,7 @@ pub mod governor; mod governor_test; pub mod staker; +pub mod staker_storage; #[cfg(test)] mod staker_test; diff --git a/src/staker.cairo b/src/staker.cairo index 4fcccc6..13b4b54 100644 --- a/src/staker.cairo +++ b/src/staker.cairo @@ -72,6 +72,7 @@ pub mod Staker { Vec, VecTrait, MutableVecTrait, }; use crate::utils::fp::{UFixedPoint, UFixedPointZero}; + use crate::staker_storage::{StakingLogRecord}; use starknet::{ get_block_timestamp, get_caller_address, get_contract_address, @@ -107,41 +108,10 @@ pub mod Staker { } } - #[derive(Drop, Serde)] - struct StakingLogRecord { - timestamp: u64, - total_staked: u128, - cumulative_seconds_per_total_staked: UFixedPoint, - } - - pub impl StakingLogRecordStorePacking of StorePacking { - fn pack(value: StakingLogRecord) -> (felt252, felt252) { - let first: felt252 = u256 { - high: value.timestamp.into(), - low: value.total_staked, - }.try_into().unwrap(); - - let second: felt252 = value.cumulative_seconds_per_total_staked - .try_into() - .unwrap(); - - (first, second) - } - - fn unpack(value: (felt252, felt252)) -> StakingLogRecord { - let (packed_ts_total_staked, cumulative_seconds_per_total_staked) = value; - let medium: u256 = packed_ts_total_staked.into(); - StakingLogRecord { - timestamp: medium.high.try_into().unwrap(), - total_staked: medium.low, - cumulative_seconds_per_total_staked: cumulative_seconds_per_total_staked.try_into().unwrap(), - } - } - } - #[storage] struct Storage { token: IERC20Dispatcher, + // owner, delegate => amount staked: Map<(ContractAddress, ContractAddress), u128>, amount_delegated: Map, diff --git a/src/staker_storage.cairo b/src/staker_storage.cairo new file mode 100644 index 0000000..562433e --- /dev/null +++ b/src/staker_storage.cairo @@ -0,0 +1,118 @@ +use starknet::{ContractAddress, Store}; +use starknet::storage_access::{StorePacking}; +use starknet::storage::{StoragePointer, SubPointers, SubPointersMut, Mutable}; +use crate::utils::fp::{UFixedPoint}; + + +#[derive(Drop, Serde)] +pub(crate) struct StakingLogRecord { + pub(crate) timestamp: u64, + pub(crate) total_staked: u128, + pub(crate) cumulative_seconds_per_total_staked: UFixedPoint, +} + +pub(crate) impl StakingLogRecordStorePacking of StorePacking { + fn pack(value: StakingLogRecord) -> (felt252, felt252) { + let first: felt252 = u256 { + high: value.timestamp.into(), + low: value.total_staked, + }.try_into().unwrap(); + + let second: felt252 = value.cumulative_seconds_per_total_staked + .try_into() + .unwrap(); + + (first, second) + } + + fn unpack(value: (felt252, felt252)) -> StakingLogRecord { + let (packed_ts_total_staked, cumulative_seconds_per_total_staked) = value; + let medium: u256 = packed_ts_total_staked.into(); + StakingLogRecord { + timestamp: medium.high.try_into().unwrap(), + total_staked: medium.low, + cumulative_seconds_per_total_staked: cumulative_seconds_per_total_staked.try_into().unwrap(), + } + } +} + + +#[derive(Drop, Copy)] +pub(crate) struct StakingLogRecordSubPointers { + pub(crate) timestamp: StoragePointer, + pub(crate) total_staked: StoragePointer, + pub(crate) cumulative_seconds_per_total_staked: StoragePointer, +} + +pub(crate) impl StakingLogRecordSubPointersImpl of SubPointers { + + type SubPointersType = StakingLogRecordSubPointers; + + fn sub_pointers(self: StoragePointer) -> StakingLogRecordSubPointers { + let base_address = self.__storage_pointer_address__; + + let mut current_offset = self.__storage_pointer_offset__; + let __packed_low_128__ = StoragePointer { + __storage_pointer_address__: base_address, + __storage_pointer_offset__: current_offset, + }; + + let __packed_high_124__ = StoragePointer { + __storage_pointer_address__: base_address, + __storage_pointer_offset__: current_offset + Store::::size(), + }; + + current_offset = current_offset + Store::::size(); + let __packed_felt2__ = StoragePointer { + __storage_pointer_address__: base_address, + __storage_pointer_offset__: current_offset, + }; + + StakingLogRecordSubPointers { + timestamp: __packed_high_124__, + total_staked: __packed_low_128__, + cumulative_seconds_per_total_staked: __packed_felt2__, + } + } +} + +#[derive(Drop, Copy)] +pub(crate) struct StakingLogRecordSubPointersMut { + pub(crate) timestamp: StoragePointer>, + pub(crate) total_staked: StoragePointer>, + pub(crate) cumulative_seconds_per_total_staked: StoragePointer>, +} + +pub(crate) impl StakingLogRecordSubPointersMutImpl of SubPointersMut { + + type SubPointersType = StakingLogRecordSubPointersMut; + + fn sub_pointers_mut( + self: StoragePointer>, + ) -> StakingLogRecordSubPointersMut { + let base_address = self.__storage_pointer_address__; + + let mut current_offset = self.__storage_pointer_offset__; + let __packed_low_128__ = StoragePointer { + __storage_pointer_address__: base_address, + __storage_pointer_offset__: current_offset, + }; + + let __packed_high_124__ = StoragePointer { + __storage_pointer_address__: base_address, + __storage_pointer_offset__: current_offset + Store::::size(), + }; + + current_offset = current_offset + Store::::size(); + let __packed_felt2__ = StoragePointer { + __storage_pointer_address__: base_address, + __storage_pointer_offset__: current_offset, + }; + + StakingLogRecordSubPointersMut { + timestamp: __packed_high_124__, + total_staked: __packed_low_128__, + cumulative_seconds_per_total_staked: __packed_felt2__, + } + } +} diff --git a/src/staker_test.cairo b/src/staker_test.cairo index b53ed7e..4614424 100644 --- a/src/staker_test.cairo +++ b/src/staker_test.cairo @@ -467,8 +467,8 @@ mod staker_staked_seconds_per_total_staked_calculation { fn assert_fp(value: UFixedPoint, integer: u128, fractional: u128) { - assert(value.get_integer() == integer, 'Integer part is not correct'); - assert(value.get_fractional() == fractional, 'Fractional part is not correct'); + assert_eq!(value.get_integer(), integer); + assert_eq!(value.get_fractional(), fractional); } diff --git a/src/utils/fp_test.cairo b/src/utils/fp_test.cairo index fb83622..fb45209 100644 --- a/src/utils/fp_test.cairo +++ b/src/utils/fp_test.cairo @@ -12,18 +12,18 @@ fn test_add() { let f2 : UFixedPoint = 1_u64.into(); let res = f1 + f2; let z: u256 = res.try_into().unwrap(); - assert(z.low == 0, 'low 0'); - assert(z.high == 18446744073709551616, 'high 18446744073709551616'); + assert_eq!(z.low, 0); + assert_eq!(z.high, 18446744073709551616); } #[test] fn test_fp_value_mapping() { let f1 : UFixedPoint = 7_u64.into(); - assert(f1.value.limb0 == 0x0, 'limb0 == 0'); - assert(f1.value.limb1 == 0x7, 'limb1 == 7'); + assert_eq!(f1.value.limb0, 0x0); + assert_eq!(f1.value.limb1, 0x7); let val: u256 = f1.try_into().unwrap(); - assert(val == 7_u256*0x100000000000000000000000000000000, 'val has to be 128 bit shifted'); + assert_eq!(val, 7_u256*0x100000000000000000000000000000000); } @@ -34,35 +34,35 @@ fn test_mul() { let expected = (7_u256*SCALE_FACTOR).wide_mul(7_u256*SCALE_FACTOR); - assert(expected.limb0 == 0, 'limb0==0'); - assert(expected.limb1 == 0, 'limb1==0'); - assert(expected.limb2 == 49, 'limb2==0'); - assert(expected.limb3 == 0, 'limb3==0'); + assert_eq!(expected.limb0, 0); + assert_eq!(expected.limb1, 0); + assert_eq!(expected.limb2, 49); + assert_eq!(expected.limb3, 0); let res: u256 = (f1 * f2).try_into().unwrap(); - assert(res.high == 49, 'high 49'); - assert(res.low == 0, 'low 0'); + assert_eq!(res.high, 49); + assert_eq!(res.low, 0); } #[test] fn test_multiplication() { let f1 : UFixedPoint = 9223372036854775808_u128.into(); - assert(f1.value.limb0 == 0, 'f1.limb0 0= 0'); - assert(f1.value.limb1 == 9223372036854775808_u128, 'f1.limb1 != 0'); - assert(f1.value.limb2 == 0, 'f1.limb2 == 0'); - assert(f1.value.limb3 == 0, 'f1.limb3 == 0'); + assert_eq!(f1.value.limb0, 0); + assert_eq!(f1.value.limb1, 9223372036854775808_u128); + assert_eq!(f1.value.limb2, 0); + assert_eq!(f1.value.limb3, 0); let res = f1 * f1; - assert(res.value.limb0 == 0, 'res.limb0 != 0'); - assert(res.value.limb1 == 0x40000000000000000000000000000000, 'res.limb1 != 0'); - assert(res.value.limb2 == 0, 'res.limb2 == 0'); - assert(res.value.limb3 == 0, 'res.limb3 == 0'); + assert_eq!(res.value.limb0, 0); + assert_eq!(res.value.limb1, 0x40000000000000000000000000000000); + assert_eq!(res.value.limb2, 0); + assert_eq!(res.value.limb3, 0); let expected = 9223372036854775808_u128.wide_mul(9223372036854775808_u128) * SCALE_FACTOR; - assert(expected.low == 0, 'low == 0'); - assert(expected.high == 0x40000000000000000000000000000000, 'high != 0'); + assert_eq!(expected.low, 0); + assert_eq!(expected.high, 0x40000000000000000000000000000000); let result: u256 = res.try_into().unwrap(); assert(result == expected, 'unexpected mult result'); @@ -72,42 +72,42 @@ fn test_multiplication() { fn test_u256_conversion() { let f: u256 = 0x0123456789ABCDEFFEDCBA987654321000112233445566778899AABBCCDDEEFF_u256; - assert(f.low == 0x00112233445566778899AABBCCDDEEFF, 'low'); - assert(f.high == 0x0123456789ABCDEFFEDCBA9876543210, 'high'); + assert_eq!(f.low, 0x00112233445566778899AABBCCDDEEFF); + assert_eq!(f.high, 0x0123456789ABCDEFFEDCBA9876543210); // BITSHIFT DOWN let fp: UFixedPoint = f.into(); - assert(fp.get_integer() == f.high, 'integer == f.high'); - assert(fp.get_fractional() == f.low, 'fractional == f.low'); + assert_eq!(fp.get_integer(), f.high); + assert_eq!(fp.get_fractional(), f.low); let fp = fp.bitshift_128_down(); - assert(fp.get_integer() == 0, 'integer==0 bs_down'); - assert(fp.get_fractional() == f.high, 'fractional == f.low bs_down'); + assert_eq!(fp.get_integer(), 0); + assert_eq!(fp.get_fractional(), f.high); let fp = fp.bitshift_128_down(); - assert(fp.get_integer() == 0, 'integer==0 bs_down 2'); - assert(fp.get_fractional() == 0, 'fractional == 0 bs_down 2'); + assert_eq!(fp.get_integer(), 0); + assert_eq!(fp.get_fractional(), 0); // BITSHIFT UP let fp: UFixedPoint = f.into(); - assert(fp.get_integer() == f.high, 'integer == f.high'); - assert(fp.get_fractional() == f.low, 'fractional == f.low'); + assert_eq!(fp.get_integer(), f.high); + assert_eq!(fp.get_fractional(), f.low); let fp = fp.bitshift_128_up(); - assert(fp.get_integer() == f.low, 'integer == f.high bs_up'); - assert(fp.get_fractional() == 0, 'fractional == f.low bs_up'); + assert_eq!(fp.get_integer(), f.low); + assert_eq!(fp.get_fractional(), 0); let fp = fp.bitshift_128_up(); - assert(fp.get_integer() == 0, 'integer == f.high bs_up'); - assert(fp.get_fractional() == 0, 'fractional == f.low bs_up'); + assert_eq!(fp.get_integer(), 0); + assert_eq!(fp.get_fractional(), 0); } fn run_division_test(left: u128, right: u128, expected_int: u128, expected_frac: u128) { let f1 : UFixedPoint = left.into(); let f2 : UFixedPoint = right.into(); let res = f1 / f2; - assert(res.get_integer() == expected_int, 'integer'); - assert(res.get_fractional() == expected_frac, 'fractional'); + assert_eq!(res.get_integer(), expected_int); + assert_eq!(res.get_fractional(), expected_frac); } fn run_division_and_multiplication_test(numenator: u128, divisor: u128, mult: u128, expected_int: u128, expected_frac: u128) { @@ -115,8 +115,8 @@ fn run_division_and_multiplication_test(numenator: u128, divisor: u128, mult: u1 let f2 : UFixedPoint = divisor.into(); let f3 : UFixedPoint = mult.into(); let res = f1 / f2 * f3; - assert(res.get_integer() == expected_int, 'integer'); - assert(res.get_fractional() == expected_frac, 'fractional'); + assert_eq!(res.get_integer(), expected_int); + assert_eq!(res.get_fractional(), expected_frac); } From 7fdfb19318fd881fe870eca1472462505f05b964 Mon Sep 17 00:00:00 2001 From: baitcode Date: Mon, 6 Jan 2025 23:31:32 +0300 Subject: [PATCH 18/40] rename staker_storage -> staker_log. moved all relevant logic there --- src/lib.cairo | 2 +- src/staker.cairo | 111 ++--------------- ...{staker_storage.cairo => staker_log.cairo} | 117 +++++++++++++++++- 3 files changed, 124 insertions(+), 106 deletions(-) rename src/{staker_storage.cairo => staker_log.cairo} (54%) diff --git a/src/lib.cairo b/src/lib.cairo index 3bf2933..e2e93a7 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -16,7 +16,7 @@ pub mod governor; mod governor_test; pub mod staker; -pub mod staker_storage; +pub mod staker_log; #[cfg(test)] mod staker_test; diff --git a/src/staker.cairo b/src/staker.cairo index 13b4b54..789766a 100644 --- a/src/staker.cairo +++ b/src/staker.cairo @@ -63,16 +63,15 @@ pub trait IStaker { #[starknet::contract] pub mod Staker { - use starknet::storage::StorageAsPath; + use super::super::staker_log::LogOperations; use core::num::traits::zero::{Zero}; use governance::interfaces::erc20::{IERC20Dispatcher, IERC20DispatcherTrait}; use starknet::storage::{ - Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePathEntry, StoragePath, + Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess, - Vec, VecTrait, MutableVecTrait, }; use crate::utils::fp::{UFixedPoint, UFixedPointZero}; - use crate::staker_storage::{StakingLogRecord}; + use crate::staker_log::{StakingLog}; use starknet::{ get_block_timestamp, get_caller_address, get_contract_address, @@ -118,7 +117,7 @@ pub mod Staker { delegated_cumulative_num_snapshots: Map, delegated_cumulative_snapshot: Map>, - staking_log: Vec, + staking_log: StakingLog, } #[constructor] @@ -228,100 +227,6 @@ pub mod Staker { } } - fn log_change(ref self: ContractState, amount: u128, is_add: bool) { - let log = self.staking_log.as_path(); - - if log.len() == 0 { - // Add the first record. If withdrawal, then it's underflow. - assert(is_add, 'BAD AMOUNT'); - - log.append().write( - StakingLogRecord { - timestamp: get_block_timestamp(), - total_staked: amount, - cumulative_seconds_per_total_staked: 0_u64.into(), - } - ); - - return; - } - - let last_record_ptr = log.at(log.len() - 1); - - let mut last_record = last_record_ptr.read(); - - let mut record = if last_record.timestamp == get_block_timestamp() { - // update record - last_record_ptr - } else { - // create new record - log.append() - }; - - // Might be zero - let seconds_diff = (get_block_timestamp() - last_record.timestamp) / 1000; - - let staked_seconds: UFixedPoint = if last_record.total_staked == 0 { - 0_u64.into() - } else { - seconds_diff.into() / last_record.total_staked.into() - }; - - let total_staked = if is_add { - // overflow check - assert(last_record.total_staked + amount >= last_record.total_staked, 'BAD AMOUNT'); - last_record.total_staked + amount - } else { - // underflow check - assert(last_record.total_staked >= amount, 'BAD AMOUNT'); - last_record.total_staked - amount - }; - - // Add a new record. - record.write( - StakingLogRecord { - timestamp: get_block_timestamp(), - total_staked: total_staked, - cumulative_seconds_per_total_staked: ( - last_record.cumulative_seconds_per_total_staked + staked_seconds - ), - } - ); - } - - fn find_in_change_log(self: @ContractState, timestamp: u64) -> Option { - // Find first log record in an array whos timestamp is less or equal to timestamp. - // Uses binary search. - let log = self.staking_log.as_path(); - - if log.len() == 0 { - return Option::None; - } - - let mut left = 0; - let mut right = log.len() - 1; - - // To avoid reading from the storage multiple times. - let mut result_ptr: Option> = Option::None; - - while (left <= right) { - let center = (right + left) / 2; - let record = log.at(center); - - if record.timestamp.read() <= timestamp { - result_ptr = Option::Some(record); - left = center + 1; - } else { - right = center - 1; - }; - }; - - if let Option::Some(result) = result_ptr { - return Option::Some(result.read()); - } - - return Option::None; - } } @@ -366,7 +271,7 @@ pub mod Staker { .amount_delegated .write(delegate, self.insert_snapshot(delegate, get_block_timestamp()) + amount); - self.log_change(amount, true); + self.staking_log.log_change(amount, true); self.emit(Staked { from, delegate, amount }); } @@ -396,7 +301,7 @@ pub mod Staker { .write(delegate, self.insert_snapshot(delegate, get_block_timestamp()) - amount); assert(self.token.read().transfer(recipient, amount.into()), 'TRANSFER_FAILED'); - self.log_change(amount, false); + self.staking_log.log_change(amount, false); self.emit(Withdrawn { from, delegate, to: recipient, amount }); } @@ -436,7 +341,7 @@ pub mod Staker { fn get_average_delegated( self: @ContractState, delegate: ContractAddress, start: u64, end: u64, ) -> u128 { - assert(end > start, 'ORDER'); + assert(end > start, '6'); assert(end <= get_block_timestamp(), 'FUTURE'); let start_snapshot = self.get_delegated_cumulative(delegate, start); @@ -453,7 +358,7 @@ pub mod Staker { } fn get_cumulative_seconds_per_total_staked_at(self: @ContractState, timestamp: u64) -> UFixedPoint { - if let Option::Some(log_record) = self.find_in_change_log(timestamp) { + if let Option::Some(log_record) = self.staking_log.find_in_change_log(timestamp) { let seconds_diff = (timestamp - log_record.timestamp) / 1000; let staked_seconds: UFixedPoint = if log_record.total_staked == 0 { diff --git a/src/staker_storage.cairo b/src/staker_log.cairo similarity index 54% rename from src/staker_storage.cairo rename to src/staker_log.cairo index 562433e..bbe4d3a 100644 --- a/src/staker_storage.cairo +++ b/src/staker_log.cairo @@ -1,8 +1,19 @@ -use starknet::{ContractAddress, Store}; +use starknet::storage::StoragePointerWriteAccess; +use starknet::storage::MutableVecTrait; +use starknet::{ContractAddress, Store, get_block_timestamp}; use starknet::storage_access::{StorePacking}; -use starknet::storage::{StoragePointer, SubPointers, SubPointersMut, Mutable}; + +use starknet::storage::{ + Vec, VecTrait +}; +use starknet::storage::{ + StoragePointer, StorageBase, StoragePath, StorageAsPath, + SubPointers, SubPointersMut, Mutable, StoragePointerReadAccess +}; + use crate::utils::fp::{UFixedPoint}; +pub type StakingLog = Vec; #[derive(Drop, Serde)] pub(crate) struct StakingLogRecord { @@ -11,6 +22,108 @@ pub(crate) struct StakingLogRecord { pub(crate) cumulative_seconds_per_total_staked: UFixedPoint, } +#[generate_trait] +pub impl StakingLogOperations of LogOperations { + fn find_in_change_log(self: @StorageBase, timestamp: u64) -> Option { + let log = self.as_path(); + + if log.len() == 0 { + return Option::None; + } + + let mut left = 0; + let mut right = log.len() - 1; + + // To avoid reading from the storage multiple times. + let mut result_ptr: Option> = Option::None; + + while (left <= right) { + let center = (right + left) / 2; + let record = log.at(center); + + if record.timestamp.read() <= timestamp { + result_ptr = Option::Some(record); + left = center + 1; + } else { + right = center - 1; + }; + }; + + if let Option::Some(result) = result_ptr { + return Option::Some(result.read()); + } + + return Option::None; + } + + // TODO: shall I use ref here? + fn log_change(self: StorageBase>, amount: u128, is_add: bool) { + let log = self.as_path(); + + if log.len() == 0 { + // Add the first record. If withdrawal, then it's underflow. + assert(is_add, 'BAD AMOUNT'); + + log.append().write( + StakingLogRecord { + timestamp: get_block_timestamp(), + total_staked: amount, + cumulative_seconds_per_total_staked: 0_u64.into(), + } + ); + + return; + } + + let last_record_ptr = log.at(log.len() - 1); + + let mut last_record = last_record_ptr.read(); + + let mut record = if last_record.timestamp == get_block_timestamp() { + // update record + last_record_ptr + } else { + // create new record + log.append() + }; + + // Might be zero + let seconds_diff = (get_block_timestamp() - last_record.timestamp) / 1000; + + let staked_seconds: UFixedPoint = if last_record.total_staked == 0 { + 0_u64.into() + } else { + seconds_diff.into() / last_record.total_staked.into() + }; + + let total_staked = if is_add { + // overflow check + assert(last_record.total_staked + amount >= last_record.total_staked, 'BAD AMOUNT'); + last_record.total_staked + amount + } else { + // underflow check + assert(last_record.total_staked >= amount, 'BAD AMOUNT'); + last_record.total_staked - amount + }; + + // Add a new record. + record.write( + StakingLogRecord { + timestamp: get_block_timestamp(), + total_staked: total_staked, + cumulative_seconds_per_total_staked: ( + last_record.cumulative_seconds_per_total_staked + staked_seconds + ), + } + ); + } +} + + +// +// Storage layout for StakingLogRecord +// + pub(crate) impl StakingLogRecordStorePacking of StorePacking { fn pack(value: StakingLogRecord) -> (felt252, felt252) { let first: felt252 = u256 { From b82953ff8af444fe59d3457bd41b737c7664721c Mon Sep 17 00:00:00 2001 From: baitcode Date: Tue, 7 Jan 2025 00:14:49 +0300 Subject: [PATCH 19/40] Simplified Fixed Point operations library. Reduced internal storage size. Added convienience method for u64 / u128 case. --- src/staker_log.cairo | 14 +-- src/utils/fp.cairo | 215 +++++++++++++++++----------------------- src/utils/fp_test.cairo | 24 ++--- 3 files changed, 110 insertions(+), 143 deletions(-) diff --git a/src/staker_log.cairo b/src/staker_log.cairo index bbe4d3a..026d0a0 100644 --- a/src/staker_log.cairo +++ b/src/staker_log.cairo @@ -1,17 +1,19 @@ -use starknet::storage::StoragePointerWriteAccess; use starknet::storage::MutableVecTrait; -use starknet::{ContractAddress, Store, get_block_timestamp}; +use starknet::{Store, get_block_timestamp}; use starknet::storage_access::{StorePacking}; use starknet::storage::{ Vec, VecTrait }; use starknet::storage::{ - StoragePointer, StorageBase, StoragePath, StorageAsPath, - SubPointers, SubPointersMut, Mutable, StoragePointerReadAccess + StoragePointer, + StorageBase, Mutable, + StoragePath, StorageAsPath, + SubPointers, SubPointersMut, + StoragePointerReadAccess, StoragePointerWriteAccess }; -use crate::utils::fp::{UFixedPoint}; +use crate::utils::fp::{UFixedPoint, div_u64_by_u128}; pub type StakingLog = Vec; @@ -93,7 +95,7 @@ pub impl StakingLogOperations of LogOperations { let staked_seconds: UFixedPoint = if last_record.total_staked == 0 { 0_u64.into() } else { - seconds_diff.into() / last_record.total_staked.into() + div_u64_by_u128(seconds_diff, last_record.total_staked) }; let total_staked = if is_add { diff --git a/src/utils/fp.cairo b/src/utils/fp.cairo index ec3cfbf..841ae48 100644 --- a/src/utils/fp.cairo +++ b/src/utils/fp.cairo @@ -1,13 +1,13 @@ use starknet::storage_access::{StorePacking}; use core::num::traits::{WideMul, Zero}; -use core::integer::{u512, u512_safe_div_rem_by_u256}; +use core::integer::{u512, u512_safe_div_rem_by_u256 }; pub const EPSILON: u256 = 0x10_u256; // 128.128 -#[derive(Drop, Copy)] +#[derive(Drop, Copy, Serde)] pub struct UFixedPoint { - pub(crate) value: u512 + pub(crate) value: u256 } pub impl UFixedPointStorePacking of StorePacking { @@ -22,13 +22,15 @@ pub impl UFixedPointStorePacking of StorePacking { pub impl UFixedPointPartialEq of PartialEq { fn eq(lhs: @UFixedPoint, rhs: @UFixedPoint) -> bool { - let left: u256 = (*lhs).try_into().unwrap(); - let right: u256 = (*rhs).try_into().unwrap(); + let left: u256 = (*lhs).value; + let right: u256 = (*rhs).value; + let diff = if left > right { left - right } else { right - left }; + diff < EPSILON } } @@ -36,74 +38,26 @@ pub impl UFixedPointPartialEq of PartialEq { pub impl UFixedPointZero of Zero { fn zero() -> UFixedPoint { UFixedPoint { - value: u512 { - limb0: 0, - limb1: 0, - limb2: 0, - limb3: 0, + value: u256 { + low: 0, + high: 0, } } } fn is_zero(self: @UFixedPoint) -> bool { - self.value.limb0 == @0 && - self.value.limb1 == @0 && - self.value.limb2 == @0 && - self.value.limb3 == @0 + self.value.is_zero() } fn is_non_zero(self: @UFixedPoint) -> bool { !self.is_zero() } } -impl UFixedPointSerde of core::serde::Serde { - fn serialize(self: @UFixedPoint, ref output: Array) { - let value: felt252 = (*self).try_into().unwrap(); - Serde::serialize(@value, ref output) - } - - fn deserialize(ref serialized: Span) -> Option { - let value: felt252 = Serde::deserialize(ref serialized)?; - Option::Some(value.into()) - } -} - -pub(crate) impl U64IntoUFixedPoint of Into { - fn into(self: u64) -> UFixedPoint { - UFixedPoint { - value: u512 { - limb0: 0, // fractional - limb1: self.into(), // integer - limb2: 0, - limb3: 0, - } - } - } -} - -pub(crate) impl U128IntoUFixedPoint of Into { - fn into(self: u128) -> UFixedPoint { - UFixedPoint { - value: u512 { - limb0: 0, // fractional - limb1: self, // integer - limb2: 0, - limb3: 0, - } - } - } +pub(crate) impl U256IntoUFixedPoint of Into { + fn into(self: u256) -> UFixedPoint { UFixedPoint { value: self } } } -pub(crate) impl U256IntoUFixedPoint of Into { - fn into(self: u256) -> UFixedPoint { - UFixedPoint { - value: u512 { - limb0: self.low, // fractional - limb1: self.high, // integer - limb2: 0, - limb3: 0, - } - } - } +pub(crate) impl UFixedPointIntoU256 of Into { + fn into(self: UFixedPoint) -> u256 { self.value } } pub(crate) impl Felt252IntoUFixedPoint of Into { @@ -115,13 +69,8 @@ pub(crate) impl Felt252IntoUFixedPoint of Into { #[generate_trait] pub impl UFixedPointImpl of UFixedPointTrait { - fn get_integer(self: UFixedPoint) -> u128 { - self.value.limb1 - } - - fn get_fractional(self: UFixedPoint) -> u128 { - self.value.limb0 - } + fn get_integer(self: UFixedPoint) -> u128 { self.value.high } + fn get_fractional(self: UFixedPoint) -> u128 { self.value.low } } #[generate_trait] @@ -129,100 +78,120 @@ pub impl UFixedPointShiftImpl of BitShiftImpl { fn bitshift_128_up(self: UFixedPoint) -> UFixedPoint { UFixedPoint { - value: u512 { - limb0: 0, - limb1: self.value.limb0, - limb2: self.value.limb1, - limb3: self.value.limb2, + value: u256 { + low: 0, + high: self.value.low, } } } fn bitshift_128_down(self: UFixedPoint) -> UFixedPoint { UFixedPoint { - value: u512 { - limb0: self.value.limb1, - limb1: self.value.limb2, - limb2: self.value.limb3, - limb3: 0, - } - } - } - - fn bitshift_256_down(self: UFixedPoint) -> UFixedPoint { - UFixedPoint { - value: u512 { - limb0: self.value.limb2, - limb1: self.value.limb3, - limb2: 0, - limb3: 0, + value: u256 { + low: self.value.high, + high: 0, } } } } -pub(crate) impl FixedPointIntoU256 of TryInto { - fn try_into(self: UFixedPoint) -> Option { self.value.try_into() } -} - pub(crate) impl FixedPointIntoFelt252 of TryInto { fn try_into(self: UFixedPoint) -> Option { - let medium: u256 = self.try_into().unwrap(); - medium.try_into() + self.value.try_into() } } - pub impl UFpImplAdd of Add { fn add(lhs: UFixedPoint, rhs: UFixedPoint) -> UFixedPoint { - let sum: u256 = rhs.try_into().unwrap() + lhs.try_into().unwrap(); + // TODO: overflow checking UFixedPoint { - value: u512 { - limb0: sum.low, - limb1: sum.high, - limb2: 0, - limb3: 0 - } + value: rhs.value + lhs.value } } } pub impl UFpImplSub of Sub { fn sub(lhs: UFixedPoint, rhs: UFixedPoint) -> UFixedPoint { - let sum: u256 = rhs.try_into().unwrap() - lhs.try_into().unwrap(); + // TODO: underflow checking UFixedPoint { - value: u512 { - limb0: sum.low, - limb1: sum.high, - limb2: 0, - limb3: 0 - } + value: rhs.value - lhs.value } } } -// 20100 pub impl UFpImplMul of Mul { - fn mul(lhs: UFixedPoint, rhs: UFixedPoint) -> UFixedPoint { - let left: u256 = lhs.try_into().unwrap(); - let right: u256 = rhs.try_into().unwrap(); + fn mul(lhs: UFixedPoint, rhs: UFixedPoint) -> UFixedPoint { + let res: u512 = lhs.value.wide_mul(rhs.value); - let res = left.wide_mul(right); - - UFixedPoint { value: res }.bitshift_128_down() + UFixedPoint { + // res << 128 + value: u256 { + low: res.limb1, + high: res.limb2, + } + } } } pub impl UFpImplDiv of Div { - fn div(lhs: UFixedPoint, rhs: UFixedPoint) -> UFixedPoint { - let rhs: u256 = rhs.try_into().unwrap(); - assert(rhs != 0, 'DIVISION_BY_ZERO'); + fn div(lhs: UFixedPoint, rhs: UFixedPoint) -> UFixedPoint { + let left: u512 = u512 { + limb0: 0, + limb1: 0, + limb2: lhs.value.low, + limb3: lhs.value.high, + }; + + assert(rhs.value != 0, 'DIVISION_BY_ZERO'); let (result, _) = u512_safe_div_rem_by_u256( - lhs.bitshift_128_up().value, - rhs.try_into().unwrap(), + left, + rhs.value.try_into().unwrap(), ); - UFixedPoint { value: result } + UFixedPoint { + value: u256 { + low: result.limb2, + high: result.limb3, + } + } + } +} + +pub fn div_u64_by_u128(lhs: u64, rhs: u128) -> UFixedPoint { + // lhs >> 128 + let left: u256 = u256 { + low: 0, + high: lhs.into(), + }; + + UFixedPoint { + value: left / rhs.into() + } +} + +// +// TODO: Not sure if that is needed. Tests use it. +// + +pub(crate) impl U64IntoUFixedPoint of Into { + fn into(self: u64) -> UFixedPoint { + UFixedPoint { + value: u256 { + low: 0, // fractional + high: self.into(), // integer + } + } + } +} + +pub(crate) impl U128IntoUFixedPoint of Into { + fn into(self: u128) -> UFixedPoint { + UFixedPoint { + value: u256 { + low: 0, // fractional + high: self, // integer + } + } } } \ No newline at end of file diff --git a/src/utils/fp_test.cairo b/src/utils/fp_test.cairo index fb45209..ed7342e 100644 --- a/src/utils/fp_test.cairo +++ b/src/utils/fp_test.cairo @@ -11,7 +11,7 @@ fn test_add() { let f1 : UFixedPoint = 0xFFFFFFFFFFFFFFFF_u64.into(); let f2 : UFixedPoint = 1_u64.into(); let res = f1 + f2; - let z: u256 = res.try_into().unwrap(); + let z: u256 = res.into(); assert_eq!(z.low, 0); assert_eq!(z.high, 18446744073709551616); } @@ -19,10 +19,10 @@ fn test_add() { #[test] fn test_fp_value_mapping() { let f1 : UFixedPoint = 7_u64.into(); - assert_eq!(f1.value.limb0, 0x0); - assert_eq!(f1.value.limb1, 0x7); + assert_eq!(f1.value.low, 0x0); + assert_eq!(f1.value.high, 0x7); - let val: u256 = f1.try_into().unwrap(); + let val: u256 = f1.into(); assert_eq!(val, 7_u256*0x100000000000000000000000000000000); } @@ -39,7 +39,7 @@ fn test_mul() { assert_eq!(expected.limb2, 49); assert_eq!(expected.limb3, 0); - let res: u256 = (f1 * f2).try_into().unwrap(); + let res: u256 = (f1 * f2).into(); assert_eq!(res.high, 49); assert_eq!(res.low, 0); } @@ -47,24 +47,20 @@ fn test_mul() { #[test] fn test_multiplication() { let f1 : UFixedPoint = 9223372036854775808_u128.into(); - assert_eq!(f1.value.limb0, 0); - assert_eq!(f1.value.limb1, 9223372036854775808_u128); - assert_eq!(f1.value.limb2, 0); - assert_eq!(f1.value.limb3, 0); + assert_eq!(f1.value.low, 0); + assert_eq!(f1.value.high, 9223372036854775808_u128); let res = f1 * f1; - assert_eq!(res.value.limb0, 0); - assert_eq!(res.value.limb1, 0x40000000000000000000000000000000); - assert_eq!(res.value.limb2, 0); - assert_eq!(res.value.limb3, 0); + assert_eq!(res.value.low, 0); + assert_eq!(res.value.high, 0x40000000000000000000000000000000); let expected = 9223372036854775808_u128.wide_mul(9223372036854775808_u128) * SCALE_FACTOR; assert_eq!(expected.low, 0); assert_eq!(expected.high, 0x40000000000000000000000000000000); - let result: u256 = res.try_into().unwrap(); + let result: u256 = res.into(); assert(result == expected, 'unexpected mult result'); } From 80cf041f1e20c319e6f81ecf8d79a1d3d01d8c9e Mon Sep 17 00:00:00 2001 From: baitcode Date: Tue, 7 Jan 2025 00:31:20 +0300 Subject: [PATCH 20/40] simplified more. better naming --- src/staker.cairo | 10 ++-- src/staker_log.cairo | 10 ++-- src/staker_test.cairo | 4 +- src/utils/fp.cairo | 108 ++++++++++++++++------------------------ src/utils/fp_test.cairo | 28 +++++------ 5 files changed, 69 insertions(+), 91 deletions(-) diff --git a/src/staker.cairo b/src/staker.cairo index 789766a..4f815b2 100644 --- a/src/staker.cairo +++ b/src/staker.cairo @@ -1,5 +1,5 @@ use starknet::{ContractAddress}; -use crate::utils::fp::{UFixedPoint}; +use crate::utils::fp::{UFixedPoint124x128}; #[starknet::interface] @@ -56,7 +56,7 @@ pub trait IStaker { ) -> u128; // Gets the cumulative staked amount * per second staked for the given timestamp and account. - fn get_cumulative_seconds_per_total_staked_at(self: @TContractState, timestamp: u64) -> UFixedPoint; + fn get_cumulative_seconds_per_total_staked_at(self: @TContractState, timestamp: u64) -> UFixedPoint124x128; } @@ -70,7 +70,7 @@ pub mod Staker { Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess, }; - use crate::utils::fp::{UFixedPoint, UFixedPointZero}; + use crate::utils::fp::{UFixedPoint124x128, UFixedPoint124x128Zero}; use crate::staker_log::{StakingLog}; use starknet::{ @@ -357,11 +357,11 @@ pub mod Staker { self.get_average_delegated(delegate, now - period, now) } - fn get_cumulative_seconds_per_total_staked_at(self: @ContractState, timestamp: u64) -> UFixedPoint { + fn get_cumulative_seconds_per_total_staked_at(self: @ContractState, timestamp: u64) -> UFixedPoint124x128 { if let Option::Some(log_record) = self.staking_log.find_in_change_log(timestamp) { let seconds_diff = (timestamp - log_record.timestamp) / 1000; - let staked_seconds: UFixedPoint = if log_record.total_staked == 0 { + let staked_seconds: UFixedPoint124x128 = if log_record.total_staked == 0 { 0_u64.into() } else { seconds_diff.into() / log_record.total_staked.into() diff --git a/src/staker_log.cairo b/src/staker_log.cairo index 026d0a0..8e402a4 100644 --- a/src/staker_log.cairo +++ b/src/staker_log.cairo @@ -13,7 +13,7 @@ use starknet::storage::{ StoragePointerReadAccess, StoragePointerWriteAccess }; -use crate::utils::fp::{UFixedPoint, div_u64_by_u128}; +use crate::utils::fp::{UFixedPoint124x128, div_u64_by_u128}; pub type StakingLog = Vec; @@ -21,7 +21,7 @@ pub type StakingLog = Vec; pub(crate) struct StakingLogRecord { pub(crate) timestamp: u64, pub(crate) total_staked: u128, - pub(crate) cumulative_seconds_per_total_staked: UFixedPoint, + pub(crate) cumulative_seconds_per_total_staked: UFixedPoint124x128, } #[generate_trait] @@ -92,7 +92,7 @@ pub impl StakingLogOperations of LogOperations { // Might be zero let seconds_diff = (get_block_timestamp() - last_record.timestamp) / 1000; - let staked_seconds: UFixedPoint = if last_record.total_staked == 0 { + let staked_seconds: UFixedPoint124x128 = if last_record.total_staked == 0 { 0_u64.into() } else { div_u64_by_u128(seconds_diff, last_record.total_staked) @@ -156,7 +156,7 @@ pub(crate) impl StakingLogRecordStorePacking of StorePacking, pub(crate) total_staked: StoragePointer, - pub(crate) cumulative_seconds_per_total_staked: StoragePointer, + pub(crate) cumulative_seconds_per_total_staked: StoragePointer, } pub(crate) impl StakingLogRecordSubPointersImpl of SubPointers { @@ -195,7 +195,7 @@ pub(crate) impl StakingLogRecordSubPointersImpl of SubPointers pub(crate) struct StakingLogRecordSubPointersMut { pub(crate) timestamp: StoragePointer>, pub(crate) total_staked: StoragePointer>, - pub(crate) cumulative_seconds_per_total_staked: StoragePointer>, + pub(crate) cumulative_seconds_per_total_staked: StoragePointer>, } pub(crate) impl StakingLogRecordSubPointersMutImpl of SubPointersMut { diff --git a/src/staker_test.cairo b/src/staker_test.cairo index 4614424..ead5ab2 100644 --- a/src/staker_test.cairo +++ b/src/staker_test.cairo @@ -405,7 +405,7 @@ fn test_delegate_undelegate() { mod staker_staked_seconds_per_total_staked_calculation { use starknet::{get_caller_address}; - use crate::utils::fp::{UFixedPoint, UFixedPointImpl}; + use crate::utils::fp::{UFixedPoint124x128, UFixedPoint124x128Impl}; use super::{ setup, contract_address_const, set_block_timestamp, IERC20DispatcherTrait, IStakerDispatcherTrait, @@ -466,7 +466,7 @@ mod staker_staked_seconds_per_total_staked_calculation { } - fn assert_fp(value: UFixedPoint, integer: u128, fractional: u128) { + fn assert_fp(value: UFixedPoint124x128, integer: u128, fractional: u128) { assert_eq!(value.get_integer(), integer); assert_eq!(value.get_fractional(), fractional); } diff --git a/src/utils/fp.cairo b/src/utils/fp.cairo index 841ae48..9b81cbb 100644 --- a/src/utils/fp.cairo +++ b/src/utils/fp.cairo @@ -4,24 +4,24 @@ use core::integer::{u512, u512_safe_div_rem_by_u256 }; pub const EPSILON: u256 = 0x10_u256; -// 128.128 +// 64.128 #[derive(Drop, Copy, Serde)] -pub struct UFixedPoint { +pub struct UFixedPoint124x128 { pub(crate) value: u256 } -pub impl UFixedPointStorePacking of StorePacking { - fn pack(value: UFixedPoint) -> felt252 { +pub impl UFixedPoint124x128StorePacking of StorePacking { + fn pack(value: UFixedPoint124x128) -> felt252 { value.try_into().unwrap() } - fn unpack(value: felt252) -> UFixedPoint { + fn unpack(value: felt252) -> UFixedPoint124x128 { value.into() } } -pub impl UFixedPointPartialEq of PartialEq { - fn eq(lhs: @UFixedPoint, rhs: @UFixedPoint) -> bool { +pub impl UFixedPoint124x128PartialEq of PartialEq { + fn eq(lhs: @UFixedPoint124x128, rhs: @UFixedPoint124x128) -> bool { let left: u256 = (*lhs).value; let right: u256 = (*rhs).value; @@ -35,9 +35,9 @@ pub impl UFixedPointPartialEq of PartialEq { } } -pub impl UFixedPointZero of Zero { - fn zero() -> UFixedPoint { - UFixedPoint { +pub impl UFixedPoint124x128Zero of Zero { + fn zero() -> UFixedPoint124x128 { + UFixedPoint124x128 { value: u256 { low: 0, high: 0, @@ -45,85 +45,63 @@ pub impl UFixedPointZero of Zero { } } - fn is_zero(self: @UFixedPoint) -> bool { + fn is_zero(self: @UFixedPoint124x128) -> bool { self.value.is_zero() } - fn is_non_zero(self: @UFixedPoint) -> bool { !self.is_zero() } + fn is_non_zero(self: @UFixedPoint124x128) -> bool { !self.is_zero() } } -pub(crate) impl U256IntoUFixedPoint of Into { - fn into(self: u256) -> UFixedPoint { UFixedPoint { value: self } } +pub(crate) impl U256IntoUFixedPoint of Into { + fn into(self: u256) -> UFixedPoint124x128 { UFixedPoint124x128 { value: self } } } -pub(crate) impl UFixedPointIntoU256 of Into { - fn into(self: UFixedPoint) -> u256 { self.value } +pub(crate) impl UFixedPointIntoU256 of Into { + fn into(self: UFixedPoint124x128) -> u256 { self.value } } -pub(crate) impl Felt252IntoUFixedPoint of Into { - fn into(self: felt252) -> UFixedPoint { +pub(crate) impl Felt252IntoUFixedPoint of Into { + fn into(self: felt252) -> UFixedPoint124x128 { let medium: u256 = self.into(); medium.into() } } #[generate_trait] -pub impl UFixedPointImpl of UFixedPointTrait { - fn get_integer(self: UFixedPoint) -> u128 { self.value.high } - fn get_fractional(self: UFixedPoint) -> u128 { self.value.low } +pub impl UFixedPoint124x128Impl of UFixedPointTrait { + fn get_integer(self: UFixedPoint124x128) -> u128 { self.value.high } + fn get_fractional(self: UFixedPoint124x128) -> u128 { self.value.low } } -#[generate_trait] -pub impl UFixedPointShiftImpl of BitShiftImpl { - - fn bitshift_128_up(self: UFixedPoint) -> UFixedPoint { - UFixedPoint { - value: u256 { - low: 0, - high: self.value.low, - } - } - } - - fn bitshift_128_down(self: UFixedPoint) -> UFixedPoint { - UFixedPoint { - value: u256 { - low: self.value.high, - high: 0, - } - } - } -} - -pub(crate) impl FixedPointIntoFelt252 of TryInto { - fn try_into(self: UFixedPoint) -> Option { +pub(crate) impl UFixedPoint124x128IntoFelt252 of TryInto { + fn try_into(self: UFixedPoint124x128) -> Option { self.value.try_into() } } -pub impl UFpImplAdd of Add { - fn add(lhs: UFixedPoint, rhs: UFixedPoint) -> UFixedPoint { +pub impl UFixedPoint124x128ImplAdd of Add { + fn add(lhs: UFixedPoint124x128, rhs: UFixedPoint124x128) -> UFixedPoint124x128 { // TODO: overflow checking - UFixedPoint { + UFixedPoint124x128 { value: rhs.value + lhs.value } } } -pub impl UFpImplSub of Sub { - fn sub(lhs: UFixedPoint, rhs: UFixedPoint) -> UFixedPoint { +pub impl UFixedPoint124x128ImplSub of Sub { + fn sub(lhs: UFixedPoint124x128, rhs: UFixedPoint124x128) -> UFixedPoint124x128 { // TODO: underflow checking - UFixedPoint { + UFixedPoint124x128 { value: rhs.value - lhs.value } } } -pub impl UFpImplMul of Mul { - fn mul(lhs: UFixedPoint, rhs: UFixedPoint) -> UFixedPoint { +pub impl UFixedPoint124x128ImplMul of Mul { + fn mul(lhs: UFixedPoint124x128, rhs: UFixedPoint124x128) -> UFixedPoint124x128 { let res: u512 = lhs.value.wide_mul(rhs.value); - UFixedPoint { + UFixedPoint124x128 { // res << 128 value: u256 { low: res.limb1, @@ -133,8 +111,8 @@ pub impl UFpImplMul of Mul { } } -pub impl UFpImplDiv of Div { - fn div(lhs: UFixedPoint, rhs: UFixedPoint) -> UFixedPoint { +pub impl UFixedPoint124x128ImplDiv of Div { + fn div(lhs: UFixedPoint124x128, rhs: UFixedPoint124x128) -> UFixedPoint124x128 { let left: u512 = u512 { limb0: 0, limb1: 0, @@ -149,7 +127,7 @@ pub impl UFpImplDiv of Div { rhs.value.try_into().unwrap(), ); - UFixedPoint { + UFixedPoint124x128 { value: u256 { low: result.limb2, high: result.limb3, @@ -158,14 +136,14 @@ pub impl UFpImplDiv of Div { } } -pub fn div_u64_by_u128(lhs: u64, rhs: u128) -> UFixedPoint { +pub fn div_u64_by_u128(lhs: u64, rhs: u128) -> UFixedPoint124x128 { // lhs >> 128 let left: u256 = u256 { low: 0, high: lhs.into(), }; - UFixedPoint { + UFixedPoint124x128 { value: left / rhs.into() } } @@ -174,9 +152,9 @@ pub fn div_u64_by_u128(lhs: u64, rhs: u128) -> UFixedPoint { // TODO: Not sure if that is needed. Tests use it. // -pub(crate) impl U64IntoUFixedPoint of Into { - fn into(self: u64) -> UFixedPoint { - UFixedPoint { +pub(crate) impl U64IntoUFixedPoint of Into { + fn into(self: u64) -> UFixedPoint124x128 { + UFixedPoint124x128 { value: u256 { low: 0, // fractional high: self.into(), // integer @@ -185,9 +163,9 @@ pub(crate) impl U64IntoUFixedPoint of Into { } } -pub(crate) impl U128IntoUFixedPoint of Into { - fn into(self: u128) -> UFixedPoint { - UFixedPoint { +pub(crate) impl U128IntoUFixedPoint of Into { + fn into(self: u128) -> UFixedPoint124x128 { + UFixedPoint124x128 { value: u256 { low: 0, // fractional high: self, // integer diff --git a/src/utils/fp_test.cairo b/src/utils/fp_test.cairo index ed7342e..c1146f5 100644 --- a/src/utils/fp_test.cairo +++ b/src/utils/fp_test.cairo @@ -2,14 +2,14 @@ use core::num::traits::WideMul; use super::fp::BitShiftImpl; use super::fp::UFixedPointTrait; -use crate::utils::fp::{UFixedPoint, UFixedPointShiftImpl}; +use crate::utils::fp::{UFixedPoint124x128}; const SCALE_FACTOR: u256 = 0x100000000000000000000000000000000; #[test] fn test_add() { - let f1 : UFixedPoint = 0xFFFFFFFFFFFFFFFF_u64.into(); - let f2 : UFixedPoint = 1_u64.into(); + let f1 : UFixedPoint124x128 = 0xFFFFFFFFFFFFFFFF_u64.into(); + let f2 : UFixedPoint124x128 = 1_u64.into(); let res = f1 + f2; let z: u256 = res.into(); assert_eq!(z.low, 0); @@ -18,7 +18,7 @@ fn test_add() { #[test] fn test_fp_value_mapping() { - let f1 : UFixedPoint = 7_u64.into(); + let f1 : UFixedPoint124x128 = 7_u64.into(); assert_eq!(f1.value.low, 0x0); assert_eq!(f1.value.high, 0x7); @@ -29,8 +29,8 @@ fn test_fp_value_mapping() { #[test] fn test_mul() { - let f1 : UFixedPoint = 7_u64.into(); - let f2 : UFixedPoint = 7_u64.into(); + let f1 : UFixedPoint124x128 = 7_u64.into(); + let f2 : UFixedPoint124x128 = 7_u64.into(); let expected = (7_u256*SCALE_FACTOR).wide_mul(7_u256*SCALE_FACTOR); @@ -46,7 +46,7 @@ fn test_mul() { #[test] fn test_multiplication() { - let f1 : UFixedPoint = 9223372036854775808_u128.into(); + let f1 : UFixedPoint124x128 = 9223372036854775808_u128.into(); assert_eq!(f1.value.low, 0); assert_eq!(f1.value.high, 9223372036854775808_u128); @@ -72,7 +72,7 @@ fn test_u256_conversion() { assert_eq!(f.high, 0x0123456789ABCDEFFEDCBA9876543210); // BITSHIFT DOWN - let fp: UFixedPoint = f.into(); + let fp: UFixedPoint124x128 = f.into(); assert_eq!(fp.get_integer(), f.high); assert_eq!(fp.get_fractional(), f.low); @@ -85,7 +85,7 @@ fn test_u256_conversion() { assert_eq!(fp.get_fractional(), 0); // BITSHIFT UP - let fp: UFixedPoint = f.into(); + let fp: UFixedPoint124x128 = f.into(); assert_eq!(fp.get_integer(), f.high); assert_eq!(fp.get_fractional(), f.low); @@ -99,17 +99,17 @@ fn test_u256_conversion() { } fn run_division_test(left: u128, right: u128, expected_int: u128, expected_frac: u128) { - let f1 : UFixedPoint = left.into(); - let f2 : UFixedPoint = right.into(); + let f1 : UFixedPoint124x128 = left.into(); + let f2 : UFixedPoint124x128 = right.into(); let res = f1 / f2; assert_eq!(res.get_integer(), expected_int); assert_eq!(res.get_fractional(), expected_frac); } fn run_division_and_multiplication_test(numenator: u128, divisor: u128, mult: u128, expected_int: u128, expected_frac: u128) { - let f1 : UFixedPoint = numenator.into(); - let f2 : UFixedPoint = divisor.into(); - let f3 : UFixedPoint = mult.into(); + let f1 : UFixedPoint124x128 = numenator.into(); + let f2 : UFixedPoint124x128 = divisor.into(); + let f3 : UFixedPoint124x128 = mult.into(); let res = f1 / f2 * f3; assert_eq!(res.get_integer(), expected_int); assert_eq!(res.get_fractional(), expected_frac); From f507631c4eb7006f9f7b7f9034293978b87957d0 Mon Sep 17 00:00:00 2001 From: baitcode Date: Tue, 7 Jan 2025 00:33:42 +0300 Subject: [PATCH 21/40] clean up bitshifts from tests --- src/utils/fp_test.cairo | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/src/utils/fp_test.cairo b/src/utils/fp_test.cairo index c1146f5..e84fbd0 100644 --- a/src/utils/fp_test.cairo +++ b/src/utils/fp_test.cairo @@ -1,5 +1,4 @@ use core::num::traits::WideMul; -use super::fp::BitShiftImpl; use super::fp::UFixedPointTrait; use crate::utils::fp::{UFixedPoint124x128}; @@ -71,31 +70,9 @@ fn test_u256_conversion() { assert_eq!(f.low, 0x00112233445566778899AABBCCDDEEFF); assert_eq!(f.high, 0x0123456789ABCDEFFEDCBA9876543210); - // BITSHIFT DOWN let fp: UFixedPoint124x128 = f.into(); assert_eq!(fp.get_integer(), f.high); assert_eq!(fp.get_fractional(), f.low); - - let fp = fp.bitshift_128_down(); - assert_eq!(fp.get_integer(), 0); - assert_eq!(fp.get_fractional(), f.high); - - let fp = fp.bitshift_128_down(); - assert_eq!(fp.get_integer(), 0); - assert_eq!(fp.get_fractional(), 0); - - // BITSHIFT UP - let fp: UFixedPoint124x128 = f.into(); - assert_eq!(fp.get_integer(), f.high); - assert_eq!(fp.get_fractional(), f.low); - - let fp = fp.bitshift_128_up(); - assert_eq!(fp.get_integer(), f.low); - assert_eq!(fp.get_fractional(), 0); - - let fp = fp.bitshift_128_up(); - assert_eq!(fp.get_integer(), 0); - assert_eq!(fp.get_fractional(), 0); } fn run_division_test(left: u128, right: u128, expected_int: u128, expected_frac: u128) { From 164f64fea47642b14f6018a767aefd7533431d93 Mon Sep 17 00:00:00 2001 From: baitcode Date: Tue, 7 Jan 2025 00:49:04 +0300 Subject: [PATCH 22/40] overflow checks --- src/utils/fp.cairo | 78 +++++++++++++++++++++++++++++++++------------- 1 file changed, 56 insertions(+), 22 deletions(-) diff --git a/src/utils/fp.cairo b/src/utils/fp.cairo index 9b81cbb..3787346 100644 --- a/src/utils/fp.cairo +++ b/src/utils/fp.cairo @@ -4,12 +4,20 @@ use core::integer::{u512, u512_safe_div_rem_by_u256 }; pub const EPSILON: u256 = 0x10_u256; -// 64.128 +// 2^124 +pub const MAX_INT: u128 = 0x10000000000000000000000000000000_u128; + +// 124.128 (= 252 which 1 felt exactly) #[derive(Drop, Copy, Serde)] pub struct UFixedPoint124x128 { pub(crate) value: u256 } +pub mod Errors { + pub const INTEGER_OVERFLOW: felt252 = 'INTEGER_OVERFLOW'; + pub const DIVISION_BY_ZERO: felt252 = 'DIVISION_BY_ZERO'; +} + pub impl UFixedPoint124x128StorePacking of StorePacking { fn pack(value: UFixedPoint124x128) -> felt252 { value.try_into().unwrap() @@ -71,6 +79,10 @@ pub(crate) impl Felt252IntoUFixedPoint of Into { pub impl UFixedPoint124x128Impl of UFixedPointTrait { fn get_integer(self: UFixedPoint124x128) -> u128 { self.value.high } fn get_fractional(self: UFixedPoint124x128) -> u128 { self.value.low } + + fn validate(self: UFixedPoint124x128) { + assert(self.value.high < MAX_INT, Errors::INTEGER_OVERFLOW); + } } pub(crate) impl UFixedPoint124x128IntoFelt252 of TryInto { @@ -81,33 +93,41 @@ pub(crate) impl UFixedPoint124x128IntoFelt252 of TryInto { fn add(lhs: UFixedPoint124x128, rhs: UFixedPoint124x128) -> UFixedPoint124x128 { - // TODO: overflow checking - UFixedPoint124x128 { + let res = UFixedPoint124x128 { value: rhs.value + lhs.value - } + }; + res.validate(); + + res } } pub impl UFixedPoint124x128ImplSub of Sub { fn sub(lhs: UFixedPoint124x128, rhs: UFixedPoint124x128) -> UFixedPoint124x128 { // TODO: underflow checking - UFixedPoint124x128 { + let res = UFixedPoint124x128 { value: rhs.value - lhs.value - } + }; + res.validate(); + + res } } pub impl UFixedPoint124x128ImplMul of Mul { fn mul(lhs: UFixedPoint124x128, rhs: UFixedPoint124x128) -> UFixedPoint124x128 { - let res: u512 = lhs.value.wide_mul(rhs.value); - - UFixedPoint124x128 { + let mult_res: u512 = lhs.value.wide_mul(rhs.value); + + let res = UFixedPoint124x128 { // res << 128 value: u256 { - low: res.limb1, - high: res.limb2, + low: mult_res.limb1, + high: mult_res.limb2, } - } + }; + res.validate(); + + res } } @@ -120,32 +140,42 @@ pub impl UFixedPoint124x128ImplDiv of Div { limb3: lhs.value.high, }; - assert(rhs.value != 0, 'DIVISION_BY_ZERO'); + assert(rhs.value != 0, Errors::DIVISION_BY_ZERO); - let (result, _) = u512_safe_div_rem_by_u256( + let (div_res, _) = u512_safe_div_rem_by_u256( left, rhs.value.try_into().unwrap(), ); - UFixedPoint124x128 { + let res = UFixedPoint124x128 { value: u256 { - low: result.limb2, - high: result.limb3, + low: div_res.limb2, + high: div_res.limb3, } - } + }; + + res.validate(); + + res } } pub fn div_u64_by_u128(lhs: u64, rhs: u128) -> UFixedPoint124x128 { + assert(!rhs.is_zero(), Errors::DIVISION_BY_ZERO); + // lhs >> 128 let left: u256 = u256 { low: 0, high: lhs.into(), }; - UFixedPoint124x128 { + let res = UFixedPoint124x128 { value: left / rhs.into() - } + }; + + res.validate(); + + res } // @@ -165,11 +195,15 @@ pub(crate) impl U64IntoUFixedPoint of Into { pub(crate) impl U128IntoUFixedPoint of Into { fn into(self: u128) -> UFixedPoint124x128 { - UFixedPoint124x128 { + let res = UFixedPoint124x128 { value: u256 { low: 0, // fractional high: self, // integer } - } + }; + + res.validate(); + + res } } \ No newline at end of file From 6e931c6c8f5756381f0a0182ac7e3897722b945e Mon Sep 17 00:00:00 2001 From: baitcode Date: Tue, 7 Jan 2025 18:27:59 +0300 Subject: [PATCH 23/40] old tests did not respect overflow. had to create new ones. finished subpointers for staking_log. Subpointer always read whole felt252 from memory and converts it. So I had to change approach for packing. --- .gitignore | 2 +- src/lib.cairo | 1 + src/staker.cairo | 3 +- src/staker_log.cairo | 120 +++++++++++--------------- src/staker_test.cairo | 2 +- src/utils/fp.cairo | 10 +-- src/utils/fp_test.cairo | 183 +++++++++++++++++++++------------------- 7 files changed, 154 insertions(+), 167 deletions(-) diff --git a/.gitignore b/.gitignore index aee9b44..934c234 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ .env .vscode .starkli - +.DS_Store ####### Scarb ####### diff --git a/src/lib.cairo b/src/lib.cairo index e2e93a7..fad73df 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -17,6 +17,7 @@ mod governor_test; pub mod staker; pub mod staker_log; + #[cfg(test)] mod staker_test; diff --git a/src/staker.cairo b/src/staker.cairo index 4f815b2..961e342 100644 --- a/src/staker.cairo +++ b/src/staker.cairo @@ -256,7 +256,6 @@ pub mod Staker { } fn stake_amount(ref self: ContractState, delegate: ContractAddress, amount: u128) { - assert(amount != 0, 'PFFFFF'); let from = get_caller_address(); let token = self.token.read(); @@ -341,7 +340,7 @@ pub mod Staker { fn get_average_delegated( self: @ContractState, delegate: ContractAddress, start: u64, end: u64, ) -> u128 { - assert(end > start, '6'); + assert(end > start, 'ORDER'); assert(end <= get_block_timestamp(), 'FUTURE'); let start_snapshot = self.get_delegated_cumulative(delegate, start); diff --git a/src/staker_log.cairo b/src/staker_log.cairo index 8e402a4..f6c8350 100644 --- a/src/staker_log.cairo +++ b/src/staker_log.cairo @@ -9,7 +9,7 @@ use starknet::storage::{ StoragePointer, StorageBase, Mutable, StoragePath, StorageAsPath, - SubPointers, SubPointersMut, + SubPointers, StoragePointerReadAccess, StoragePointerWriteAccess }; @@ -43,7 +43,8 @@ pub impl StakingLogOperations of LogOperations { let center = (right + left) / 2; let record = log.at(center); - if record.timestamp.read() <= timestamp { + let record_part = record.packed_timestamp_and_total_staked.read(); + if record_part.timestamp <= timestamp { result_ptr = Option::Some(record); left = center + 1; } else { @@ -128,106 +129,87 @@ pub impl StakingLogOperations of LogOperations { pub(crate) impl StakingLogRecordStorePacking of StorePacking { fn pack(value: StakingLogRecord) -> (felt252, felt252) { - let first: felt252 = u256 { - high: value.timestamp.into(), - low: value.total_staked, - }.try_into().unwrap(); + let packed_ts_total_staked: felt252 = PackedValuesStorePacking::pack((value.timestamp, value.total_staked).into()); - let second: felt252 = value.cumulative_seconds_per_total_staked + let cumulative_seconds_per_total_staked: felt252 = value.cumulative_seconds_per_total_staked .try_into() .unwrap(); - (first, second) + (packed_ts_total_staked, cumulative_seconds_per_total_staked) } fn unpack(value: (felt252, felt252)) -> StakingLogRecord { let (packed_ts_total_staked, cumulative_seconds_per_total_staked) = value; - let medium: u256 = packed_ts_total_staked.into(); + let record = PackedValuesStorePacking::unpack(packed_ts_total_staked); StakingLogRecord { - timestamp: medium.high.try_into().unwrap(), - total_staked: medium.low, + timestamp: record.timestamp, + total_staked: record.total_staked, cumulative_seconds_per_total_staked: cumulative_seconds_per_total_staked.try_into().unwrap(), } } } - -#[derive(Drop, Copy)] -pub(crate) struct StakingLogRecordSubPointers { - pub(crate) timestamp: StoragePointer, - pub(crate) total_staked: StoragePointer, - pub(crate) cumulative_seconds_per_total_staked: StoragePointer, +#[derive(Drop, Serde)] +pub(crate) struct PackedPart { + timestamp: u64, + total_staked: u128, } -pub(crate) impl StakingLogRecordSubPointersImpl of SubPointers { - - type SubPointersType = StakingLogRecordSubPointers; - - fn sub_pointers(self: StoragePointer) -> StakingLogRecordSubPointers { - let base_address = self.__storage_pointer_address__; - - let mut current_offset = self.__storage_pointer_offset__; - let __packed_low_128__ = StoragePointer { - __storage_pointer_address__: base_address, - __storage_pointer_offset__: current_offset, - }; - - let __packed_high_124__ = StoragePointer { - __storage_pointer_address__: base_address, - __storage_pointer_offset__: current_offset + Store::::size(), - }; +pub(crate) impl TupleToPackedPart of Into<(u64, u128), PackedPart> { + fn into(self: (u64, u128)) -> PackedPart { + let (timestamp, total_staked) = self; + PackedPart { timestamp: timestamp, total_staked: total_staked } + } +} - current_offset = current_offset + Store::::size(); - let __packed_felt2__ = StoragePointer { - __storage_pointer_address__: base_address, - __storage_pointer_offset__: current_offset, - }; +pub impl PackedValuesStorePacking of StorePacking { + fn pack(value: PackedPart) -> felt252 { + u256 { + high: value.timestamp.into(), + low: value.total_staked, + }.try_into().unwrap() + } - StakingLogRecordSubPointers { - timestamp: __packed_high_124__, - total_staked: __packed_low_128__, - cumulative_seconds_per_total_staked: __packed_felt2__, + fn unpack(value: felt252) -> PackedPart { + let packed_ts_total_staked_u256: u256 = value.into(); + PackedPart { + timestamp: packed_ts_total_staked_u256.high.try_into().unwrap(), + total_staked: packed_ts_total_staked_u256.low } } } + +// +// Record subpoiters allow to minimase memory read operarions. Thus while doing binary search we ca read only 1 felt252 from memory. +// + #[derive(Drop, Copy)] -pub(crate) struct StakingLogRecordSubPointersMut { - pub(crate) timestamp: StoragePointer>, - pub(crate) total_staked: StoragePointer>, - pub(crate) cumulative_seconds_per_total_staked: StoragePointer>, +pub(crate) struct StakingLogRecordSubPointers { + pub(crate) packed_timestamp_and_total_staked: StoragePointer, + pub(crate) cumulative_seconds_per_total_staked: StoragePointer, } -pub(crate) impl StakingLogRecordSubPointersMutImpl of SubPointersMut { - - type SubPointersType = StakingLogRecordSubPointersMut; +pub(crate) impl StakingLogRecordSubPointersImpl of SubPointers { - fn sub_pointers_mut( - self: StoragePointer>, - ) -> StakingLogRecordSubPointersMut { + type SubPointersType = StakingLogRecordSubPointers; + + fn sub_pointers(self: StoragePointer) -> StakingLogRecordSubPointers { let base_address = self.__storage_pointer_address__; - let mut current_offset = self.__storage_pointer_offset__; - let __packed_low_128__ = StoragePointer { + let total_staked_ptr = StoragePointer { __storage_pointer_address__: base_address, - __storage_pointer_offset__: current_offset, + __storage_pointer_offset__: self.__storage_pointer_offset__, }; - let __packed_high_124__ = StoragePointer { + let cumulative_seconds_per_total_staked_ptr = StoragePointer { __storage_pointer_address__: base_address, - __storage_pointer_offset__: current_offset + Store::::size(), + __storage_pointer_offset__: self.__storage_pointer_offset__ + Store::::size(), }; - current_offset = current_offset + Store::::size(); - let __packed_felt2__ = StoragePointer { - __storage_pointer_address__: base_address, - __storage_pointer_offset__: current_offset, - }; - - StakingLogRecordSubPointersMut { - timestamp: __packed_high_124__, - total_staked: __packed_low_128__, - cumulative_seconds_per_total_staked: __packed_felt2__, + StakingLogRecordSubPointers { + packed_timestamp_and_total_staked: total_staked_ptr, + cumulative_seconds_per_total_staked: cumulative_seconds_per_total_staked_ptr, } } -} +} \ No newline at end of file diff --git a/src/staker_test.cairo b/src/staker_test.cairo index ead5ab2..c2a241d 100644 --- a/src/staker_test.cairo +++ b/src/staker_test.cairo @@ -515,7 +515,7 @@ mod staker_staked_seconds_per_total_staked_calculation { assert_fp(staker.get_cumulative_seconds_per_total_staked_at(8000), 2, 0x80000000000000000000000000000000_u128); assert_fp(staker.get_cumulative_seconds_per_total_staked_at(9000), 2, 0x80000000000000000000000000000000_u128); assert_fp(staker.get_cumulative_seconds_per_total_staked_at(10000), 2, 0x80000000000000000000000000000000_u128); - // 7 were staked here + // // 7 were staked here assert_fp(staker.get_cumulative_seconds_per_total_staked_at(17000), 3, 0x80000000000000000000000000000000_u128); assert_fp(staker.get_cumulative_seconds_per_total_staked_at(24000), 4, 0x80000000000000000000000000000000_u128); } diff --git a/src/utils/fp.cairo b/src/utils/fp.cairo index 3787346..80ab4c9 100644 --- a/src/utils/fp.cairo +++ b/src/utils/fp.cairo @@ -8,9 +8,9 @@ pub const EPSILON: u256 = 0x10_u256; pub const MAX_INT: u128 = 0x10000000000000000000000000000000_u128; // 124.128 (= 252 which 1 felt exactly) -#[derive(Drop, Copy, Serde)] +#[derive(Debug, Drop, Copy, Serde)] pub struct UFixedPoint124x128 { - pub(crate) value: u256 + value: u256 } pub mod Errors { @@ -149,11 +149,11 @@ pub impl UFixedPoint124x128ImplDiv of Div { let res = UFixedPoint124x128 { value: u256 { - low: div_res.limb2, - high: div_res.limb3, + low: div_res.limb1, + high: div_res.limb2, } }; - + res.validate(); res diff --git a/src/utils/fp_test.cairo b/src/utils/fp_test.cairo index e84fbd0..58b61b1 100644 --- a/src/utils/fp_test.cairo +++ b/src/utils/fp_test.cairo @@ -1,7 +1,7 @@ use core::num::traits::WideMul; use super::fp::UFixedPointTrait; -use crate::utils::fp::{UFixedPoint124x128}; +use crate::utils::fp::{UFixedPoint124x128, div_u64_by_u128}; const SCALE_FACTOR: u256 = 0x100000000000000000000000000000000; @@ -18,8 +18,8 @@ fn test_add() { #[test] fn test_fp_value_mapping() { let f1 : UFixedPoint124x128 = 7_u64.into(); - assert_eq!(f1.value.low, 0x0); - assert_eq!(f1.value.high, 0x7); + assert_eq!(f1.get_fractional(), 0x0); + assert_eq!(f1.get_integer(), 0x7); let val: u256 = f1.into(); assert_eq!(val, 7_u256*0x100000000000000000000000000000000); @@ -44,15 +44,16 @@ fn test_mul() { } #[test] -fn test_multiplication() { +#[should_panic(expected: 'INTEGER_OVERFLOW')] +fn test_multiplication_overflow() { let f1 : UFixedPoint124x128 = 9223372036854775808_u128.into(); - assert_eq!(f1.value.low, 0); - assert_eq!(f1.value.high, 9223372036854775808_u128); + assert_eq!(f1.get_fractional(), 0); + assert_eq!(f1.get_integer(), 9223372036854775808_u128); let res = f1 * f1; - assert_eq!(res.value.low, 0); - assert_eq!(res.value.high, 0x40000000000000000000000000000000); + assert_eq!(res.get_fractional(), 0); + assert_eq!(res.get_integer(), 0x40000000000000000000000000000000); let expected = 9223372036854775808_u128.wide_mul(9223372036854775808_u128) * SCALE_FACTOR; @@ -63,6 +64,25 @@ fn test_multiplication() { assert(result == expected, 'unexpected mult result'); } +#[test] +fn test_multiplication() { + let f1 : UFixedPoint124x128 = 1152921504606846976_u128.into(); + let f2 : UFixedPoint124x128 = 9223372036854775808_u128.into(); + + let res = f1 * f2; + + assert_eq!(res.get_fractional(), 0); + assert_eq!(res.get_integer(), 0x8000000000000000000000000000000); + + let expected = 1152921504606846976_u128.wide_mul(9223372036854775808_u128) * SCALE_FACTOR; + + assert_eq!(expected.low, 0); + assert_eq!(expected.high, 0x8000000000000000000000000000000); + + let result: u256 = res.into(); + assert(result == expected, 'unexpected mult result'); +} + #[test] fn test_u256_conversion() { let f: u256 = 0x0123456789ABCDEFFEDCBA987654321000112233445566778899AABBCCDDEEFF_u256; @@ -75,10 +95,19 @@ fn test_u256_conversion() { assert_eq!(fp.get_fractional(), f.low); } -fn run_division_test(left: u128, right: u128, expected_int: u128, expected_frac: u128) { +fn run_division_test(left: u64, right: u128, expected_int: u128, expected_frac: u128) { + let res = div_u64_by_u128(left, right); + + assert_eq!(res.get_integer(), expected_int); + assert_eq!(res.get_fractional(), expected_frac); +} + +fn run_division_test_using_class(left: u64, right: u128, expected_int: u128, expected_frac: u128) { let f1 : UFixedPoint124x128 = left.into(); let f2 : UFixedPoint124x128 = right.into(); + let res = f1 / f2; + assert_eq!(res.get_integer(), expected_int); assert_eq!(res.get_fractional(), expected_frac); } @@ -87,6 +116,7 @@ fn run_division_and_multiplication_test(numenator: u128, divisor: u128, mult: u1 let f1 : UFixedPoint124x128 = numenator.into(); let f2 : UFixedPoint124x128 = divisor.into(); let f3 : UFixedPoint124x128 = mult.into(); + let res = f1 / f2 * f3; assert_eq!(res.get_integer(), expected_int); assert_eq!(res.get_fractional(), expected_frac); @@ -98,91 +128,66 @@ fn test_division() { run_division_test(1, 1000, 0, 0x4189374bc6a7ef9db22d0e56041893); run_division_test(56, 7, 8, 0); run_division_test(0, 7, 0, 0); + + run_division_test(0x6c9444e9af6eb21, 0xd0ba0d5da1c09d9e57d94820ec138cce, 0x0, 0x852bac969a2350b); + run_division_test(0xdcbfffffa6958e23, 0x4918d4829fcdf183d6d99f0570f1745, 0x0, 0x3051c1a9f372fbfd4a); + run_division_test(0xd426b156df76a5a0, 0x6a8f314198ecb47d43cd4aa0a9ca43d9, 0x0, 0x1fdacf0b6d911b54a); + run_division_test(0xab4741c332625aba, 0xb930f984727d03cbefad17a2e094607d, 0x0, 0xecc471b02bb827d2); + run_division_test(0xf456e2beec7fafa6, 0xc206153d5f83abbc774cf8bea8f0a27e, 0x0, 0x14263442f5d80ef3d); + run_division_test(0x85bc433c34460024, 0x8a748e982e70d29d3b59fd4736113ceb, 0x0, 0xf745e5b672b00d3a); + run_division_test(0xb1db6e9041be65b2, 0x9c7a91635d61b2bccf88160a2fd53dee, 0x0, 0x122f9a1641f14e42b); + run_division_test(0x2180c4fcbc0f47a3, 0xda8f12b54ad0f583da1417295e12b48a, 0x0, 0x273e0c487a908538); + run_division_test(0x966c069564807e34, 0xbd2087c1a063e4c9514889201f4293db, 0x0, 0xcb9bf9617ac98b00); + run_division_test(0xb117842a13d72f4, 0x8ba79e67edcfbd2bec7379332208587b, 0x0, 0x144a02a03bcb8771); + run_division_test(0x1f48f3feddc9b742, 0x2b409d77edb124901b6cca8b496cd3e0, 0x0, 0xb92af636e340d0d6); + + run_division_test(0x51cb6348d4f073eb, 0xae797e7d, 0x7803938f, 0xc7836eec158af9487775eb8656cf38fc); + run_division_test(0x62337e9d72ddfb51, 0x59214f47, 0x11a0dcac3, 0x1acf56e174d88003af63f46791468df3); + run_division_test(0xd5ecb2a682ba1fee, 0x865f3300, 0x1978f8bd2, 0x3354716b276ed7e6c759e14d58ab5767); + run_division_test(0xda53f5e167d39325, 0x491317ba, 0x2fcdca379, 0xcaab30305da3be6620c494367695761e); + run_division_test(0x22afcdd641467cd1, 0xdedbc9d, 0x27d85372e, 0x76e1c4fe6939db4dcd8b67f2def5c102); + run_division_test(0x37bc576886b36435, 0x9563dc74, 0x5f82b8e1, 0xc7c4701f73ebf57b4c59d2f357775a0d); + run_division_test(0x84bd569f3d7ce768, 0x555d2820, 0x18e1384ee, 0x596b1a4ce6995f4a1229e9f185ec0672); + run_division_test(0x46106cf2302ff8fa, 0xf6d765c9, 0x48a9ec96, 0x44eb3c9b3e7337e3750761dca19c8098); + run_division_test(0xe4311b247d0fccd4, 0x751c9792, 0x1f2d0b9ef, 0xb5e4969913d37a40ba6cc33b74a5ea6d); + run_division_test(0x7bc92347ffca33de, 0x73038c20, 0x113864700, 0xa7549c75ef40e011c8ab37b7f05a706d); + + run_division_test_using_class(0x51cb6348d4f073eb, 0xae797e7d, 0x7803938f, 0xc7836eec158af9487775eb8656cf38fc); + run_division_test_using_class(0x62337e9d72ddfb51, 0x59214f47, 0x11a0dcac3, 0x1acf56e174d88003af63f46791468df3); + run_division_test_using_class(0xd5ecb2a682ba1fee, 0x865f3300, 0x1978f8bd2, 0x3354716b276ed7e6c759e14d58ab5767); + run_division_test_using_class(0xda53f5e167d39325, 0x491317ba, 0x2fcdca379, 0xcaab30305da3be6620c494367695761e); + run_division_test_using_class(0x22afcdd641467cd1, 0xdedbc9d, 0x27d85372e, 0x76e1c4fe6939db4dcd8b67f2def5c102); + run_division_test_using_class(0x37bc576886b36435, 0x9563dc74, 0x5f82b8e1, 0xc7c4701f73ebf57b4c59d2f357775a0d); + run_division_test_using_class(0x84bd569f3d7ce768, 0x555d2820, 0x18e1384ee, 0x596b1a4ce6995f4a1229e9f185ec0672); + run_division_test_using_class(0x46106cf2302ff8fa, 0xf6d765c9, 0x48a9ec96, 0x44eb3c9b3e7337e3750761dca19c8098); + run_division_test_using_class(0xe4311b247d0fccd4, 0x751c9792, 0x1f2d0b9ef, 0xb5e4969913d37a40ba6cc33b74a5ea6d); + run_division_test_using_class(0x7bc92347ffca33de, 0x73038c20, 0x113864700, 0xa7549c75ef40e011c8ab37b7f05a706d); + - run_division_test(0x373a1db0ac54ce4e580815828152f, 0x68ee6e5a163cd022760d1bb6eb4a0f7d, 0x0, 0x86bc9e2a429fb89528cb71f24afbb); - run_division_test(0x6420b39c9a627ed6b97f50bed3887, 0xdfc9859d615e2a1423bb70f42426ceeb, 0x0, 0x728a5f7ea534a5858bd21791663d2); - run_division_test(0x54948d04aab7daa0ac42f255df182, 0x5d97ed36740f5e528f154b565d039df6, 0x0, 0xe758c9b672eb559b2a09feb6e5082); - run_division_test(0x3603c70c39769c861d6a9e1c4ab93, 0x1cd625b5488f3b7443f7baf785db9d23, 0x0, 0x1df85f1cabacb5b7af7dbfb4f3eb29); - run_division_test(0x3454ad56a8a03af36c0c682f80ff4, 0x221ff40a3c0917b64cdc72b7c2201553, 0x0, 0x1889426dabe2220f0bdf5e6d91aa22); - run_division_test(0xc5151b7adb4f20fec8affdacdbd0, 0x9b9f9c2893ba96b28460bd6651ef35b4, 0x0, 0x144332932b6b6b845eb0619b6460d); - run_division_test(0x30d4f65bb98392fac119cb0d1a001, 0xe816835b58266378ef4ff98551febaa6, 0x0, 0x35dcf041fc7c566cee3c08524ac3c); - run_division_test(0x5fdcece9a04e6e2ec3b9b97daae29, 0xafc02e6f0de1d5f1a03f856e3ff0ac50, 0x0, 0x8ba28622026c1e149c76ce780d48c); - run_division_test(0x2dbd5a118251261474e78c00d6218, 0xd648b9b997123ec4ad9c28efe41429c0, 0x0, 0x36a4e0f5ccd2c7245ccc92eaa3032); - run_division_test(0x5dd565d8cff2c3910670e7281437, 0xc98fe84476bcde26354201c914b67bc2, 0x0, 0x772d1799f8bce31f7938b243b1a3); - run_division_test(0x395e07b9b8b0101ace9e5d8e81b3, 0x32ab696d44691512dcb0f703652cbc50, 0x0, 0x121d6d65094a60a4e0cacde47959a); - run_division_test(0x23d7fae99ca75b5b3895532d3af7e, 0xfdd9cc91f526a4604e5155f44cca3932, 0x0, 0x2425ab1a41dec9f5cec9309c9f215); - run_division_test(0x17a95c6d826ae28da48bf9c7e1d37, 0xdf1d2b4ffad0429069d1149849420ca, 0x0, 0x1b2630cc7021c0eed8b50b883f27c1); - run_division_test(0x2f4eccfae439a7537c80d4ea48abb, 0x7c783f2d20675729ffa2370389b6041a, 0x0, 0x614c96d92643f1bba9d426a45ca79); - run_division_test(0x50ef73f24f0359bf40f9af1c54503, 0x469d30f299b08c1fc606db348bec82e6, 0x0, 0x1256b1a2feed6ed8a72d08d53fc7e5); - run_division_test(0x1f759ee0b81dcc772682f40ebffb0, 0x167ec3ed79e55af7bd2d80b89c1a2308, 0x0, 0x16603f3888ecab6083fd8f64cd3fea); - run_division_test(0x1942fb52cd7b845ec1f0baf6c4eb1, 0xa32bfb866092db90fe4b43e847d0074f, 0x0, 0x27a209af1d8918a287093736dc0f1); - run_division_test(0x40e38886d28ca946f16aa26424243, 0x3296ac028d8f1ab8b78a0c05299e8c49, 0x0, 0x1485d8aab412f548495df395399e8b); - run_division_test(0x2941a1e28f5541a80859ce2f2fc4f, 0x4c744a392588e6801597ddf8611bbf82, 0x0, 0x8a24a5d0d212e897ddc97671b457d); - run_division_test(0x12ba728d7d93924d73e5292b37627, 0x2432897ea1854505bdae6d810ee658db, 0x0, 0x8473e91137f0d7c56a7fc231ab83e); - run_division_test(0xd3b366a58fe3821a0299458a948e697c, 0xd36969b2718202cecefc06c537e6201f, 0x1, 0x5997bab757f3870d31faee27348ea1); - run_division_test(0x1c5531122412a124204881c422341974, 0x16bdecf1affca09bf42a85af2791b978, 0x1, 0x3eef6a228c9ed60eae1635602d1dd8fb); - run_division_test(0x7662d55984edd61d7e4d6da1c8f7f609, 0xf7c7cfbb07a727f624ed66ea1b822860, 0x0, 0x7a502f6f5d0c2dfaf29130900236627a); - run_division_test(0x394f5c6063a9bf5e78f3e342cccc96e, 0x4b53133fe701640e79961ed2a272ef12, 0x0, 0xc2c67f91abe37777ac2c2be19bb40bc); - run_division_test(0x50d5c44bd44ff124dd8a11ed7f13a12c, 0x725eb08a055bbdc1fe4dcacbb0f9e91a, 0x0, 0xb4efecbd889dee1b3445025fe257e7c0); - run_division_test(0x73c461991f9afba0594ac191776804a, 0x2520a3758144f7e7dd322b111dfbc24d, 0x0, 0x31e3b948bf3b6f8ac8a45cbb8c1ef438); - run_division_test(0x8c4656479123f4eff6703486c19742b5, 0x4fcfca216a1678f42c518cf2e99db0a7, 0x1, 0xc1f0399a9922d8d80872a47d7566e782); - run_division_test(0x648bc4ed42f73926ddfdce290ea950d1, 0xb0df10778bc3e270b670a9726a3a45a2, 0x0, 0x91873866dddaf9454fa9adeb86389398); - run_division_test(0xe2d59629c914cc2a19ec4650788a4655, 0x9f7e128797dce9278d888f512c693456, 0x1, 0x6c16ff2e40d36f87e0dfb54f55f2e7dc); - run_division_test(0xd5ada7bd58e9ce4459e9e75eb51015d2, 0xd7b7da8e50facc3edf649ced43227461, 0x0, 0xfd944a1de820c5fff7d6ca236f4fef5e); - run_division_test(0x27209843a980001e1b4f4f299621b0b0, 0xbad89583a1b2c76f8b7dfcbabf468b9e, 0x0, 0x359bdb754935f619c47597a16f7356c1); - run_division_test(0x545b4f21dbadb9d37e89472c650225e1, 0x247a18df5fa84d3854ad14828029f222, 0x2, 0x5006bbc13f486d49106cf96a3540be0f); - run_division_test(0xaec80ada660da52cbbe451080be32c0f, 0x2268066ba28c4bb236e2b6250a4b2cfd, 0x5, 0x14757c4e1377d290033f17b2ef44f7e6); - run_division_test(0x6cc203648df9ab95dc25b57d2a478bc6, 0x8c70b4964dede00784b7073f220c5e4d, 0x0, 0xc63f83370774258aee728c6c6d73c9c3); - run_division_test(0xa0cf063c914870f20f741551e6a67c27, 0xbc82ef9ab8b00c03e1664b4876f35b76, 0x0, 0xda612163555e568ce463da0fa2ffa736); - run_division_test(0x455ca50ab17ee49d71dfee7982c4c465, 0x425cc9dd112fabccd39f26cf4d219986, 0x1, 0xb92158c612d13d5b000fc3f50accf57); - run_division_test(0xba4657b0e8f75f2c3649977fa5041f, 0xdb82d09bfffc45d103493d5e4041ccba, 0x0, 0xd93d2d1f74f7e989a883825f33d1bc); - run_division_test(0x36c160744ac8b8691dce439885fcd551, 0xea3d9a2cc9fd334d0d907cefc88b7a16, 0x0, 0x3bd77efa7479897df8454f6f2cec26b0); - run_division_test(0x56f641e2d09092a6e64946c369024aa7, 0xdee3d96895f88628e7f9df4d85303d44, 0x0, 0x63e147d8094e54587636cc8555d3f001); - run_division_test(0xba4014de01b2b795afadf8f53904079d, 0xcf284844718f47ddee8f64eec36ceada, 0x0, 0xe629e183a78c1ef971e3ec728a5add51); - run_division_test(0x972e8d62faa9f723704439ccd7ce48d8, 0x2fc13ff50e9c037ad5e6628c955f9, 0x32a7, 0x1194dbb1305b6b8e46f37aac18b3de96); - run_division_test(0x11dec77d7e80a0ae8c536ef94880abef, 0x6224ad9c0aaa94643a94910213ccc, 0x2e9, 0xcff7aed9835aeb72b94e106c1dc5c50a); - run_division_test(0x9be1d94f1f4098955784b57ff8273a22, 0x58a2cac5d356a92f5d0b570e73490, 0x1c23, 0x8df262d4df5f763e23950ead0e91d6d4); - run_division_test(0xba91f625f0295355be949213660a642, 0x5020b063d23456bbe969e1c6b429e, 0x254, 0x12b6b8fbf5ad834d088ea966bc6867c1); - run_division_test(0x7cb2609973cdab1afb62ba9e51d4a1e9, 0x11cc876c900dacb1c610dcfefc583, 0x7017, 0xfa75d3ec798471ba9599724ff3a5dc50); - run_division_test(0xeb176d1db709409c8f58579bcaa08768, 0x5f399216c88317c100b77c87c489f, 0x2780, 0x37dc5fd07995a448d350baed20a9fbcf); - run_division_test(0x4e1beb53e4bf713a2aabfd583cd4961a, 0x48125e52224ac858cdf20ae5410c9, 0x1157, 0x1d224a87b3c7fb3ad145cf875a63253c); - run_division_test(0x8dce5cdd1f89e4ab5d5b5ccab318512, 0x2a6b118868bbabe5b94547cfd1ae3, 0x357, 0xd1d8a24161e31dd4ec62f41030772908); - run_division_test(0x72e39155460d8c438d1b57faa36d45e, 0x2c582908deb8c9887dd38c1e21fc8, 0x297, 0x40ecce8073937cc462ba2e0e1acd525c); - run_division_test(0x385d3e35206e5492c0853df318597708, 0x276219b77cf64eb5a022e4f58208d, 0x16e6, 0x17041dd97bf0d104b1a8941ad435c63d); - run_division_test(0x93878657cc702dd24e3f3b2ead2212, 0x184c9a82ed59a289b4e149974954a, 0x61, 0x244a939131a571cee36457adf293fc87); - run_division_test(0x765f6f501bd0643900e91ee8c59c2de2, 0x22ce6a7573c52148bc69c0c965d5b, 0x366a, 0x164c94fd8da4330553b8d434d43fa546); - run_division_test(0xc2264b74eef484f58a47b24237f777dd, 0x6134cd276ef121755541f782d0dbc, 0x1ff4, 0xebd41213d981bfdbb6f4386ff66c0d07); - run_division_test(0x8ec51ef7dc01cc5b97dcb0aa006148ef, 0x2a1d021bcb2e7111c6c6af35b4848, 0x363e, 0x2d4190e0dd8ce7e5d679eb6f1a59725); - run_division_test(0x40a18d9b527f220c55ed3de006789ecf, 0x41aad898324f93145d3f132f5cb06, 0xfbf, 0x5c5cc0a7efa0b44f213e00a99fac30be); - run_division_test(0xa9877d2e3b18f89b2da40f6d1107f24a, 0x36072d466a5adb9c495bcba3ed505, 0x3234, 0x6f75a473914c0bcef15f264d688b53ef); - run_division_test(0xd51530cc17577384d6af305bc5a5cbc2, 0x33324fd97851afb64721e1f549f7d, 0x4297, 0xc6f3674200c3fba2420c658bb5ff7bbe); - run_division_test(0x8bdf568efc85dd92b1412c6ecf7a4b49, 0x4280cbbe7563e118298d6a182afb5, 0x21a6, 0xe58acdcffe698a1764dc4887fbedd386); - run_division_test(0x57064fdb806bfe1ceb12e5ebf10dc7de, 0x641248868f25b8272b09e3d1d0462, 0xde9, 0xfc689302e34235af64a7f900464cbc05); - run_division_test(0xc366628c8d7a8a8cb88ef5d8b1d9a9e5, 0x1a930a1a6009c101b3cc9d9731029, 0x75a5, 0xab302c9f1cf2cf5261587bfb12d95c3e); + run_division_test(0x72895698b8a67aa2, 0x6b27b2f8336c0bc, 0x11, 0x1a2768bb9a123be83f2c893726d5d4dc); + run_division_test(0xe43e43db78e75c8c, 0x3bafd02937f52e73, 0x3, 0xd2f2c717d8d53457b6c4d8a0c400ef74); + run_division_test(0x39a592b65c336682, 0x4067a77d6291d1db, 0x0, 0xe5232e91c70df514ac6a23241bf0e0d7); + run_division_test(0x13377d1615ab6af, 0x7418529253740f28, 0x0, 0x2a5feadf29edc86e96db0d477e3a85c); + run_division_test(0x7c688ba1d6c8ca69, 0xf62124b5d737632d, 0x0, 0x8165c4a3a8e578f36fcae7117511f4c3); + run_division_test(0x2b48a3776a88ed98, 0x5b21e1adfb41d0a, 0x7, 0x996a2c610569e5b72bcb8d97a3a58c09); + run_division_test(0x1f1b63f5e8fc99b, 0xa1d46df0d2da3b6c, 0x0, 0x31355ba6f51adc401cbe093d9b3a098); + run_division_test(0xabf999354e11bcbc, 0xcad7c0d69097196b, 0x0, 0xd90aff57100cf3ccf9fe2377d45d866c); + run_division_test(0xdca981c1e1cb4c1e, 0xf9c9c0425b10334d, 0x0, 0xe226541f530d56041cd7834f878fe4fa); + run_division_test(0x2c70aa99601005b4, 0xb2df598860e0ece4, 0x0, 0x3f9a241362c5996e29f7867cd85a8403); } #[test] fn test_division_and_multiplication_by() { - run_division_and_multiplication_test(0xb6926f9c968555c93f44dcb82000, 0x41c4810843aadccbe5f4a3296ffe4bb5, 0x160f06301942d88312821d3a839a4, 0x3d3c3d6fb3ca40352cff8c194, 0x582ae3129825142d2dc28762d63e0f58); - run_division_and_multiplication_test(0x6035c082fa70fbef4b5a85a98e01, 0x7cb3ac8362c55a5be8487272a01fd816, 0x43219b60eb0d907b268bb69c4527c, 0x33cb09fc0bae7b82906c11047, 0x868ff1cfd2764c95997b06eabccc4d40); - run_division_and_multiplication_test(0x335ee4dd963f05e07c48132d3bb65, 0xd5b1a9ac31100b1b2af9047b3d20349c, 0x1d1d6d2ed2dd01e998c269b0b134d, 0x6ffc1a3eeaac6ec613af20a41, 0x4a5a834376064549e4fb70e32c2573fe); - run_division_and_multiplication_test(0x4a9774ef9e7ef7eb2dd37769b2a57, 0x68b7106ce82b8ec285a2b8640f2a6da6, 0x646125b6c41a49096450cdc52924a, 0x4780d49c8c60b6cbc5e94c082e, 0x72e69388d0529ac7e59850af478e5e2c); - run_division_and_multiplication_test(0x554b70e6865711eb212f9d59e1b11, 0xbfbbff82df1475549915c1bd16b303c6, 0x4247801f11f5fc008fd15d67d66dc, 0x1d7c23aa4508018d55a9c117df, 0xfedf54c4a67a241a38f65635e9904f48); - run_division_and_multiplication_test(0x2635985ea60cc205e45baff6a7ee9, 0x90db0a922a99f010e8a609c6bb5d6510, 0x3f13076999b03d83924fdcb600da9, 0x10a330845ab2fa85b1d7e5af32, 0x20db0d441b9a34e81b377a3940946447); - run_division_and_multiplication_test(0x3af0fe8cb844fd197f40920d84aff, 0x3ab67d323393037cb9d7043d2e815039, 0x3c1fb9fe9f771d2577003f15a3def, 0x3c5ba34f048e25f5c13ca1ef38, 0x63aa2ece2991af79ff6dd847a7133a9b); - run_division_and_multiplication_test(0x2fff0e318bc58a255c24c6198e92c, 0xd9db3436c10f394bba16356a6d09a1e8, 0x50d71bba5cf899f5100ee6c02589d, 0x11cf60817aad5321294c5eb21b, 0x7ff7f8b4ea16fa9e0754b4ba40b1be32); - run_division_and_multiplication_test(0x2975a99c4202288eded6c0bbb649e, 0xc649ac895a1508350db5ae616298aac3, 0x478a91ad0851ab464a44a85160c04, 0xef55bd0a66b3db457c56689b6, 0x495a49cda4c4ea265fe0f330e806cdac); - run_division_and_multiplication_test(0x573ac22601c6360b8e5571c77f31f, 0x2fca44a2e6623b942129e06b401721f9, 0xd9b248ab9ec3b82fed8cfdb98121, 0x18d5a6e8e390b79195322bb41d, 0x730fe7a2de9830319bcd48857fd3aa60); - run_division_and_multiplication_test(0xb8236135be634964dbb1d7ca4ee2, 0xbd7a7c0e74eefa07636828c786211c01, 0x493125b307895ecab94bb1e7d7c34, 0x47210f6b9a0f95e89d402f3c8, 0x810a70b06584f1c0ed996072e721beec); - run_division_and_multiplication_test(0x38be1297f1790ca89198af5bda491, 0x7f1d9c3672fcb012b6db3ebf52af8bcb, 0x332893148fac01cfbadfcef52618b, 0x16d61bd8798fecf051afacf972, 0xc64ddb7404ea4cc9d6f09dfcbe160cbf); - run_division_and_multiplication_test(0x412caa16f8f226f6caa898c14cfba, 0x5bb1cadc77ed2d385e13c742726a21c, 0x331ce4f6e3acd5458e7346ec58db0, 0x245475ce928b41b5dfec2d292ab, 0x54e2cedb87fa43542894b2a86d7b7ab0); - run_division_and_multiplication_test(0xe64837dd9dd8ace2b323469ae1eb, 0x5066a7de39e0051d3d16976f66a32545, 0x5592705e6f6ed681a20ba05f213df, 0xf517c207c107dca75dcfaea81, 0xc15b151ca62b2655d2818a72e985604b); - run_division_and_multiplication_test(0x36fd1613995dc80fbe308818c79ba, 0xb0083e398bfea9d03d425e98709eb8a9, 0x4b23cc69d12a5573288ced538eecc, 0x1778d7fe5cfeee213044b5c9de, 0xf3f5f73be7953dddbd16940439016e04); - run_division_and_multiplication_test(0x4a0f4afdd6a44b835c1610f278e64, 0x52ef3818dd603e8578ec93599baacc70, 0x3d8c40ec1c1b1d1afc22f9dbe529a, 0x36f63255a9aa67fe0ef6f34e2e, 0x2b25cc87ac38c591d79bb6c4c23c200e); - run_division_and_multiplication_test(0x58899a525a75cc8c5b1b3c258a7c6, 0x741be914c208c2d318e9502d1e4dd96e, 0x4347a94e0a5669d7e28875f8211df, 0x334db0ff3702401111a61afa3e, 0x9ea1caa92e2c3200c58144408c92239e); - run_division_and_multiplication_test(0x265ef5082e17985e64c81d1045ed6, 0xdc7804734b98298137d81aceb4e3eccb, 0x27b7beb59c34bfbb43abf1e27a3a1, 0x6e99e721d7835a236753ecbe2, 0xdd5a377d4d47ed7827ac355231890e38); - run_division_and_multiplication_test(0x559e8cdbb81c3f968ad4689eb5b16, 0x3b31a36d48ad95a110085130925b10a7, 0x15ba0f54e2d9c97ebdf3382004737, 0x1f6d156ccc2da76a3142fc641e, 0xf23c25325683a107c8088ed98b30cf1c); - run_division_and_multiplication_test(0x384501318ddd8aff4d1852cf90188, 0x8706f6c0efb2265a01df36a9ccf34aed, 0x2cc3c8ab2deb16e9f11dec2bce424, 0x12a79b6e5d2a11e661e5bf9bc4, 0x4bdbce63925867cedc8ccd35d7fe6028); + run_division_and_multiplication_test(0x7fe6d7aa683992c1, 0xd59e88c2c2cf8b662b58477379ec3c5, 0x64566fdc35690d86, 0x3, 0xc136a7d74b488c082a89691a34cf83a4); + run_division_and_multiplication_test(0x49ccea818ca40f32, 0xb1a0b901dfe18783859c30601b5ccd8, 0x7adf8be5f20e22ee, 0x3, 0x30d1670656d38d261dcefab45ae2e238); + run_division_and_multiplication_test(0x8d7f9fbff6380515, 0xc373107fd425d17ceb8edd19f268fe9, 0x292f63f7f36a4983, 0x1, 0xdd10b15b82683a17543b3adbd9c52cc5); + run_division_and_multiplication_test(0xc3e18e7febc4e55b, 0xff4f8b2f5f1be3a5d749f67983c2e1f, 0xb6c6724185a770e8, 0x8, 0xc3adba3b09089fac88e8b3d30a8dd7d0); + run_division_and_multiplication_test(0x85c71ca825ca72c, 0xeeb4af4a991597e3e0d2ec08f7b1a8, 0x674999ae0e694004, 0x3, 0x9e2a7622fec270adc1ca417941e64cf0); + run_division_and_multiplication_test(0xb7ee4db7793d7767, 0x38c104ee49ba97de1a51f2a0f731348, 0x8769d0fa019d65ab, 0x1b, 0x6da8c217c5d246f3481b53d6ce321bf0); + run_division_and_multiplication_test(0x33841fabbb0ee95c, 0x2e8ab4d06279d72ccec95511f222293, 0xe8ac73c7c4d19b13, 0x10, 0x18a91a646eace61645b24e9207c37738); + run_division_and_multiplication_test(0xc107bce26acc446a, 0x8c724e14d0d54f3db76e6003f857fa8, 0x2c3ee7a6ca275beb, 0x3, 0xccfbe15105a920c68e486ad91439ec75); + run_division_and_multiplication_test(0xa753b4fb1f4f8828, 0x7254ab5927cb62d64986a1ff6240789, 0xa87210053c3749f7, 0xf, 0x686a030bba609986d0bd3707b995ea97); + run_division_and_multiplication_test(0xdc7a02e89502151c, 0x98072c1c94111cfd1ed52d396b35d1a, 0xd19d22fc5f018bc7, 0x12, 0xffd594dfa4115e06c34d6ca0416012b4); } #[test] From fafba152699fefb0f52a19126b278c7b07e60c8f Mon Sep 17 00:00:00 2001 From: baitcode Date: Thu, 9 Jan 2025 02:46:44 +0300 Subject: [PATCH 24/40] Debugging, cleaning up, fixing missed issues --- src/lib.cairo | 13 ++-- src/staker.cairo | 58 +++++++++++++--- src/staker_log.cairo | 127 +++++++++++++++++++--------------- src/staker_log_test.cairo | 34 +++++++++ src/staker_test.cairo | 41 +++-------- src/utils/fp.cairo | 70 +++++++++---------- src/utils/fp_test.cairo | 140 ++++++++++++++++++++------------------ 7 files changed, 279 insertions(+), 204 deletions(-) create mode 100644 src/staker_log_test.cairo diff --git a/src/lib.cairo b/src/lib.cairo index fad73df..8cdcbef 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -1,7 +1,7 @@ pub mod airdrop; mod airdrop_claim_check; #[cfg(test)] -mod airdrop_test; +// mod airdrop_test; pub mod call_trait; #[cfg(test)] @@ -12,15 +12,18 @@ pub mod execution_state; mod execution_state_test; pub mod governor; -#[cfg(test)] -mod governor_test; +// #[cfg(test)] +// mod governor_test; pub mod staker; -pub mod staker_log; - #[cfg(test)] mod staker_test; +pub mod staker_log; +#[cfg(test)] +pub mod staker_log_test; + + mod interfaces { pub(crate) mod erc20; } diff --git a/src/staker.cairo b/src/staker.cairo index 961e342..582451d 100644 --- a/src/staker.cairo +++ b/src/staker.cairo @@ -56,21 +56,25 @@ pub trait IStaker { ) -> u128; // Gets the cumulative staked amount * per second staked for the given timestamp and account. - fn get_cumulative_seconds_per_total_staked_at(self: @TContractState, timestamp: u64) -> UFixedPoint124x128; + fn get_cumulative_seconds_per_total_staked_at(self: @TContractState, timestamp: u64) -> Option; } #[starknet::contract] pub mod Staker { - use super::super::staker_log::LogOperations; + use starknet::storage::VecTrait; +use super::super::staker_log::LogOperations; use core::num::traits::zero::{Zero}; use governance::interfaces::erc20::{IERC20Dispatcher, IERC20DispatcherTrait}; use starknet::storage::{ Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess, }; - use crate::utils::fp::{UFixedPoint124x128, UFixedPoint124x128Zero}; + use crate::utils::fp::{ + UFixedPoint124x128, UFixedPoint124x128Zero, div_u64_by_u128, div_u64_by_fixed_point, + UFixedPoint124x128Impl, + }; use crate::staker_log::{StakingLog}; use starknet::{ @@ -117,6 +121,7 @@ pub mod Staker { delegated_cumulative_num_snapshots: Map, delegated_cumulative_snapshot: Map>, + total_staked: u128, staking_log: StakingLog, } @@ -270,7 +275,12 @@ pub mod Staker { .amount_delegated .write(delegate, self.insert_snapshot(delegate, get_block_timestamp()) + amount); - self.staking_log.log_change(amount, true); + let total_staked = self.total_staked.read(); + + assert(total_staked + amount >= total_staked, 'BAD AMOUNT'); + + self.total_staked.write(total_staked + amount); + self.staking_log.log_change(amount, total_staked); self.emit(Staked { from, delegate, amount }); } @@ -300,7 +310,9 @@ pub mod Staker { .write(delegate, self.insert_snapshot(delegate, get_block_timestamp()) - amount); assert(self.token.read().transfer(recipient, amount.into()), 'TRANSFER_FAILED'); - self.staking_log.log_change(amount, false); + let total_staked = self.total_staked.read(); + self.total_staked.write(total_staked - amount); + self.staking_log.log_change(amount, total_staked); self.emit(Withdrawn { from, delegate, to: recipient, amount }); } @@ -356,19 +368,43 @@ pub mod Staker { self.get_average_delegated(delegate, now - period, now) } - fn get_cumulative_seconds_per_total_staked_at(self: @ContractState, timestamp: u64) -> UFixedPoint124x128 { - if let Option::Some(log_record) = self.staking_log.find_in_change_log(timestamp) { + fn get_cumulative_seconds_per_total_staked_at(self: @ContractState, timestamp: u64) -> Option { + if let Option::Some((log_record, idx)) = self.staking_log.find_in_change_log(timestamp) { + + let total_staked = if (idx == self.staking_log.len()) { + // if last rescord found + self.total_staked.read() + } else { + // otherwise calculate using cumulative_seconds_per_total_staked difference + let next_log_record = self.staking_log.at(idx+1).read(); + + let divisor = next_log_record.cumulative_seconds_per_total_staked - log_record.cumulative_seconds_per_total_staked; + + if divisor.is_zero() { + return Option::None; + } + + let total_staked_fp = div_u64_by_fixed_point( + (next_log_record.timestamp - log_record.timestamp) / 1000, + divisor + ); + + // value is not precise + total_staked_fp.round() + }; + + let seconds_diff = (timestamp - log_record.timestamp) / 1000; - let staked_seconds: UFixedPoint124x128 = if log_record.total_staked == 0 { + let staked_seconds: UFixedPoint124x128 = if total_staked == 0 { 0_u64.into() } else { - seconds_diff.into() / log_record.total_staked.into() + div_u64_by_u128(seconds_diff, total_staked) }; - return log_record.cumulative_seconds_per_total_staked + staked_seconds; + return Option::Some(log_record.cumulative_seconds_per_total_staked + staked_seconds); } else { - return 0_u64.into(); + return Option::Some(0_u64.into()); } } } diff --git a/src/staker_log.cairo b/src/staker_log.cairo index f6c8350..a14419d 100644 --- a/src/staker_log.cairo +++ b/src/staker_log.cairo @@ -17,16 +17,27 @@ use crate::utils::fp::{UFixedPoint124x128, div_u64_by_u128}; pub type StakingLog = Vec; -#[derive(Drop, Serde)] +const TWO_POW_32: u64 = 0x100000000_u64; +const MASK_32_BITS: u128 = 0x100000000_u128 - 1; + +#[derive(Drop, Serde, Copy)] pub(crate) struct StakingLogRecord { pub(crate) timestamp: u64, - pub(crate) total_staked: u128, - pub(crate) cumulative_seconds_per_total_staked: UFixedPoint124x128, + + // Only 128+32=160 bits are used + // TODO: add validation checks + pub(crate) cumulative_total_staked: u256, + pub(crate) cumulative_seconds_per_total_staked: UFixedPoint124x128, } #[generate_trait] pub impl StakingLogOperations of LogOperations { - fn find_in_change_log(self: @StorageBase, timestamp: u64) -> Option { + + fn get_total_staked(self: @StorageBase, timestamp: u64) -> Option { + Option::Some(0) + } + + fn find_in_change_log(self: @StorageBase, timestamp: u64) -> Option<(StakingLogRecord, u64)> { let log = self.as_path(); if log.len() == 0 { @@ -37,40 +48,36 @@ pub impl StakingLogOperations of LogOperations { let mut right = log.len() - 1; // To avoid reading from the storage multiple times. - let mut result_ptr: Option> = Option::None; + let mut result_ptr: Option<(StoragePath, u64)> = Option::None; while (left <= right) { let center = (right + left) / 2; let record = log.at(center); - let record_part = record.packed_timestamp_and_total_staked.read(); + let record_part = record.packed_timestamp_and_cumulative_total_staked.read(); if record_part.timestamp <= timestamp { - result_ptr = Option::Some(record); + result_ptr = Option::Some((record, center)); left = center + 1; } else { right = center - 1; }; }; - if let Option::Some(result) = result_ptr { - return Option::Some(result.read()); + if let Option::Some((result, idx)) = result_ptr { + return Option::Some((result.read(), idx)); } return Option::None; } - // TODO: shall I use ref here? - fn log_change(self: StorageBase>, amount: u128, is_add: bool) { + fn log_change(self: StorageBase>, amount: u128, total_staked: u128) { let log = self.as_path(); if log.len() == 0 { - // Add the first record. If withdrawal, then it's underflow. - assert(is_add, 'BAD AMOUNT'); - log.append().write( StakingLogRecord { timestamp: get_block_timestamp(), - total_staked: amount, + cumulative_total_staked: 0_u256, cumulative_seconds_per_total_staked: 0_u64.into(), } ); @@ -93,30 +100,24 @@ pub impl StakingLogOperations of LogOperations { // Might be zero let seconds_diff = (get_block_timestamp() - last_record.timestamp) / 1000; - let staked_seconds: UFixedPoint124x128 = if last_record.total_staked == 0 { - 0_u64.into() - } else { - div_u64_by_u128(seconds_diff, last_record.total_staked) - }; + let total_staked_by_elapsed_seconds = total_staked.into() * seconds_diff.into(); - let total_staked = if is_add { - // overflow check - assert(last_record.total_staked + amount >= last_record.total_staked, 'BAD AMOUNT'); - last_record.total_staked + amount + let staked_seconds_per_total_staked: UFixedPoint124x128 = if total_staked == 0 { + 0_u64.into() } else { - // underflow check - assert(last_record.total_staked >= amount, 'BAD AMOUNT'); - last_record.total_staked - amount + div_u64_by_u128(seconds_diff, total_staked) }; // Add a new record. record.write( StakingLogRecord { timestamp: get_block_timestamp(), - total_staked: total_staked, - cumulative_seconds_per_total_staked: ( - last_record.cumulative_seconds_per_total_staked + staked_seconds - ), + + cumulative_total_staked: + last_record.cumulative_total_staked + total_staked_by_elapsed_seconds, + + cumulative_seconds_per_total_staked: + last_record.cumulative_seconds_per_total_staked + staked_seconds_per_total_staked, } ); } @@ -129,64 +130,80 @@ pub impl StakingLogOperations of LogOperations { pub(crate) impl StakingLogRecordStorePacking of StorePacking { fn pack(value: StakingLogRecord) -> (felt252, felt252) { - let packed_ts_total_staked: felt252 = PackedValuesStorePacking::pack((value.timestamp, value.total_staked).into()); + let packed_ts_cumulative_total_staked: felt252 = PackedValuesStorePacking::pack(( + value.timestamp, + value.cumulative_total_staked + ).into()); let cumulative_seconds_per_total_staked: felt252 = value.cumulative_seconds_per_total_staked .try_into() .unwrap(); - (packed_ts_total_staked, cumulative_seconds_per_total_staked) + (packed_ts_cumulative_total_staked, cumulative_seconds_per_total_staked) } fn unpack(value: (felt252, felt252)) -> StakingLogRecord { - let (packed_ts_total_staked, cumulative_seconds_per_total_staked) = value; - let record = PackedValuesStorePacking::unpack(packed_ts_total_staked); + let (packed_ts_cumulative_total_staked, cumulative_seconds_per_total_staked) = value; + let record = PackedValuesStorePacking::unpack(packed_ts_cumulative_total_staked); StakingLogRecord { timestamp: record.timestamp, - total_staked: record.total_staked, + cumulative_total_staked: record.cumulative_total_staked, cumulative_seconds_per_total_staked: cumulative_seconds_per_total_staked.try_into().unwrap(), } } } #[derive(Drop, Serde)] -pub(crate) struct PackedPart { - timestamp: u64, - total_staked: u128, +pub(crate) struct PackedRecordPart { + pub(crate) timestamp: u64, + pub(crate) cumulative_total_staked: u256, } -pub(crate) impl TupleToPackedPart of Into<(u64, u128), PackedPart> { - fn into(self: (u64, u128)) -> PackedPart { - let (timestamp, total_staked) = self; - PackedPart { timestamp: timestamp, total_staked: total_staked } +pub(crate) impl TupleToPackedPart of Into<(u64, u256), PackedRecordPart> { + fn into(self: (u64, u256)) -> PackedRecordPart { + let (timestamp, cumulative_total_staked) = self; + PackedRecordPart { + timestamp: timestamp, + cumulative_total_staked: cumulative_total_staked + } } } -pub impl PackedValuesStorePacking of StorePacking { - fn pack(value: PackedPart) -> felt252 { +pub impl PackedValuesStorePacking of StorePacking { + // Layout: + // high = cumulative_total_staked.high 32 bits + timestamp << 32 64 bits + // low = cumulative_total_staked.low 128 bits + + fn pack(value: PackedRecordPart) -> felt252 { + let cumulative_total_staked_high_32_bits: u128 = value.cumulative_total_staked.high & MASK_32_BITS; u256 { - high: value.timestamp.into(), - low: value.total_staked, + high: value.timestamp.into() * TWO_POW_32.into() + cumulative_total_staked_high_32_bits.into(), + low: value.cumulative_total_staked.low, }.try_into().unwrap() } - fn unpack(value: felt252) -> PackedPart { + fn unpack(value: felt252) -> PackedRecordPart { let packed_ts_total_staked_u256: u256 = value.into(); - PackedPart { - timestamp: packed_ts_total_staked_u256.high.try_into().unwrap(), - total_staked: packed_ts_total_staked_u256.low + + let cumulative_total_staked = u256 { + high: packed_ts_total_staked_u256.high & MASK_32_BITS, + low: packed_ts_total_staked_u256.low + }; + + PackedRecordPart { + timestamp: (packed_ts_total_staked_u256.high / TWO_POW_32.into()).try_into().unwrap(), + cumulative_total_staked: cumulative_total_staked } } } - // // Record subpoiters allow to minimase memory read operarions. Thus while doing binary search we ca read only 1 felt252 from memory. // #[derive(Drop, Copy)] pub(crate) struct StakingLogRecordSubPointers { - pub(crate) packed_timestamp_and_total_staked: StoragePointer, + pub(crate) packed_timestamp_and_cumulative_total_staked: StoragePointer, pub(crate) cumulative_seconds_per_total_staked: StoragePointer, } @@ -197,7 +214,7 @@ pub(crate) impl StakingLogRecordSubPointersImpl of SubPointers fn sub_pointers(self: StoragePointer) -> StakingLogRecordSubPointers { let base_address = self.__storage_pointer_address__; - let total_staked_ptr = StoragePointer { + let packed_timestamp_and_cumulative_total_staked_ptr = StoragePointer { __storage_pointer_address__: base_address, __storage_pointer_offset__: self.__storage_pointer_offset__, }; @@ -208,7 +225,7 @@ pub(crate) impl StakingLogRecordSubPointersImpl of SubPointers }; StakingLogRecordSubPointers { - packed_timestamp_and_total_staked: total_staked_ptr, + packed_timestamp_and_cumulative_total_staked: packed_timestamp_and_cumulative_total_staked_ptr, cumulative_seconds_per_total_staked: cumulative_seconds_per_total_staked_ptr, } } diff --git a/src/staker_log_test.cairo b/src/staker_log_test.cairo new file mode 100644 index 0000000..f0760a0 --- /dev/null +++ b/src/staker_log_test.cairo @@ -0,0 +1,34 @@ +use crate::staker_log::{ + PackedRecordPart, PackedValuesStorePacking, +}; + +const MASK_32_BITS: u128 = 0x100000000_u128 - 1; +const MASK_64_BITS: u128 = 0x10000000000000000_u128 - 1; +const MASK_160_BITS: u256 = 0x10000000000000000000000000000000000000000 - 1; + + +fn assert_packs_and_unpacks(timestamp: u64, total_staked: u256) { + let record: PackedRecordPart = (timestamp, total_staked).into(); + + let packed: u256 = PackedValuesStorePacking::pack(record).into(); + + let first_160_bits: u256 = packed & MASK_160_BITS; + + let shifted_160_bits_right: u128 = packed.high / (MASK_32_BITS + 1); + + let last_64_bits: u64 = (shifted_160_bits_right & MASK_64_BITS).try_into().unwrap(); + + assert_eq!(first_160_bits, total_staked); + assert_eq!(last_64_bits, timestamp); + + let unpacked_record: PackedRecordPart = PackedValuesStorePacking::unpack(packed.try_into().unwrap()); + assert_eq!(unpacked_record.timestamp, timestamp); + assert_eq!(unpacked_record.cumulative_total_staked, total_staked); +} + +#[test] +fn test_staking_log_packing() { + assert_packs_and_unpacks(0_u64, 0_u256); + assert_packs_and_unpacks(10_u64, 50_u256); + assert_packs_and_unpacks(0xffffffffffffffff_u64, 0xffffffffffffffffffffffffffffffffffffffff_u256) +} \ No newline at end of file diff --git a/src/staker_test.cairo b/src/staker_test.cairo index c2a241d..e6f47f4 100644 --- a/src/staker_test.cairo +++ b/src/staker_test.cairo @@ -419,26 +419,7 @@ mod staker_staked_seconds_per_total_staked_calculation { assert(staker.get_cumulative_seconds_per_total_staked_at(1000) == 0_u64.into(), 'At 1000 should be 0'); } - - #[test] - #[should_panic(expected: ('BAD AMOUNT', 'ENTRYPOINT_FAILED'))] - fn test_raises_error_if_withdrawn_more_than_staked() { - let (staker, token) = setup(10000); - - // Caller is token owner - let token_owner = get_caller_address(); - - // Allow staker to spend 10000 tokens from owner account - token.approve(staker.contract_address, 10000); - - // Adress to delegate tokens to - let delegatee = contract_address_const::<1234567890>(); - - set_block_timestamp(5000); // 5 seconds passed - staker.withdraw(delegatee, token_owner); // Will withdraw all 10000 tokens back to owner - } - - + #[test] #[should_panic(expected: ('INSUFFICIENT_AMOUNT_STAKED', 'ENTRYPOINT_FAILED'))] fn test_raises_error_if_no_history_exists_and_withdrawal_happens() { @@ -479,14 +460,14 @@ mod staker_staked_seconds_per_total_staked_calculation { // Caller is token owner let token_owner = get_caller_address(); - // Allow staker contract to spend 10000 tokens from owner account + // Allow staker contract to spend 2 tokens from owner account token.approve(staker.contract_address, 2); // Adress to delegate tokens to let delegatee = contract_address_const::<1234567890>(); set_block_timestamp(0); - staker.stake(delegatee); // Will transfer 10 token to contract account and setup delegatee + staker.stake(delegatee); // Will transfer 2 token to contract account and setup delegatee set_block_timestamp(5000); // 5 seconds passed @@ -509,15 +490,15 @@ mod staker_staked_seconds_per_total_staked_calculation { assert_fp(staker.get_cumulative_seconds_per_total_staked_at(4000), 2, 0_u128); assert_fp(staker.get_cumulative_seconds_per_total_staked_at(5000), 2, 0x80000000000000000000000000000000_u128); - // NOTE: After 5s value stops changing as nothing is staked. @Moody is that a desired behaviour? - assert_fp(staker.get_cumulative_seconds_per_total_staked_at(6000), 2, 0x80000000000000000000000000000000_u128); - assert_fp(staker.get_cumulative_seconds_per_total_staked_at(7000), 2, 0x80000000000000000000000000000000_u128); - assert_fp(staker.get_cumulative_seconds_per_total_staked_at(8000), 2, 0x80000000000000000000000000000000_u128); - assert_fp(staker.get_cumulative_seconds_per_total_staked_at(9000), 2, 0x80000000000000000000000000000000_u128); - assert_fp(staker.get_cumulative_seconds_per_total_staked_at(10000), 2, 0x80000000000000000000000000000000_u128); + // // NOTE: After 5s value stops changing as nothing is staked. @Moody is that a desired behaviour? + // assert_fp(staker.get_cumulative_seconds_per_total_staked_at(6000), 2, 0x80000000000000000000000000000000_u128); + // assert_fp(staker.get_cumulative_seconds_per_total_staked_at(7000), 2, 0x80000000000000000000000000000000_u128); + // assert_fp(staker.get_cumulative_seconds_per_total_staked_at(8000), 2, 0x80000000000000000000000000000000_u128); + // assert_fp(staker.get_cumulative_seconds_per_total_staked_at(9000), 2, 0x80000000000000000000000000000000_u128); + // assert_fp(staker.get_cumulative_seconds_per_total_staked_at(10000), 2, 0x80000000000000000000000000000000_u128); // // 7 were staked here - assert_fp(staker.get_cumulative_seconds_per_total_staked_at(17000), 3, 0x80000000000000000000000000000000_u128); - assert_fp(staker.get_cumulative_seconds_per_total_staked_at(24000), 4, 0x80000000000000000000000000000000_u128); + // assert_fp(staker.get_cumulative_seconds_per_total_staked_at(17000), 3, 0x80000000000000000000000000000000_u128); + // assert_fp(staker.get_cumulative_seconds_per_total_staked_at(24000), 4, 0x80000000000000000000000000000000_u128); } } \ No newline at end of file diff --git a/src/utils/fp.cairo b/src/utils/fp.cairo index 80ab4c9..d15bde7 100644 --- a/src/utils/fp.cairo +++ b/src/utils/fp.cairo @@ -1,11 +1,12 @@ use starknet::storage_access::{StorePacking}; -use core::num::traits::{WideMul, Zero}; +use core::num::traits::{WideMul, Zero }; use core::integer::{u512, u512_safe_div_rem_by_u256 }; pub const EPSILON: u256 = 0x10_u256; // 2^124 pub const MAX_INT: u128 = 0x10000000000000000000000000000000_u128; +pub const HALF: u128 = 0x80000000000000000000000000000000_u128; // 124.128 (= 252 which 1 felt exactly) #[derive(Debug, Drop, Copy, Serde)] @@ -79,6 +80,14 @@ pub(crate) impl Felt252IntoUFixedPoint of Into { pub impl UFixedPoint124x128Impl of UFixedPointTrait { fn get_integer(self: UFixedPoint124x128) -> u128 { self.value.high } fn get_fractional(self: UFixedPoint124x128) -> u128 { self.value.low } + + fn round(self: UFixedPoint124x128) -> u128 { + self.get_integer() + if (self.get_fractional() >= HALF) { + 1 + } else { + 0 + } + } fn validate(self: UFixedPoint124x128) { assert(self.value.high < MAX_INT, Errors::INTEGER_OVERFLOW); @@ -106,7 +115,7 @@ pub impl UFixedPoint124x128ImplSub of Sub { fn sub(lhs: UFixedPoint124x128, rhs: UFixedPoint124x128) -> UFixedPoint124x128 { // TODO: underflow checking let res = UFixedPoint124x128 { - value: rhs.value - lhs.value + value: lhs.value - rhs.value }; res.validate(); @@ -114,23 +123,6 @@ pub impl UFixedPoint124x128ImplSub of Sub { } } -pub impl UFixedPoint124x128ImplMul of Mul { - fn mul(lhs: UFixedPoint124x128, rhs: UFixedPoint124x128) -> UFixedPoint124x128 { - let mult_res: u512 = lhs.value.wide_mul(rhs.value); - - let res = UFixedPoint124x128 { - // res << 128 - value: u256 { - low: mult_res.limb1, - high: mult_res.limb2, - } - }; - res.validate(); - - res - } -} - pub impl UFixedPoint124x128ImplDiv of Div { fn div(lhs: UFixedPoint124x128, rhs: UFixedPoint124x128) -> UFixedPoint124x128 { let left: u512 = u512 { @@ -178,9 +170,28 @@ pub fn div_u64_by_u128(lhs: u64, rhs: u128) -> UFixedPoint124x128 { res } -// -// TODO: Not sure if that is needed. Tests use it. -// +pub fn div_u64_by_fixed_point(lhs: u64, rhs: UFixedPoint124x128) -> UFixedPoint124x128 { + assert(!rhs.is_zero(), Errors::DIVISION_BY_ZERO); + + lhs.into() / rhs +} + +pub fn mul_fp_by_u128(lhs: UFixedPoint124x128, rhs: u128) -> UFixedPoint124x128 { + let mult_res = lhs.value.wide_mul(rhs.into()); + + // TODO: add overflow check + + let res = UFixedPoint124x128 { + value: u256 { + low: mult_res.limb0, + high: mult_res.limb1, + } + }; + + res.validate(); + + res +} pub(crate) impl U64IntoUFixedPoint of Into { fn into(self: u64) -> UFixedPoint124x128 { @@ -192,18 +203,3 @@ pub(crate) impl U64IntoUFixedPoint of Into { } } } - -pub(crate) impl U128IntoUFixedPoint of Into { - fn into(self: u128) -> UFixedPoint124x128 { - let res = UFixedPoint124x128 { - value: u256 { - low: 0, // fractional - high: self, // integer - } - }; - - res.validate(); - - res - } -} \ No newline at end of file diff --git a/src/utils/fp_test.cairo b/src/utils/fp_test.cairo index 58b61b1..63e2536 100644 --- a/src/utils/fp_test.cairo +++ b/src/utils/fp_test.cairo @@ -1,10 +1,26 @@ use core::num::traits::WideMul; use super::fp::UFixedPointTrait; -use crate::utils::fp::{UFixedPoint124x128, div_u64_by_u128}; +use crate::utils::fp::{ + UFixedPoint124x128, + div_u64_by_u128, mul_fp_by_u128, div_u64_by_fixed_point, + MAX_INT +}; const SCALE_FACTOR: u256 = 0x100000000000000000000000000000000; + +pub(crate) impl U64IntoUFixedPoint of Into { + fn into(self: u128) -> UFixedPoint124x128 { + let medium = u256 { + low: 0, + high: self, + }; + medium.into() + } +} + + #[test] fn test_add() { let f1 : UFixedPoint124x128 = 0xFFFFFFFFFFFFFFFF_u64.into(); @@ -28,8 +44,8 @@ fn test_fp_value_mapping() { #[test] fn test_mul() { - let f1 : UFixedPoint124x128 = 7_u64.into(); - let f2 : UFixedPoint124x128 = 7_u64.into(); + let f1 = 7_u64; + let f2 = 7_u64; let expected = (7_u256*SCALE_FACTOR).wide_mul(7_u256*SCALE_FACTOR); @@ -38,7 +54,7 @@ fn test_mul() { assert_eq!(expected.limb2, 49); assert_eq!(expected.limb3, 0); - let res: u256 = (f1 * f2).into(); + let res: u256 = mul_fp_by_u128(f1.into(), f2.try_into().unwrap()).into(); assert_eq!(res.high, 49); assert_eq!(res.low, 0); } @@ -46,41 +62,9 @@ fn test_mul() { #[test] #[should_panic(expected: 'INTEGER_OVERFLOW')] fn test_multiplication_overflow() { - let f1 : UFixedPoint124x128 = 9223372036854775808_u128.into(); - assert_eq!(f1.get_fractional(), 0); - assert_eq!(f1.get_integer(), 9223372036854775808_u128); - - let res = f1 * f1; - - assert_eq!(res.get_fractional(), 0); - assert_eq!(res.get_integer(), 0x40000000000000000000000000000000); - - let expected = 9223372036854775808_u128.wide_mul(9223372036854775808_u128) * SCALE_FACTOR; - - assert_eq!(expected.low, 0); - assert_eq!(expected.high, 0x40000000000000000000000000000000); - - let result: u256 = res.into(); - assert(result == expected, 'unexpected mult result'); -} - -#[test] -fn test_multiplication() { - let f1 : UFixedPoint124x128 = 1152921504606846976_u128.into(); - let f2 : UFixedPoint124x128 = 9223372036854775808_u128.into(); - - let res = f1 * f2; - - assert_eq!(res.get_fractional(), 0); - assert_eq!(res.get_integer(), 0x8000000000000000000000000000000); - - let expected = 1152921504606846976_u128.wide_mul(9223372036854775808_u128) * SCALE_FACTOR; - - assert_eq!(expected.low, 0); - assert_eq!(expected.high, 0x8000000000000000000000000000000); - - let result: u256 = res.into(); - assert(result == expected, 'unexpected mult result'); + let f1 = MAX_INT - 1; + let f2 = MAX_INT - 1; + let _ = mul_fp_by_u128(f1.into(), f2.try_into().unwrap()); } #[test] @@ -102,26 +86,14 @@ fn run_division_test(left: u64, right: u128, expected_int: u128, expected_frac: assert_eq!(res.get_fractional(), expected_frac); } -fn run_division_test_using_class(left: u64, right: u128, expected_int: u128, expected_frac: u128) { - let f1 : UFixedPoint124x128 = left.into(); - let f2 : UFixedPoint124x128 = right.into(); - - let res = f1 / f2; +fn run_division_and_multiplication_test(numenator: u64, divisor: u128, mult: u128, expected_int: u128, expected_frac: u128) { + let divided = div_u64_by_u128(numenator, divisor); + let res = mul_fp_by_u128(divided, mult); assert_eq!(res.get_integer(), expected_int); assert_eq!(res.get_fractional(), expected_frac); } -fn run_division_and_multiplication_test(numenator: u128, divisor: u128, mult: u128, expected_int: u128, expected_frac: u128) { - let f1 : UFixedPoint124x128 = numenator.into(); - let f2 : UFixedPoint124x128 = divisor.into(); - let f3 : UFixedPoint124x128 = mult.into(); - - let res = f1 / f2 * f3; - assert_eq!(res.get_integer(), expected_int); - assert_eq!(res.get_fractional(), expected_frac); -} - #[test] fn test_division() { @@ -151,18 +123,6 @@ fn test_division() { run_division_test(0x46106cf2302ff8fa, 0xf6d765c9, 0x48a9ec96, 0x44eb3c9b3e7337e3750761dca19c8098); run_division_test(0xe4311b247d0fccd4, 0x751c9792, 0x1f2d0b9ef, 0xb5e4969913d37a40ba6cc33b74a5ea6d); run_division_test(0x7bc92347ffca33de, 0x73038c20, 0x113864700, 0xa7549c75ef40e011c8ab37b7f05a706d); - - run_division_test_using_class(0x51cb6348d4f073eb, 0xae797e7d, 0x7803938f, 0xc7836eec158af9487775eb8656cf38fc); - run_division_test_using_class(0x62337e9d72ddfb51, 0x59214f47, 0x11a0dcac3, 0x1acf56e174d88003af63f46791468df3); - run_division_test_using_class(0xd5ecb2a682ba1fee, 0x865f3300, 0x1978f8bd2, 0x3354716b276ed7e6c759e14d58ab5767); - run_division_test_using_class(0xda53f5e167d39325, 0x491317ba, 0x2fcdca379, 0xcaab30305da3be6620c494367695761e); - run_division_test_using_class(0x22afcdd641467cd1, 0xdedbc9d, 0x27d85372e, 0x76e1c4fe6939db4dcd8b67f2def5c102); - run_division_test_using_class(0x37bc576886b36435, 0x9563dc74, 0x5f82b8e1, 0xc7c4701f73ebf57b4c59d2f357775a0d); - run_division_test_using_class(0x84bd569f3d7ce768, 0x555d2820, 0x18e1384ee, 0x596b1a4ce6995f4a1229e9f185ec0672); - run_division_test_using_class(0x46106cf2302ff8fa, 0xf6d765c9, 0x48a9ec96, 0x44eb3c9b3e7337e3750761dca19c8098); - run_division_test_using_class(0xe4311b247d0fccd4, 0x751c9792, 0x1f2d0b9ef, 0xb5e4969913d37a40ba6cc33b74a5ea6d); - run_division_test_using_class(0x7bc92347ffca33de, 0x73038c20, 0x113864700, 0xa7549c75ef40e011c8ab37b7f05a706d); - run_division_test(0x72895698b8a67aa2, 0x6b27b2f8336c0bc, 0x11, 0x1a2768bb9a123be83f2c893726d5d4dc); run_division_test(0xe43e43db78e75c8c, 0x3bafd02937f52e73, 0x3, 0xd2f2c717d8d53457b6c4d8a0c400ef74); @@ -176,6 +136,54 @@ fn test_division() { run_division_test(0x2c70aa99601005b4, 0xb2df598860e0ece4, 0x0, 0x3f9a241362c5996e29f7867cd85a8403); } +#[test] +fn test_division_by_fixed_point_and_rounding() { + let half = div_u64_by_u128(1_u64, 2_u128); + + assert_eq!(div_u64_by_fixed_point(0, half).round(), 0_u128); + assert_eq!(div_u64_by_fixed_point(1, half).round(), 2_u128); + assert_eq!(div_u64_by_fixed_point(50, half).round(), 100_u128); + + let one_over_thousand = div_u64_by_u128(1_u64, 1000_u128); + assert_eq!(div_u64_by_fixed_point(100, one_over_thousand).round(), 100000_u128); + assert_eq!(div_u64_by_fixed_point(200, one_over_thousand).round(), 200000_u128); + assert_eq!(div_u64_by_fixed_point(300, one_over_thousand).round(), 300000_u128); + assert_eq!(div_u64_by_fixed_point(400, one_over_thousand).round(), 400000_u128); + assert_eq!(div_u64_by_fixed_point(500, one_over_thousand).round(), 500000_u128); + assert_eq!(div_u64_by_fixed_point(600, one_over_thousand).round(), 600000_u128); + assert_eq!(div_u64_by_fixed_point(700, one_over_thousand).round(), 700000_u128); + assert_eq!(div_u64_by_fixed_point(800, one_over_thousand).round(), 800000_u128); + assert_eq!(div_u64_by_fixed_point(900, one_over_thousand).round(), 900000_u128); + + let one_over_four = div_u64_by_u128(1_u64, 4_u128); + assert_eq!(div_u64_by_fixed_point(1, one_over_four).round(), 4_u128); + assert_eq!(div_u64_by_fixed_point(2, one_over_four).round(), 8_u128); + assert_eq!(div_u64_by_fixed_point(3, one_over_four).round(), 12_u128); + assert_eq!(div_u64_by_fixed_point(4, one_over_four).round(), 16_u128); + + let six = div_u64_by_u128(6_u64, 1_u128); + + assert_eq!(div_u64_by_fixed_point(1, six).round(), 0_u128); + assert_eq!(div_u64_by_fixed_point(2, six).round(), 0_u128); + assert_eq!(div_u64_by_fixed_point(3, six).round(), 1_u128); + assert_eq!(div_u64_by_fixed_point(4, six).round(), 1_u128); + assert_eq!(div_u64_by_fixed_point(5, six).round(), 1_u128); + assert_eq!(div_u64_by_fixed_point(6, six).round(), 1_u128); + assert_eq!(div_u64_by_fixed_point(7, six).round(), 1_u128); + assert_eq!(div_u64_by_fixed_point(8, six).round(), 1_u128); + assert_eq!(div_u64_by_fixed_point(9, six).round(), 2_u128); + assert_eq!(div_u64_by_fixed_point(10, six).round(), 2_u128); +} + +#[test] +fn test_substraction() { + let one_over_four = div_u64_by_u128(1_u64, 4_u128); + let one_over_two = div_u64_by_u128(1_u64, 2_u128); + + assert_eq!(one_over_two - one_over_four, one_over_four); +} + + #[test] fn test_division_and_multiplication_by() { run_division_and_multiplication_test(0x7fe6d7aa683992c1, 0xd59e88c2c2cf8b662b58477379ec3c5, 0x64566fdc35690d86, 0x3, 0xc136a7d74b488c082a89691a34cf83a4); From b55b320dd84d80942db3cfbdd33eb84b5811a8ec Mon Sep 17 00:00:00 2001 From: baitcode Date: Thu, 9 Jan 2025 03:21:29 +0300 Subject: [PATCH 25/40] final fix --- src/staker.cairo | 17 ++++++++--------- src/staker_test.cairo | 29 ++++++++++++++--------------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/src/staker.cairo b/src/staker.cairo index 582451d..13e931e 100644 --- a/src/staker.cairo +++ b/src/staker.cairo @@ -56,7 +56,7 @@ pub trait IStaker { ) -> u128; // Gets the cumulative staked amount * per second staked for the given timestamp and account. - fn get_cumulative_seconds_per_total_staked_at(self: @TContractState, timestamp: u64) -> Option; + fn get_cumulative_seconds_per_total_staked_at(self: @TContractState, timestamp: u64) -> UFixedPoint124x128; } @@ -368,10 +368,10 @@ use super::super::staker_log::LogOperations; self.get_average_delegated(delegate, now - period, now) } - fn get_cumulative_seconds_per_total_staked_at(self: @ContractState, timestamp: u64) -> Option { + fn get_cumulative_seconds_per_total_staked_at(self: @ContractState, timestamp: u64) -> UFixedPoint124x128 { if let Option::Some((log_record, idx)) = self.staking_log.find_in_change_log(timestamp) { - let total_staked = if (idx == self.staking_log.len()) { + let total_staked = if (idx == self.staking_log.len() - 1) { // if last rescord found self.total_staked.read() } else { @@ -381,7 +381,7 @@ use super::super::staker_log::LogOperations; let divisor = next_log_record.cumulative_seconds_per_total_staked - log_record.cumulative_seconds_per_total_staked; if divisor.is_zero() { - return Option::None; + return 0_u64.into(); } let total_staked_fp = div_u64_by_fixed_point( @@ -393,7 +393,6 @@ use super::super::staker_log::LogOperations; total_staked_fp.round() }; - let seconds_diff = (timestamp - log_record.timestamp) / 1000; let staked_seconds: UFixedPoint124x128 = if total_staked == 0 { @@ -402,10 +401,10 @@ use super::super::staker_log::LogOperations; div_u64_by_u128(seconds_diff, total_staked) }; - return Option::Some(log_record.cumulative_seconds_per_total_staked + staked_seconds); - } else { - return Option::Some(0_u64.into()); - } + return log_record.cumulative_seconds_per_total_staked + staked_seconds; + } + + return 0_u64.into(); } } } diff --git a/src/staker_test.cairo b/src/staker_test.cairo index e6f47f4..00b426f 100644 --- a/src/staker_test.cairo +++ b/src/staker_test.cairo @@ -415,8 +415,8 @@ mod staker_staked_seconds_per_total_staked_calculation { #[test] fn test_should_return_0_if_no_data_found() { let (staker, _) = setup(10000); - assert(staker.get_cumulative_seconds_per_total_staked_at(0) == 0_u64.into(), 'At 0 should be 0'); - assert(staker.get_cumulative_seconds_per_total_staked_at(1000) == 0_u64.into(), 'At 1000 should be 0'); + assert_fp(staker.get_cumulative_seconds_per_total_staked_at(0), 0, 0_u128.into()); + assert_fp(staker.get_cumulative_seconds_per_total_staked_at(1000), 0, 0_u128.into()); } @@ -480,25 +480,24 @@ mod staker_staked_seconds_per_total_staked_calculation { token.approve(staker.contract_address, 7); staker.stake(delegatee); // Will transfer 7 token to contract account and setup delegatee - assert(staker.get_cumulative_seconds_per_total_staked_at(0) == 0_u64.into(), 'At 0 should be 0'); - assert(staker.get_cumulative_seconds_per_total_staked_at(500) == 0_u64.into(), 'At 500 should be 0'); - assert(staker.get_cumulative_seconds_per_total_staked_at(999) == 0_u64.into(), 'At 999 should be 0'); + assert_fp(staker.get_cumulative_seconds_per_total_staked_at(0), 0, 0_u128); + assert_fp(staker.get_cumulative_seconds_per_total_staked_at(500), 0, 0_u128); + assert_fp(staker.get_cumulative_seconds_per_total_staked_at(999), 0, 0_u128); assert_fp(staker.get_cumulative_seconds_per_total_staked_at(1000), 0, 0x80000000000000000000000000000000_u128); assert_fp(staker.get_cumulative_seconds_per_total_staked_at(2000), 1, 0_u128); assert_fp(staker.get_cumulative_seconds_per_total_staked_at(3000), 1, 0x80000000000000000000000000000000_u128); assert_fp(staker.get_cumulative_seconds_per_total_staked_at(4000), 2, 0_u128); - assert_fp(staker.get_cumulative_seconds_per_total_staked_at(5000), 2, 0x80000000000000000000000000000000_u128); - // // NOTE: After 5s value stops changing as nothing is staked. @Moody is that a desired behaviour? - // assert_fp(staker.get_cumulative_seconds_per_total_staked_at(6000), 2, 0x80000000000000000000000000000000_u128); - // assert_fp(staker.get_cumulative_seconds_per_total_staked_at(7000), 2, 0x80000000000000000000000000000000_u128); - // assert_fp(staker.get_cumulative_seconds_per_total_staked_at(8000), 2, 0x80000000000000000000000000000000_u128); - // assert_fp(staker.get_cumulative_seconds_per_total_staked_at(9000), 2, 0x80000000000000000000000000000000_u128); - // assert_fp(staker.get_cumulative_seconds_per_total_staked_at(10000), 2, 0x80000000000000000000000000000000_u128); - // // 7 were staked here - // assert_fp(staker.get_cumulative_seconds_per_total_staked_at(17000), 3, 0x80000000000000000000000000000000_u128); - // assert_fp(staker.get_cumulative_seconds_per_total_staked_at(24000), 4, 0x80000000000000000000000000000000_u128); + // here value is undefined as nothing was staked. + assert_fp(staker.get_cumulative_seconds_per_total_staked_at(5000), 0, 0_u128); + assert_fp(staker.get_cumulative_seconds_per_total_staked_at(6000), 0, 0_u128); + assert_fp(staker.get_cumulative_seconds_per_total_staked_at(7000), 0, 0_u128); + assert_fp(staker.get_cumulative_seconds_per_total_staked_at(8000), 0, 0_u128); + assert_fp(staker.get_cumulative_seconds_per_total_staked_at(9000), 0, 0_u128); + assert_fp(staker.get_cumulative_seconds_per_total_staked_at(10000), 2, 0x80000000000000000000000000000000_u128); + assert_fp(staker.get_cumulative_seconds_per_total_staked_at(17000), 3, 0x80000000000000000000000000000000_u128); + assert_fp(staker.get_cumulative_seconds_per_total_staked_at(24000), 4, 0x80000000000000000000000000000000_u128); } } \ No newline at end of file From a28971ca5c6e7bbfbe666cda63818eab57269099 Mon Sep 17 00:00:00 2001 From: baitcode Date: Fri, 10 Jan 2025 01:09:59 +0300 Subject: [PATCH 26/40] final debugging --- src/lib.cairo | 6 +++--- src/staker.cairo | 4 +--- src/staker_log.cairo | 3 +++ src/staker_test.cairo | 3 ++- src/utils/fp.cairo | 31 +++++++++++++++++-------------- src/utils/fp_test.cairo | 32 +++++++++++++++++++++++++++++--- 6 files changed, 55 insertions(+), 24 deletions(-) diff --git a/src/lib.cairo b/src/lib.cairo index 8cdcbef..66d5c02 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -1,7 +1,7 @@ pub mod airdrop; mod airdrop_claim_check; #[cfg(test)] -// mod airdrop_test; +mod airdrop_test; pub mod call_trait; #[cfg(test)] @@ -12,8 +12,8 @@ pub mod execution_state; mod execution_state_test; pub mod governor; -// #[cfg(test)] -// mod governor_test; +#[cfg(test)] +mod governor_test; pub mod staker; #[cfg(test)] diff --git a/src/staker.cairo b/src/staker.cairo index 13e931e..eadf76a 100644 --- a/src/staker.cairo +++ b/src/staker.cairo @@ -236,7 +236,7 @@ use super::super::staker_log::LogOperations; #[abi(embed_v0)] - impl StakerImpl of IStaker { + impl StakerImpl of IStaker { fn get_token(self: @ContractState) -> ContractAddress { self.token.read().contract_address } @@ -276,9 +276,7 @@ use super::super::staker_log::LogOperations; .write(delegate, self.insert_snapshot(delegate, get_block_timestamp()) + amount); let total_staked = self.total_staked.read(); - assert(total_staked + amount >= total_staked, 'BAD AMOUNT'); - self.total_staked.write(total_staked + amount); self.staking_log.log_change(amount, total_staked); diff --git a/src/staker_log.cairo b/src/staker_log.cairo index a14419d..24d6798 100644 --- a/src/staker_log.cairo +++ b/src/staker_log.cairo @@ -19,6 +19,7 @@ pub type StakingLog = Vec; const TWO_POW_32: u64 = 0x100000000_u64; const MASK_32_BITS: u128 = 0x100000000_u128 - 1; +const TWO_POW_160: u256 = 0x10000000000000000000000000000000000000000; #[derive(Drop, Serde, Copy)] pub(crate) struct StakingLogRecord { @@ -108,6 +109,8 @@ pub impl StakingLogOperations of LogOperations { div_u64_by_u128(seconds_diff, total_staked) }; + // assert(last_record.cumulative_total_staked + total_staked_by_elapsed_seconds < TWO_POW_160, 'TOTAL_STAKED_OVERFLOW'); + // Add a new record. record.write( StakingLogRecord { diff --git a/src/staker_test.cairo b/src/staker_test.cairo index 00b426f..d1da0ec 100644 --- a/src/staker_test.cairo +++ b/src/staker_test.cairo @@ -5,6 +5,7 @@ use governance::staker::{ IStakerDispatcher, IStakerDispatcherTrait, Staker, Staker::{DelegatedSnapshot, DelegatedSnapshotStorePacking}, }; + use governance::test::test_token::{TestToken, deploy as deploy_token}; use starknet::testing::{pop_log, set_block_timestamp}; use starknet::{ @@ -452,7 +453,6 @@ mod staker_staked_seconds_per_total_staked_calculation { assert_eq!(value.get_fractional(), fractional); } - #[test] fn test_should_stake_10000_tokens_for_5_seconds_adding_10000_every_second_to_staked_seconds() { let (staker, token) = setup(1000); @@ -474,6 +474,7 @@ mod staker_staked_seconds_per_total_staked_calculation { assert(staker.get_staked(token_owner, delegatee) == 2, 'Something went wrong'); staker.withdraw(delegatee, token_owner); // Will withdraw all 10 tokens back to owner + assert(staker.get_staked(delegatee, token_owner) == 0, 'Not all tokens were withdrawn'); set_block_timestamp(10000); diff --git a/src/utils/fp.cairo b/src/utils/fp.cairo index d15bde7..41eda4d 100644 --- a/src/utils/fp.cairo +++ b/src/utils/fp.cairo @@ -6,7 +6,7 @@ pub const EPSILON: u256 = 0x10_u256; // 2^124 pub const MAX_INT: u128 = 0x10000000000000000000000000000000_u128; -pub const HALF: u128 = 0x80000000000000000000000000000000_u128; +pub const HALF: u128 = 0x80000000000000000000000000000000_u128; // 124.128 (= 252 which 1 felt exactly) #[derive(Debug, Drop, Copy, Serde)] @@ -15,7 +15,12 @@ pub struct UFixedPoint124x128 { } pub mod Errors { - pub const INTEGER_OVERFLOW: felt252 = 'INTEGER_OVERFLOW'; + pub const FP_ADD_OVERFLOW: felt252 = 'FP_ADD_OVERFLOW'; + pub const FP_SUB_OVERFLOW: felt252 = 'FP_SUB_OVERFLOW'; + pub const FP_MUL_OVERFLOW: felt252 = 'FP_MUL_OVERFLOW'; + pub const FP_DIV_OVERFLOW: felt252 = 'FP_DIV_OVERFLOW'; + pub const FP_SUB_UNDERFLOW: felt252 = 'FP_SUB_UNDERFLOW'; + pub const DIVISION_BY_ZERO: felt252 = 'DIVISION_BY_ZERO'; } @@ -88,10 +93,6 @@ pub impl UFixedPoint124x128Impl of UFixedPointTrait { 0 } } - - fn validate(self: UFixedPoint124x128) { - assert(self.value.high < MAX_INT, Errors::INTEGER_OVERFLOW); - } } pub(crate) impl UFixedPoint124x128IntoFelt252 of TryInto { @@ -102,10 +103,13 @@ pub(crate) impl UFixedPoint124x128IntoFelt252 of TryInto { fn add(lhs: UFixedPoint124x128, rhs: UFixedPoint124x128) -> UFixedPoint124x128 { + assert(rhs.value <= (rhs.value + lhs.value), Errors::FP_ADD_OVERFLOW); + assert(lhs.value <= rhs.value + lhs.value, Errors::FP_ADD_OVERFLOW); + let res = UFixedPoint124x128 { value: rhs.value + lhs.value }; - res.validate(); + assert(res.value.high < MAX_INT, Errors::FP_ADD_OVERFLOW); res } @@ -113,12 +117,13 @@ pub impl UFixedPoint124x128ImplAdd of Add { pub impl UFixedPoint124x128ImplSub of Sub { fn sub(lhs: UFixedPoint124x128, rhs: UFixedPoint124x128) -> UFixedPoint124x128 { + assert(lhs.value >= rhs.value, Errors::FP_SUB_UNDERFLOW); // TODO: underflow checking let res = UFixedPoint124x128 { value: lhs.value - rhs.value }; - res.validate(); - + assert(res.value.high < MAX_INT, Errors::FP_SUB_OVERFLOW); + res } } @@ -146,7 +151,7 @@ pub impl UFixedPoint124x128ImplDiv of Div { } }; - res.validate(); + assert(res.value.high < MAX_INT, Errors::FP_DIV_OVERFLOW); res } @@ -165,7 +170,7 @@ pub fn div_u64_by_u128(lhs: u64, rhs: u128) -> UFixedPoint124x128 { value: left / rhs.into() }; - res.validate(); + assert(res.value.high < MAX_INT, Errors::FP_DIV_OVERFLOW); res } @@ -178,8 +183,6 @@ pub fn div_u64_by_fixed_point(lhs: u64, rhs: UFixedPoint124x128) -> UFixedPoint1 pub fn mul_fp_by_u128(lhs: UFixedPoint124x128, rhs: u128) -> UFixedPoint124x128 { let mult_res = lhs.value.wide_mul(rhs.into()); - - // TODO: add overflow check let res = UFixedPoint124x128 { value: u256 { @@ -188,7 +191,7 @@ pub fn mul_fp_by_u128(lhs: UFixedPoint124x128, rhs: u128) -> UFixedPoint124x128 } }; - res.validate(); + assert(res.value.high < MAX_INT, Errors::FP_MUL_OVERFLOW); res } diff --git a/src/utils/fp_test.cairo b/src/utils/fp_test.cairo index 63e2536..a7104b2 100644 --- a/src/utils/fp_test.cairo +++ b/src/utils/fp_test.cairo @@ -22,13 +22,13 @@ pub(crate) impl U64IntoUFixedPoint of Into { #[test] -fn test_add() { +fn test_add() { let f1 : UFixedPoint124x128 = 0xFFFFFFFFFFFFFFFF_u64.into(); let f2 : UFixedPoint124x128 = 1_u64.into(); let res = f1 + f2; let z: u256 = res.into(); assert_eq!(z.low, 0); - assert_eq!(z.high, 18446744073709551616); + assert_eq!(z.high, 0x10000000000000000); } #[test] @@ -60,7 +60,7 @@ fn test_mul() { } #[test] -#[should_panic(expected: 'INTEGER_OVERFLOW')] +#[should_panic(expected: 'FP_MUL_OVERFLOW')] fn test_multiplication_overflow() { let f1 = MAX_INT - 1; let f2 = MAX_INT - 1; @@ -198,8 +198,34 @@ fn test_division_and_multiplication_by() { run_division_and_multiplication_test(0xdc7a02e89502151c, 0x98072c1c94111cfd1ed52d396b35d1a, 0xd19d22fc5f018bc7, 0x12, 0xffd594dfa4115e06c34d6ca0416012b4); } +#[test] +#[should_panic(expected: 'FP_DIV_OVERFLOW')] +fn test_division_overflow() { + // 1 / 2**124 + let fp1 = div_u64_by_u128(1, MAX_INT); + div_u64_by_fixed_point(1, fp1); +} + #[test] #[should_panic(expected: 'DIVISION_BY_ZERO')] fn test_division_by_zero() { run_division_test(56, 0, 0, 0); } + +#[test] +fn test_should_work_fine_with_zeroes() { + let f1 = div_u64_by_u128(5_u64, 2_u128).into(); + let f2: UFixedPoint124x128 = 0.into(); + + let res = f1 + f2; + assert_eq!(res, f1); + + let res = f1 - f2; + assert_eq!(res, f1); + + let res = f1 - f1; + assert_eq!(res, 0.into()); + + let res = f2 - f2; + assert_eq!(res, 0.into()); +} From aa078011c1c2200dd1f833a8fc8b9aaec828921f Mon Sep 17 00:00:00 2001 From: baitcode Date: Fri, 10 Jan 2025 01:17:56 +0300 Subject: [PATCH 27/40] cleanup --- src/lib.cairo | 2 +- src/staker.cairo | 12 ++---------- src/staker_log.cairo | 2 +- src/staker_log_test.cairo | 2 +- src/staker_test.cairo | 9 +-------- 5 files changed, 6 insertions(+), 21 deletions(-) diff --git a/src/lib.cairo b/src/lib.cairo index 66d5c02..34688ce 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -23,13 +23,13 @@ pub mod staker_log; #[cfg(test)] pub mod staker_log_test; - mod interfaces { pub(crate) mod erc20; } mod utils { pub(crate) mod exp2; pub(crate) mod fp; + #[cfg(test)] pub(crate) mod fp_test; } diff --git a/src/staker.cairo b/src/staker.cairo index eadf76a..9c5e66a 100644 --- a/src/staker.cairo +++ b/src/staker.cairo @@ -1,7 +1,6 @@ use starknet::{ContractAddress}; use crate::utils::fp::{UFixedPoint124x128}; - #[starknet::interface] pub trait IStaker { // Returns the token this staker references. @@ -57,14 +56,11 @@ pub trait IStaker { // Gets the cumulative staked amount * per second staked for the given timestamp and account. fn get_cumulative_seconds_per_total_staked_at(self: @TContractState, timestamp: u64) -> UFixedPoint124x128; - } - #[starknet::contract] pub mod Staker { use starknet::storage::VecTrait; -use super::super::staker_log::LogOperations; use core::num::traits::zero::{Zero}; use governance::interfaces::erc20::{IERC20Dispatcher, IERC20DispatcherTrait}; use starknet::storage::{ @@ -75,7 +71,7 @@ use super::super::staker_log::LogOperations; UFixedPoint124x128, UFixedPoint124x128Zero, div_u64_by_u128, div_u64_by_fixed_point, UFixedPoint124x128Impl, }; - use crate::staker_log::{StakingLog}; + use crate::staker_log::{StakingLog, LogOperations}; use starknet::{ get_block_timestamp, get_caller_address, get_contract_address, @@ -114,13 +110,11 @@ use super::super::staker_log::LogOperations; #[storage] struct Storage { token: IERC20Dispatcher, - // owner, delegate => amount staked: Map<(ContractAddress, ContractAddress), u128>, amount_delegated: Map, delegated_cumulative_num_snapshots: Map, delegated_cumulative_snapshot: Map>, - total_staked: u128, staking_log: StakingLog, } @@ -231,12 +225,10 @@ use super::super::staker_log::LogOperations; self.find_delegated_cumulative(delegate, mid, max_index_exclusive, timestamp) } } - } - #[abi(embed_v0)] - impl StakerImpl of IStaker { + impl StakerImpl of IStaker { fn get_token(self: @ContractState) -> ContractAddress { self.token.read().contract_address } diff --git a/src/staker_log.cairo b/src/staker_log.cairo index 24d6798..65a2118 100644 --- a/src/staker_log.cairo +++ b/src/staker_log.cairo @@ -232,4 +232,4 @@ pub(crate) impl StakingLogRecordSubPointersImpl of SubPointers cumulative_seconds_per_total_staked: cumulative_seconds_per_total_staked_ptr, } } -} \ No newline at end of file +} diff --git a/src/staker_log_test.cairo b/src/staker_log_test.cairo index f0760a0..8bc11b1 100644 --- a/src/staker_log_test.cairo +++ b/src/staker_log_test.cairo @@ -31,4 +31,4 @@ fn test_staking_log_packing() { assert_packs_and_unpacks(0_u64, 0_u256); assert_packs_and_unpacks(10_u64, 50_u256); assert_packs_and_unpacks(0xffffffffffffffff_u64, 0xffffffffffffffffffffffffffffffffffffffff_u256) -} \ No newline at end of file +} diff --git a/src/staker_test.cairo b/src/staker_test.cairo index d1da0ec..ea5f84d 100644 --- a/src/staker_test.cairo +++ b/src/staker_test.cairo @@ -17,11 +17,8 @@ use starknet::{ pub(crate) fn setup(amount: u256) -> (IStakerDispatcher, IERC20Dispatcher) { let token = deploy_token(get_contract_address(), amount); - - let class_hash: ClassHash = Staker::TEST_CLASS_HASH.try_into().unwrap(); - let (staker_address, _) = deploy_syscall( - class_hash, + Staker::TEST_CLASS_HASH.try_into().unwrap(), 0, array![token.contract_address.into()].span(), true, @@ -412,7 +409,6 @@ mod staker_staked_seconds_per_total_staked_calculation { IERC20DispatcherTrait, IStakerDispatcherTrait, }; - #[test] fn test_should_return_0_if_no_data_found() { let (staker, _) = setup(10000); @@ -420,7 +416,6 @@ mod staker_staked_seconds_per_total_staked_calculation { assert_fp(staker.get_cumulative_seconds_per_total_staked_at(1000), 0, 0_u128.into()); } - #[test] #[should_panic(expected: ('INSUFFICIENT_AMOUNT_STAKED', 'ENTRYPOINT_FAILED'))] fn test_raises_error_if_no_history_exists_and_withdrawal_happens() { @@ -446,7 +441,6 @@ mod staker_staked_seconds_per_total_staked_calculation { set_block_timestamp(4000); staker.withdraw_amount(delegatee, token_owner, 2000); } - fn assert_fp(value: UFixedPoint124x128, integer: u128, fractional: u128) { assert_eq!(value.get_integer(), integer); @@ -500,5 +494,4 @@ mod staker_staked_seconds_per_total_staked_calculation { assert_fp(staker.get_cumulative_seconds_per_total_staked_at(17000), 3, 0x80000000000000000000000000000000_u128); assert_fp(staker.get_cumulative_seconds_per_total_staked_at(24000), 4, 0x80000000000000000000000000000000_u128); } - } \ No newline at end of file From 61f085cb9a2eca4b4adb62fec3f57eead27e8f0e Mon Sep 17 00:00:00 2001 From: baitcode Date: Fri, 10 Jan 2025 01:18:49 +0300 Subject: [PATCH 28/40] cleanup --- declare-all.sh | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/declare-all.sh b/declare-all.sh index 4cb9642..e9fb4c7 100755 --- a/declare-all.sh +++ b/declare-all.sh @@ -37,11 +37,7 @@ scarb build declare_class_hash() { local class_name=$1 - starkli declare --watch \ - --network "$NETWORK" \ - --keystore-password "$STARKNET_KEYSTORE_PASSWORD" \ - --casm-file "target/dev/governance_${class_name}.compiled_contract_class.json" \ - "target/dev/governance_${class_name}.contract_class.json" + starkli declare --watch --network "$NETWORK" --keystore-password "$STARKNET_KEYSTORE_PASSWORD" --casm-file "target/dev/governance_${class_name}.compiled_contract_class.json" "target/dev/governance_${class_name}.contract_class.json" } echo "Declaring AirdropClaimCheck" From f52fff1201722b912157aec9b4dddbc4b8ec222c Mon Sep 17 00:00:00 2001 From: baitcode Date: Fri, 10 Jan 2025 13:21:45 +0300 Subject: [PATCH 29/40] removing UFixedPoint124x128 struct --- src/staker.cairo | 14 ++- src/staker_test.cairo | 1 - src/utils/fp.cairo | 205 +++++++++++++--------------------------- src/utils/fp_test.cairo | 35 ++++--- 4 files changed, 99 insertions(+), 156 deletions(-) diff --git a/src/staker.cairo b/src/staker.cairo index 9c5e66a..e6ec12e 100644 --- a/src/staker.cairo +++ b/src/staker.cairo @@ -68,8 +68,8 @@ pub mod Staker { StoragePointerReadAccess, StoragePointerWriteAccess, }; use crate::utils::fp::{ - UFixedPoint124x128, UFixedPoint124x128Zero, div_u64_by_u128, div_u64_by_fixed_point, - UFixedPoint124x128Impl, + UFixedPoint124x128, div_u64_by_u128, div_u64_by_fixed_point, + UFixedPoint124x128Impl, sub_fixed_points, add_fixed_points }; use crate::staker_log::{StakingLog, LogOperations}; @@ -368,7 +368,10 @@ pub mod Staker { // otherwise calculate using cumulative_seconds_per_total_staked difference let next_log_record = self.staking_log.at(idx+1).read(); - let divisor = next_log_record.cumulative_seconds_per_total_staked - log_record.cumulative_seconds_per_total_staked; + let divisor = sub_fixed_points( + next_log_record.cumulative_seconds_per_total_staked, + log_record.cumulative_seconds_per_total_staked + ); if divisor.is_zero() { return 0_u64.into(); @@ -391,7 +394,10 @@ pub mod Staker { div_u64_by_u128(seconds_diff, total_staked) }; - return log_record.cumulative_seconds_per_total_staked + staked_seconds; + return add_fixed_points( + log_record.cumulative_seconds_per_total_staked, + staked_seconds + ); } return 0_u64.into(); diff --git a/src/staker_test.cairo b/src/staker_test.cairo index ea5f84d..a5b501d 100644 --- a/src/staker_test.cairo +++ b/src/staker_test.cairo @@ -9,7 +9,6 @@ use governance::staker::{ use governance::test::test_token::{TestToken, deploy as deploy_token}; use starknet::testing::{pop_log, set_block_timestamp}; use starknet::{ - ClassHash, contract_address_const, get_contract_address, syscalls::deploy_syscall diff --git a/src/utils/fp.cairo b/src/utils/fp.cairo index 41eda4d..4a3d471 100644 --- a/src/utils/fp.cairo +++ b/src/utils/fp.cairo @@ -8,11 +8,7 @@ pub const EPSILON: u256 = 0x10_u256; pub const MAX_INT: u128 = 0x10000000000000000000000000000000_u128; pub const HALF: u128 = 0x80000000000000000000000000000000_u128; -// 124.128 (= 252 which 1 felt exactly) -#[derive(Debug, Drop, Copy, Serde)] -pub struct UFixedPoint124x128 { - value: u256 -} +pub type UFixedPoint124x128 = u256; pub mod Errors { pub const FP_ADD_OVERFLOW: felt252 = 'FP_ADD_OVERFLOW'; @@ -34,143 +30,66 @@ pub impl UFixedPoint124x128StorePacking of StorePacking { - fn eq(lhs: @UFixedPoint124x128, rhs: @UFixedPoint124x128) -> bool { - let left: u256 = (*lhs).value; - let right: u256 = (*rhs).value; - - let diff = if left > right { - left - right - } else { - right - left - }; - - diff < EPSILON - } -} +#[generate_trait] +pub impl UFixedPoint124x128Impl of UFixedPointTrait { + fn get_integer(self: UFixedPoint124x128) -> u128 { self.high } + fn get_fractional(self: UFixedPoint124x128) -> u128 { self.low } -pub impl UFixedPoint124x128Zero of Zero { - fn zero() -> UFixedPoint124x128 { - UFixedPoint124x128 { - value: u256 { - low: 0, - high: 0, - } + fn from_u64(value: u64) -> UFixedPoint124x128 { + u256 { + low: 0, + high: value.into(), } } - fn is_zero(self: @UFixedPoint124x128) -> bool { - self.value.is_zero() - } - - fn is_non_zero(self: @UFixedPoint124x128) -> bool { !self.is_zero() } -} - -pub(crate) impl U256IntoUFixedPoint of Into { - fn into(self: u256) -> UFixedPoint124x128 { UFixedPoint124x128 { value: self } } -} - -pub(crate) impl UFixedPointIntoU256 of Into { - fn into(self: UFixedPoint124x128) -> u256 { self.value } -} - -pub(crate) impl Felt252IntoUFixedPoint of Into { - fn into(self: felt252) -> UFixedPoint124x128 { - let medium: u256 = self.into(); - medium.into() + fn from_u128(value: u128) -> UFixedPoint124x128 { + u256 { + low: 0, + high: value, + } } -} - -#[generate_trait] -pub impl UFixedPoint124x128Impl of UFixedPointTrait { - fn get_integer(self: UFixedPoint124x128) -> u128 { self.value.high } - fn get_fractional(self: UFixedPoint124x128) -> u128 { self.value.low } fn round(self: UFixedPoint124x128) -> u128 { - self.get_integer() + if (self.get_fractional() >= HALF) { + let overflow = if (self.get_fractional() >= HALF) { 1 } else { 0 - } - } -} - -pub(crate) impl UFixedPoint124x128IntoFelt252 of TryInto { - fn try_into(self: UFixedPoint124x128) -> Option { - self.value.try_into() - } -} - -pub impl UFixedPoint124x128ImplAdd of Add { - fn add(lhs: UFixedPoint124x128, rhs: UFixedPoint124x128) -> UFixedPoint124x128 { - assert(rhs.value <= (rhs.value + lhs.value), Errors::FP_ADD_OVERFLOW); - assert(lhs.value <= rhs.value + lhs.value, Errors::FP_ADD_OVERFLOW); - - let res = UFixedPoint124x128 { - value: rhs.value + lhs.value }; - assert(res.value.high < MAX_INT, Errors::FP_ADD_OVERFLOW); - - res + self.get_integer() + overflow } } -pub impl UFixedPoint124x128ImplSub of Sub { - fn sub(lhs: UFixedPoint124x128, rhs: UFixedPoint124x128) -> UFixedPoint124x128 { - assert(lhs.value >= rhs.value, Errors::FP_SUB_UNDERFLOW); - // TODO: underflow checking - let res = UFixedPoint124x128 { - value: lhs.value - rhs.value - }; - assert(res.value.high < MAX_INT, Errors::FP_SUB_OVERFLOW); +pub fn div_u64_by_u128(lhs: u64, rhs: u128) -> UFixedPoint124x128 { + assert(!rhs.is_zero(), Errors::DIVISION_BY_ZERO); - res - } -} + let res = UFixedPoint124x128Impl::from_u64(lhs) / rhs.into(); -pub impl UFixedPoint124x128ImplDiv of Div { - fn div(lhs: UFixedPoint124x128, rhs: UFixedPoint124x128) -> UFixedPoint124x128 { - let left: u512 = u512 { - limb0: 0, - limb1: 0, - limb2: lhs.value.low, - limb3: lhs.value.high, - }; - - assert(rhs.value != 0, Errors::DIVISION_BY_ZERO); - - let (div_res, _) = u512_safe_div_rem_by_u256( - left, - rhs.value.try_into().unwrap(), - ); - - let res = UFixedPoint124x128 { - value: u256 { - low: div_res.limb1, - high: div_res.limb2, - } - }; - - assert(res.value.high < MAX_INT, Errors::FP_DIV_OVERFLOW); + assert(res.high < MAX_INT, Errors::FP_DIV_OVERFLOW); - res - } + res } -pub fn div_u64_by_u128(lhs: u64, rhs: u128) -> UFixedPoint124x128 { - assert(!rhs.is_zero(), Errors::DIVISION_BY_ZERO); - - // lhs >> 128 - let left: u256 = u256 { - low: 0, - high: lhs.into(), +pub fn div_fixed_point_by_fixed_point(lhs: UFixedPoint124x128, rhs: UFixedPoint124x128) -> UFixedPoint124x128 { + let left: u512 = u512 { + limb0: 0, + limb1: 0, + limb2: lhs.low, + limb3: lhs.high, }; - - let res = UFixedPoint124x128 { - value: left / rhs.into() + + assert(rhs != 0, Errors::DIVISION_BY_ZERO); + + let (div_res, _) = u512_safe_div_rem_by_u256( + left, + rhs.try_into().unwrap(), + ); + + let res = u256 { + low: div_res.limb1, + high: div_res.limb2, }; - - assert(res.value.high < MAX_INT, Errors::FP_DIV_OVERFLOW); + + assert(res.high < MAX_INT, Errors::FP_DIV_OVERFLOW); res } @@ -178,31 +97,41 @@ pub fn div_u64_by_u128(lhs: u64, rhs: u128) -> UFixedPoint124x128 { pub fn div_u64_by_fixed_point(lhs: u64, rhs: UFixedPoint124x128) -> UFixedPoint124x128 { assert(!rhs.is_zero(), Errors::DIVISION_BY_ZERO); - lhs.into() / rhs + div_fixed_point_by_fixed_point( + UFixedPoint124x128Impl::from_u64(lhs), + rhs + ) } -pub fn mul_fp_by_u128(lhs: UFixedPoint124x128, rhs: u128) -> UFixedPoint124x128 { - let mult_res = lhs.value.wide_mul(rhs.into()); +pub fn mul_fixed_point_by_u128(lhs: UFixedPoint124x128, rhs: u128) -> UFixedPoint124x128 { + let mult_res = lhs.wide_mul(rhs.into()); let res = UFixedPoint124x128 { - value: u256 { - low: mult_res.limb0, - high: mult_res.limb1, - } + low: mult_res.limb0, + high: mult_res.limb1, }; - assert(res.value.high < MAX_INT, Errors::FP_MUL_OVERFLOW); + assert(res.high < MAX_INT, Errors::FP_MUL_OVERFLOW); res } -pub(crate) impl U64IntoUFixedPoint of Into { - fn into(self: u64) -> UFixedPoint124x128 { - UFixedPoint124x128 { - value: u256 { - low: 0, // fractional - high: self.into(), // integer - } - } - } +pub fn add_fixed_points(lhs: UFixedPoint124x128, rhs: UFixedPoint124x128) -> UFixedPoint124x128 { + assert(rhs <= rhs + lhs, Errors::FP_ADD_OVERFLOW); + assert(lhs <= rhs + lhs, Errors::FP_ADD_OVERFLOW); + + let res = rhs + lhs; + + assert(res.high < MAX_INT, Errors::FP_ADD_OVERFLOW); + + res +} + +pub fn sub_fixed_points(lhs: UFixedPoint124x128, rhs: UFixedPoint124x128) -> UFixedPoint124x128 { + assert(lhs >= rhs, Errors::FP_SUB_UNDERFLOW); + // TODO: underflow checking + let res = lhs - rhs; + assert(res.high < MAX_INT, Errors::FP_SUB_OVERFLOW); + + res } diff --git a/src/utils/fp_test.cairo b/src/utils/fp_test.cairo index a7104b2..4f05f5f 100644 --- a/src/utils/fp_test.cairo +++ b/src/utils/fp_test.cairo @@ -3,7 +3,8 @@ use super::fp::UFixedPointTrait; use crate::utils::fp::{ UFixedPoint124x128, - div_u64_by_u128, mul_fp_by_u128, div_u64_by_fixed_point, + UFixedPoint124x128Impl, + div_u64_by_u128, mul_fixed_point_by_u128, div_u64_by_fixed_point, MAX_INT }; @@ -23,22 +24,22 @@ pub(crate) impl U64IntoUFixedPoint of Into { #[test] fn test_add() { - let f1 : UFixedPoint124x128 = 0xFFFFFFFFFFFFFFFF_u64.into(); - let f2 : UFixedPoint124x128 = 1_u64.into(); + let f1 : UFixedPoint124x128 = UFixedPoint124x128Impl::from_u64(0xFFFFFFFFFFFFFFFF_u64); + let f2 : UFixedPoint124x128 = UFixedPoint124x128Impl::from_u64(1_u64); + let res = f1 + f2; - let z: u256 = res.into(); - assert_eq!(z.low, 0); - assert_eq!(z.high, 0x10000000000000000); + + assert_eq!(res.low, 0); + assert_eq!(res.high, 0x10000000000000000); } #[test] fn test_fp_value_mapping() { - let f1 : UFixedPoint124x128 = 7_u64.into(); + let f1 : UFixedPoint124x128 = UFixedPoint124x128Impl::from_u64(7_u64); assert_eq!(f1.get_fractional(), 0x0); assert_eq!(f1.get_integer(), 0x7); - let val: u256 = f1.into(); - assert_eq!(val, 7_u256*0x100000000000000000000000000000000); + assert_eq!(f1, 7_u256*0x100000000000000000000000000000000); } @@ -54,7 +55,11 @@ fn test_mul() { assert_eq!(expected.limb2, 49); assert_eq!(expected.limb3, 0); - let res: u256 = mul_fp_by_u128(f1.into(), f2.try_into().unwrap()).into(); + let res: u256 = mul_fixed_point_by_u128( + UFixedPoint124x128Impl::from_u64(f1), + f2.try_into().unwrap() + ).into(); + assert_eq!(res.high, 49); assert_eq!(res.low, 0); } @@ -64,7 +69,10 @@ fn test_mul() { fn test_multiplication_overflow() { let f1 = MAX_INT - 1; let f2 = MAX_INT - 1; - let _ = mul_fp_by_u128(f1.into(), f2.try_into().unwrap()); + let _ = mul_fixed_point_by_u128( + UFixedPoint124x128Impl::from_u128(f1), + f2.try_into().unwrap() + ); } #[test] @@ -74,7 +82,8 @@ fn test_u256_conversion() { assert_eq!(f.low, 0x00112233445566778899AABBCCDDEEFF); assert_eq!(f.high, 0x0123456789ABCDEFFEDCBA9876543210); - let fp: UFixedPoint124x128 = f.into(); + let fp: UFixedPoint124x128 = f; + assert_eq!(fp.get_integer(), f.high); assert_eq!(fp.get_fractional(), f.low); } @@ -88,7 +97,7 @@ fn run_division_test(left: u64, right: u128, expected_int: u128, expected_frac: fn run_division_and_multiplication_test(numenator: u64, divisor: u128, mult: u128, expected_int: u128, expected_frac: u128) { let divided = div_u64_by_u128(numenator, divisor); - let res = mul_fp_by_u128(divided, mult); + let res = mul_fixed_point_by_u128(divided, mult); assert_eq!(res.get_integer(), expected_int); assert_eq!(res.get_fractional(), expected_frac); From 4cc807e5a5f610027b62e3f573f21fce509e1b56 Mon Sep 17 00:00:00 2001 From: baitcode Date: Fri, 10 Jan 2025 22:58:55 +0300 Subject: [PATCH 30/40] BugFix for a case when fetl252 is overflown --- src/utils/fp.cairo | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/utils/fp.cairo b/src/utils/fp.cairo index 4a3d471..5337c66 100644 --- a/src/utils/fp.cairo +++ b/src/utils/fp.cairo @@ -5,7 +5,7 @@ use core::integer::{u512, u512_safe_div_rem_by_u256 }; pub const EPSILON: u256 = 0x10_u256; // 2^124 -pub const MAX_INT: u128 = 0x10000000000000000000000000000000_u128; +pub const MAX_INT: u128 = 0x8000000000000110000000000000000_u128; pub const HALF: u128 = 0x80000000000000000000000000000000_u128; pub type UFixedPoint124x128 = u256; @@ -18,6 +18,7 @@ pub mod Errors { pub const FP_SUB_UNDERFLOW: felt252 = 'FP_SUB_UNDERFLOW'; pub const DIVISION_BY_ZERO: felt252 = 'DIVISION_BY_ZERO'; + pub const MAX_OVERFLOW: felt252 = 'MAX_OVERFLOW'; } pub impl UFixedPoint124x128StorePacking of StorePacking { @@ -43,10 +44,14 @@ pub impl UFixedPoint124x128Impl of UFixedPointTrait { } fn from_u128(value: u128) -> UFixedPoint124x128 { - u256 { + let res = u256 { low: 0, high: value, - } + }; + + assert(res.high < MAX_INT, Errors::MAX_OVERFLOW); + + res } fn round(self: UFixedPoint124x128) -> u128 { From 7e3513785a22382c6a77087a497cb668407d7531 Mon Sep 17 00:00:00 2001 From: baitcode Date: Sat, 11 Jan 2025 02:34:11 +0300 Subject: [PATCH 31/40] remove fixed point lib inline everything remove storage read optimisations --- src/lib.cairo | 18 ++--- src/staker.cairo | 70 +++++++++++-------- src/staker_log.cairo | 142 +++++++++++--------------------------- src/staker_log_test.cairo | 16 ++--- src/staker_test.cairo | 8 +-- 5 files changed, 100 insertions(+), 154 deletions(-) diff --git a/src/lib.cairo b/src/lib.cairo index 34688ce..1c3eaf8 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -1,7 +1,7 @@ pub mod airdrop; mod airdrop_claim_check; -#[cfg(test)] -mod airdrop_test; +// #[cfg(test)] +// mod airdrop_test; pub mod call_trait; #[cfg(test)] @@ -12,26 +12,22 @@ pub mod execution_state; mod execution_state_test; pub mod governor; -#[cfg(test)] -mod governor_test; +// #[cfg(test)] +// mod governor_test; pub mod staker; #[cfg(test)] mod staker_test; pub mod staker_log; -#[cfg(test)] -pub mod staker_log_test; +// #[cfg(test)] +// pub mod staker_log_test; mod interfaces { pub(crate) mod erc20; } mod utils { - pub(crate) mod exp2; - pub(crate) mod fp; - - #[cfg(test)] - pub(crate) mod fp_test; + pub(crate) mod exp2; } #[cfg(test)] diff --git a/src/staker.cairo b/src/staker.cairo index e6ec12e..e953c16 100644 --- a/src/staker.cairo +++ b/src/staker.cairo @@ -1,5 +1,4 @@ use starknet::{ContractAddress}; -use crate::utils::fp::{UFixedPoint124x128}; #[starknet::interface] pub trait IStaker { @@ -55,23 +54,20 @@ pub trait IStaker { ) -> u128; // Gets the cumulative staked amount * per second staked for the given timestamp and account. - fn get_cumulative_seconds_per_total_staked_at(self: @TContractState, timestamp: u64) -> UFixedPoint124x128; + fn get_cumulative_seconds_per_total_staked_at(self: @TContractState, timestamp: u64) -> u256; } #[starknet::contract] pub mod Staker { use starknet::storage::VecTrait; use core::num::traits::zero::{Zero}; + use core::integer::{u512, u512_safe_div_rem_by_u256}; use governance::interfaces::erc20::{IERC20Dispatcher, IERC20DispatcherTrait}; use starknet::storage::{ Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess, }; - use crate::utils::fp::{ - UFixedPoint124x128, div_u64_by_u128, div_u64_by_fixed_point, - UFixedPoint124x128Impl, sub_fixed_points, add_fixed_points - }; - use crate::staker_log::{StakingLog, LogOperations}; + use crate::staker_log::{StakingLog, LogOperations, MAX_FP}; use starknet::{ get_block_timestamp, get_caller_address, get_contract_address, @@ -80,6 +76,7 @@ pub mod Staker { use super::{IStaker}; + #[derive(Copy, Drop, PartialEq, Debug)] pub struct DelegatedSnapshot { pub timestamp: u64, @@ -89,6 +86,7 @@ pub mod Staker { const TWO_POW_64: u128 = 0x10000000000000000; const TWO_POW_192: u256 = 0x1000000000000000000000000000000000000000000000000; const TWO_POW_192_DIVISOR: NonZero = 0x1000000000000000000000000000000000000000000000000; + const HALF: u128 = 0x80000000000000000000000000000000_u128; pub(crate) impl DelegatedSnapshotStorePacking of StorePacking { fn pack(value: DelegatedSnapshot) -> felt252 { @@ -358,49 +356,65 @@ pub mod Staker { self.get_average_delegated(delegate, now - period, now) } - fn get_cumulative_seconds_per_total_staked_at(self: @ContractState, timestamp: u64) -> UFixedPoint124x128 { + fn get_cumulative_seconds_per_total_staked_at(self: @ContractState, timestamp: u64) -> u256 { if let Option::Some((log_record, idx)) = self.staking_log.find_in_change_log(timestamp) { - let total_staked = if (idx == self.staking_log.len() - 1) { // if last rescord found self.total_staked.read() } else { // otherwise calculate using cumulative_seconds_per_total_staked difference let next_log_record = self.staking_log.at(idx+1).read(); - - let divisor = sub_fixed_points( - next_log_record.cumulative_seconds_per_total_staked, - log_record.cumulative_seconds_per_total_staked - ); - + + // substract fixed point values + let divisor = next_log_record.cumulative_seconds_per_total_staked - log_record.cumulative_seconds_per_total_staked; + assert(divisor.high < MAX_FP, 'FP_OVERFLOW'); + if divisor.is_zero() { return 0_u64.into(); } - let total_staked_fp = div_u64_by_fixed_point( - (next_log_record.timestamp - log_record.timestamp) / 1000, - divisor + let diff_seconds: u128 = ((next_log_record.timestamp - log_record.timestamp) / 1000).into(); + + // Divide u64 by fixed point + let (total_staked_fp_medium, _) = u512_safe_div_rem_by_u256( + u512 { limb0: 0, limb1: 0, limb2: 0, limb3: diff_seconds }, + divisor.try_into().unwrap() ); + + let total_staked_fp = u256 { + low: total_staked_fp_medium.limb1, + high: total_staked_fp_medium.limb2, + }; + + assert(total_staked_fp.high < MAX_FP, 'FP_OVERFLOW'); - // value is not precise - total_staked_fp.round() + // round value + total_staked_fp.high + if (total_staked_fp.low >= HALF) { + 1 + } else { + 0 + } }; let seconds_diff = (timestamp - log_record.timestamp) / 1000; - let staked_seconds: UFixedPoint124x128 = if total_staked == 0 { - 0_u64.into() + let staked_seconds: u256 = if total_staked == 0 { + 0_u256 } else { - div_u64_by_u128(seconds_diff, total_staked) + // Divide u64 by u128 + u256 { + low: 0, + high: seconds_diff.into() + } / total_staked.into() }; - return add_fixed_points( - log_record.cumulative_seconds_per_total_staked, - staked_seconds - ); + // Sum fixed posits + let result = log_record.cumulative_seconds_per_total_staked + staked_seconds; + assert(result.high < MAX_FP, 'FP_OVERFLOW'); + return result; } - return 0_u64.into(); + return 0_u256; } } } diff --git a/src/staker_log.cairo b/src/staker_log.cairo index 65a2118..d963719 100644 --- a/src/staker_log.cairo +++ b/src/staker_log.cairo @@ -1,25 +1,21 @@ use starknet::storage::MutableVecTrait; -use starknet::{Store, get_block_timestamp}; +use starknet::{get_block_timestamp}; use starknet::storage_access::{StorePacking}; use starknet::storage::{ Vec, VecTrait }; use starknet::storage::{ - StoragePointer, - StorageBase, Mutable, - StoragePath, StorageAsPath, - SubPointers, + StorageBase, Mutable, StorageAsPath, StoragePointerReadAccess, StoragePointerWriteAccess }; -use crate::utils::fp::{UFixedPoint124x128, div_u64_by_u128}; - pub type StakingLog = Vec; const TWO_POW_32: u64 = 0x100000000_u64; const MASK_32_BITS: u128 = 0x100000000_u128 - 1; const TWO_POW_160: u256 = 0x10000000000000000000000000000000000000000; +pub const MAX_FP: u128 = 0x8000000000000110000000000000000_u128; #[derive(Drop, Serde, Copy)] pub(crate) struct StakingLogRecord { @@ -28,7 +24,7 @@ pub(crate) struct StakingLogRecord { // Only 128+32=160 bits are used // TODO: add validation checks pub(crate) cumulative_total_staked: u256, - pub(crate) cumulative_seconds_per_total_staked: UFixedPoint124x128, + pub(crate) cumulative_seconds_per_total_staked: u256, } #[generate_trait] @@ -40,23 +36,21 @@ pub impl StakingLogOperations of LogOperations { fn find_in_change_log(self: @StorageBase, timestamp: u64) -> Option<(StakingLogRecord, u64)> { let log = self.as_path(); - if log.len() == 0 { return Option::None; } - let mut left = 0; let mut right = log.len() - 1; - - // To avoid reading from the storage multiple times. - let mut result_ptr: Option<(StoragePath, u64)> = Option::None; + // To avoid reading from the storage multiple times. + let mut result_ptr: Option<(StakingLogRecord, u64)> = Option::None; + while (left <= right) { let center = (right + left) / 2; - let record = log.at(center); + let record_ptr = log.at(center); + let record = record_ptr.read(); - let record_part = record.packed_timestamp_and_cumulative_total_staked.read(); - if record_part.timestamp <= timestamp { + if record.timestamp <= timestamp { result_ptr = Option::Some((record, center)); left = center + 1; } else { @@ -65,7 +59,7 @@ pub impl StakingLogOperations of LogOperations { }; if let Option::Some((result, idx)) = result_ptr { - return Option::Some((result.read(), idx)); + return Option::Some((result, idx)); } return Option::None; @@ -103,14 +97,14 @@ pub impl StakingLogOperations of LogOperations { let total_staked_by_elapsed_seconds = total_staked.into() * seconds_diff.into(); - let staked_seconds_per_total_staked: UFixedPoint124x128 = if total_staked == 0 { + let staked_seconds_per_total_staked: u256 = if total_staked == 0 { 0_u64.into() } else { - div_u64_by_u128(seconds_diff, total_staked) + let res = u256 { low: 0, high: seconds_diff.into() } / total_staked.into(); + assert(res.high < MAX_FP, 'FP_OVERFLOW'); + res }; - // assert(last_record.cumulative_total_staked + total_staked_by_elapsed_seconds < TWO_POW_160, 'TOTAL_STAKED_OVERFLOW'); - // Add a new record. record.write( StakingLogRecord { @@ -126,18 +120,15 @@ pub impl StakingLogOperations of LogOperations { } } - // // Storage layout for StakingLogRecord // pub(crate) impl StakingLogRecordStorePacking of StorePacking { fn pack(value: StakingLogRecord) -> (felt252, felt252) { - let packed_ts_cumulative_total_staked: felt252 = PackedValuesStorePacking::pack(( - value.timestamp, - value.cumulative_total_staked - ).into()); - + let packed_ts_cumulative_total_staked: felt252 = + pack_u64_u256_tuple(value.timestamp, value.cumulative_total_staked); + let cumulative_seconds_per_total_staked: felt252 = value.cumulative_seconds_per_total_staked .try_into() .unwrap(); @@ -147,89 +138,34 @@ pub(crate) impl StakingLogRecordStorePacking of StorePacking StakingLogRecord { let (packed_ts_cumulative_total_staked, cumulative_seconds_per_total_staked) = value; - let record = PackedValuesStorePacking::unpack(packed_ts_cumulative_total_staked); + let (timestamp, cumulative_total_staked) = unpack_u64_u256_tuple(packed_ts_cumulative_total_staked); + StakingLogRecord { - timestamp: record.timestamp, - cumulative_total_staked: record.cumulative_total_staked, + timestamp: timestamp, + cumulative_total_staked: cumulative_total_staked, cumulative_seconds_per_total_staked: cumulative_seconds_per_total_staked.try_into().unwrap(), } } } -#[derive(Drop, Serde)] -pub(crate) struct PackedRecordPart { - pub(crate) timestamp: u64, - pub(crate) cumulative_total_staked: u256, +pub(crate) fn pack_u64_u256_tuple(val1: u64, val2: u256) -> felt252 { + let cumulative_total_staked_high_32_bits: u128 = val2.high & MASK_32_BITS; + u256 { + high: val1.into() * TWO_POW_32.into() + cumulative_total_staked_high_32_bits.into(), + low: val2.low, + }.try_into().unwrap() } -pub(crate) impl TupleToPackedPart of Into<(u64, u256), PackedRecordPart> { - fn into(self: (u64, u256)) -> PackedRecordPart { - let (timestamp, cumulative_total_staked) = self; - PackedRecordPart { - timestamp: timestamp, - cumulative_total_staked: cumulative_total_staked - } - } -} - -pub impl PackedValuesStorePacking of StorePacking { - // Layout: - // high = cumulative_total_staked.high 32 bits + timestamp << 32 64 bits - // low = cumulative_total_staked.low 128 bits - - fn pack(value: PackedRecordPart) -> felt252 { - let cumulative_total_staked_high_32_bits: u128 = value.cumulative_total_staked.high & MASK_32_BITS; - u256 { - high: value.timestamp.into() * TWO_POW_32.into() + cumulative_total_staked_high_32_bits.into(), - low: value.cumulative_total_staked.low, - }.try_into().unwrap() - } - - fn unpack(value: felt252) -> PackedRecordPart { - let packed_ts_total_staked_u256: u256 = value.into(); - - let cumulative_total_staked = u256 { - high: packed_ts_total_staked_u256.high & MASK_32_BITS, - low: packed_ts_total_staked_u256.low - }; - - PackedRecordPart { - timestamp: (packed_ts_total_staked_u256.high / TWO_POW_32.into()).try_into().unwrap(), - cumulative_total_staked: cumulative_total_staked - } - } -} - -// -// Record subpoiters allow to minimase memory read operarions. Thus while doing binary search we ca read only 1 felt252 from memory. -// - -#[derive(Drop, Copy)] -pub(crate) struct StakingLogRecordSubPointers { - pub(crate) packed_timestamp_and_cumulative_total_staked: StoragePointer, - pub(crate) cumulative_seconds_per_total_staked: StoragePointer, -} - -pub(crate) impl StakingLogRecordSubPointersImpl of SubPointers { - - type SubPointersType = StakingLogRecordSubPointers; +pub(crate) fn unpack_u64_u256_tuple(value: felt252) -> (u64, u256) { + let packed_ts_total_staked_u256: u256 = value.into(); - fn sub_pointers(self: StoragePointer) -> StakingLogRecordSubPointers { - let base_address = self.__storage_pointer_address__; - - let packed_timestamp_and_cumulative_total_staked_ptr = StoragePointer { - __storage_pointer_address__: base_address, - __storage_pointer_offset__: self.__storage_pointer_offset__, - }; - - let cumulative_seconds_per_total_staked_ptr = StoragePointer { - __storage_pointer_address__: base_address, - __storage_pointer_offset__: self.__storage_pointer_offset__ + Store::::size(), - }; - - StakingLogRecordSubPointers { - packed_timestamp_and_cumulative_total_staked: packed_timestamp_and_cumulative_total_staked_ptr, - cumulative_seconds_per_total_staked: cumulative_seconds_per_total_staked_ptr, - } - } + let cumulative_total_staked = u256 { + high: packed_ts_total_staked_u256.high & MASK_32_BITS, + low: packed_ts_total_staked_u256.low + }; + + return ( + (packed_ts_total_staked_u256.high / TWO_POW_32.into()).try_into().unwrap(), + cumulative_total_staked + ); } diff --git a/src/staker_log_test.cairo b/src/staker_log_test.cairo index 8bc11b1..45509e5 100644 --- a/src/staker_log_test.cairo +++ b/src/staker_log_test.cairo @@ -1,5 +1,6 @@ use crate::staker_log::{ - PackedRecordPart, PackedValuesStorePacking, + pack_u64_u256_tuple, + unpack_u64_u256_tuple, }; const MASK_32_BITS: u128 = 0x100000000_u128 - 1; @@ -8,22 +9,21 @@ const MASK_160_BITS: u256 = 0x10000000000000000000000000000000000000000 - 1; fn assert_packs_and_unpacks(timestamp: u64, total_staked: u256) { - let record: PackedRecordPart = (timestamp, total_staked).into(); - - let packed: u256 = PackedValuesStorePacking::pack(record).into(); + let packed: u256 = pack_u64_u256_tuple(timestamp, total_staked).into(); let first_160_bits: u256 = packed & MASK_160_BITS; let shifted_160_bits_right: u128 = packed.high / (MASK_32_BITS + 1); let last_64_bits: u64 = (shifted_160_bits_right & MASK_64_BITS).try_into().unwrap(); - assert_eq!(first_160_bits, total_staked); assert_eq!(last_64_bits, timestamp); - let unpacked_record: PackedRecordPart = PackedValuesStorePacking::unpack(packed.try_into().unwrap()); - assert_eq!(unpacked_record.timestamp, timestamp); - assert_eq!(unpacked_record.cumulative_total_staked, total_staked); + let (unpacked_timestamp, unpacked_cumulative_total_staked) = unpack_u64_u256_tuple( + packed.try_into().unwrap() + ); + assert_eq!(unpacked_timestamp, timestamp); + assert_eq!(unpacked_cumulative_total_staked, total_staked); } #[test] diff --git a/src/staker_test.cairo b/src/staker_test.cairo index a5b501d..3d55eea 100644 --- a/src/staker_test.cairo +++ b/src/staker_test.cairo @@ -402,7 +402,7 @@ fn test_delegate_undelegate() { mod staker_staked_seconds_per_total_staked_calculation { use starknet::{get_caller_address}; - use crate::utils::fp::{UFixedPoint124x128, UFixedPoint124x128Impl}; + use super::{ setup, contract_address_const, set_block_timestamp, IERC20DispatcherTrait, IStakerDispatcherTrait, @@ -441,9 +441,9 @@ mod staker_staked_seconds_per_total_staked_calculation { staker.withdraw_amount(delegatee, token_owner, 2000); } - fn assert_fp(value: UFixedPoint124x128, integer: u128, fractional: u128) { - assert_eq!(value.get_integer(), integer); - assert_eq!(value.get_fractional(), fractional); + fn assert_fp(value: u256, integer: u128, fractional: u128) { + assert_eq!(value.high, integer); + assert_eq!(value.low, fractional); } #[test] From 815d539fca5859f46553f5731bf8d337ebb4cd10 Mon Sep 17 00:00:00 2001 From: baitcode Date: Sat, 11 Jan 2025 02:35:29 +0300 Subject: [PATCH 32/40] small fix --- declare-all.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/declare-all.sh b/declare-all.sh index e9fb4c7..f098548 100755 --- a/declare-all.sh +++ b/declare-all.sh @@ -37,7 +37,7 @@ scarb build declare_class_hash() { local class_name=$1 - starkli declare --watch --network "$NETWORK" --keystore-password "$STARKNET_KEYSTORE_PASSWORD" --casm-file "target/dev/governance_${class_name}.compiled_contract_class.json" "target/dev/governance_${class_name}.contract_class.json" + starkli declare --watch --network "$NETWORK" --keystore-password "$STARKNET_KEYSTORE_PASSWORD" --casm-file "target/dev/governance_${class_name}.compiled_contract_class.json" "target/dev/governance_${class_name}.contract_class.json" } echo "Declaring AirdropClaimCheck" From 284ae396a3b8bceab9a9e8e27b27082a23abc434 Mon Sep 17 00:00:00 2001 From: baitcode Date: Sat, 11 Jan 2025 02:36:19 +0300 Subject: [PATCH 33/40] lost deletions --- src/lib.cairo | 12 +- src/utils/fp.cairo | 142 ------------------------ src/utils/fp_test.cairo | 240 ---------------------------------------- 3 files changed, 6 insertions(+), 388 deletions(-) delete mode 100644 src/utils/fp.cairo delete mode 100644 src/utils/fp_test.cairo diff --git a/src/lib.cairo b/src/lib.cairo index 1c3eaf8..dde6f53 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -1,7 +1,7 @@ pub mod airdrop; mod airdrop_claim_check; -// #[cfg(test)] -// mod airdrop_test; +#[cfg(test)] +mod airdrop_test; pub mod call_trait; #[cfg(test)] @@ -12,16 +12,16 @@ pub mod execution_state; mod execution_state_test; pub mod governor; -// #[cfg(test)] -// mod governor_test; +#[cfg(test)] +mod governor_test; pub mod staker; #[cfg(test)] mod staker_test; pub mod staker_log; -// #[cfg(test)] -// pub mod staker_log_test; +#[cfg(test)] +pub mod staker_log_test; mod interfaces { pub(crate) mod erc20; diff --git a/src/utils/fp.cairo b/src/utils/fp.cairo deleted file mode 100644 index 5337c66..0000000 --- a/src/utils/fp.cairo +++ /dev/null @@ -1,142 +0,0 @@ -use starknet::storage_access::{StorePacking}; -use core::num::traits::{WideMul, Zero }; -use core::integer::{u512, u512_safe_div_rem_by_u256 }; - -pub const EPSILON: u256 = 0x10_u256; - -// 2^124 -pub const MAX_INT: u128 = 0x8000000000000110000000000000000_u128; -pub const HALF: u128 = 0x80000000000000000000000000000000_u128; - -pub type UFixedPoint124x128 = u256; - -pub mod Errors { - pub const FP_ADD_OVERFLOW: felt252 = 'FP_ADD_OVERFLOW'; - pub const FP_SUB_OVERFLOW: felt252 = 'FP_SUB_OVERFLOW'; - pub const FP_MUL_OVERFLOW: felt252 = 'FP_MUL_OVERFLOW'; - pub const FP_DIV_OVERFLOW: felt252 = 'FP_DIV_OVERFLOW'; - pub const FP_SUB_UNDERFLOW: felt252 = 'FP_SUB_UNDERFLOW'; - - pub const DIVISION_BY_ZERO: felt252 = 'DIVISION_BY_ZERO'; - pub const MAX_OVERFLOW: felt252 = 'MAX_OVERFLOW'; -} - -pub impl UFixedPoint124x128StorePacking of StorePacking { - fn pack(value: UFixedPoint124x128) -> felt252 { - value.try_into().unwrap() - } - - fn unpack(value: felt252) -> UFixedPoint124x128 { - value.into() - } -} - -#[generate_trait] -pub impl UFixedPoint124x128Impl of UFixedPointTrait { - fn get_integer(self: UFixedPoint124x128) -> u128 { self.high } - fn get_fractional(self: UFixedPoint124x128) -> u128 { self.low } - - fn from_u64(value: u64) -> UFixedPoint124x128 { - u256 { - low: 0, - high: value.into(), - } - } - - fn from_u128(value: u128) -> UFixedPoint124x128 { - let res = u256 { - low: 0, - high: value, - }; - - assert(res.high < MAX_INT, Errors::MAX_OVERFLOW); - - res - } - - fn round(self: UFixedPoint124x128) -> u128 { - let overflow = if (self.get_fractional() >= HALF) { - 1 - } else { - 0 - }; - self.get_integer() + overflow - } -} - -pub fn div_u64_by_u128(lhs: u64, rhs: u128) -> UFixedPoint124x128 { - assert(!rhs.is_zero(), Errors::DIVISION_BY_ZERO); - - let res = UFixedPoint124x128Impl::from_u64(lhs) / rhs.into(); - - assert(res.high < MAX_INT, Errors::FP_DIV_OVERFLOW); - - res -} - -pub fn div_fixed_point_by_fixed_point(lhs: UFixedPoint124x128, rhs: UFixedPoint124x128) -> UFixedPoint124x128 { - let left: u512 = u512 { - limb0: 0, - limb1: 0, - limb2: lhs.low, - limb3: lhs.high, - }; - - assert(rhs != 0, Errors::DIVISION_BY_ZERO); - - let (div_res, _) = u512_safe_div_rem_by_u256( - left, - rhs.try_into().unwrap(), - ); - - let res = u256 { - low: div_res.limb1, - high: div_res.limb2, - }; - - assert(res.high < MAX_INT, Errors::FP_DIV_OVERFLOW); - - res -} - -pub fn div_u64_by_fixed_point(lhs: u64, rhs: UFixedPoint124x128) -> UFixedPoint124x128 { - assert(!rhs.is_zero(), Errors::DIVISION_BY_ZERO); - - div_fixed_point_by_fixed_point( - UFixedPoint124x128Impl::from_u64(lhs), - rhs - ) -} - -pub fn mul_fixed_point_by_u128(lhs: UFixedPoint124x128, rhs: u128) -> UFixedPoint124x128 { - let mult_res = lhs.wide_mul(rhs.into()); - - let res = UFixedPoint124x128 { - low: mult_res.limb0, - high: mult_res.limb1, - }; - - assert(res.high < MAX_INT, Errors::FP_MUL_OVERFLOW); - - res -} - -pub fn add_fixed_points(lhs: UFixedPoint124x128, rhs: UFixedPoint124x128) -> UFixedPoint124x128 { - assert(rhs <= rhs + lhs, Errors::FP_ADD_OVERFLOW); - assert(lhs <= rhs + lhs, Errors::FP_ADD_OVERFLOW); - - let res = rhs + lhs; - - assert(res.high < MAX_INT, Errors::FP_ADD_OVERFLOW); - - res -} - -pub fn sub_fixed_points(lhs: UFixedPoint124x128, rhs: UFixedPoint124x128) -> UFixedPoint124x128 { - assert(lhs >= rhs, Errors::FP_SUB_UNDERFLOW); - // TODO: underflow checking - let res = lhs - rhs; - assert(res.high < MAX_INT, Errors::FP_SUB_OVERFLOW); - - res -} diff --git a/src/utils/fp_test.cairo b/src/utils/fp_test.cairo deleted file mode 100644 index 4f05f5f..0000000 --- a/src/utils/fp_test.cairo +++ /dev/null @@ -1,240 +0,0 @@ -use core::num::traits::WideMul; -use super::fp::UFixedPointTrait; - -use crate::utils::fp::{ - UFixedPoint124x128, - UFixedPoint124x128Impl, - div_u64_by_u128, mul_fixed_point_by_u128, div_u64_by_fixed_point, - MAX_INT -}; - -const SCALE_FACTOR: u256 = 0x100000000000000000000000000000000; - - -pub(crate) impl U64IntoUFixedPoint of Into { - fn into(self: u128) -> UFixedPoint124x128 { - let medium = u256 { - low: 0, - high: self, - }; - medium.into() - } -} - - -#[test] -fn test_add() { - let f1 : UFixedPoint124x128 = UFixedPoint124x128Impl::from_u64(0xFFFFFFFFFFFFFFFF_u64); - let f2 : UFixedPoint124x128 = UFixedPoint124x128Impl::from_u64(1_u64); - - let res = f1 + f2; - - assert_eq!(res.low, 0); - assert_eq!(res.high, 0x10000000000000000); -} - -#[test] -fn test_fp_value_mapping() { - let f1 : UFixedPoint124x128 = UFixedPoint124x128Impl::from_u64(7_u64); - assert_eq!(f1.get_fractional(), 0x0); - assert_eq!(f1.get_integer(), 0x7); - - assert_eq!(f1, 7_u256*0x100000000000000000000000000000000); -} - - -#[test] -fn test_mul() { - let f1 = 7_u64; - let f2 = 7_u64; - - let expected = (7_u256*SCALE_FACTOR).wide_mul(7_u256*SCALE_FACTOR); - - assert_eq!(expected.limb0, 0); - assert_eq!(expected.limb1, 0); - assert_eq!(expected.limb2, 49); - assert_eq!(expected.limb3, 0); - - let res: u256 = mul_fixed_point_by_u128( - UFixedPoint124x128Impl::from_u64(f1), - f2.try_into().unwrap() - ).into(); - - assert_eq!(res.high, 49); - assert_eq!(res.low, 0); -} - -#[test] -#[should_panic(expected: 'FP_MUL_OVERFLOW')] -fn test_multiplication_overflow() { - let f1 = MAX_INT - 1; - let f2 = MAX_INT - 1; - let _ = mul_fixed_point_by_u128( - UFixedPoint124x128Impl::from_u128(f1), - f2.try_into().unwrap() - ); -} - -#[test] -fn test_u256_conversion() { - let f: u256 = 0x0123456789ABCDEFFEDCBA987654321000112233445566778899AABBCCDDEEFF_u256; - - assert_eq!(f.low, 0x00112233445566778899AABBCCDDEEFF); - assert_eq!(f.high, 0x0123456789ABCDEFFEDCBA9876543210); - - let fp: UFixedPoint124x128 = f; - - assert_eq!(fp.get_integer(), f.high); - assert_eq!(fp.get_fractional(), f.low); -} - -fn run_division_test(left: u64, right: u128, expected_int: u128, expected_frac: u128) { - let res = div_u64_by_u128(left, right); - - assert_eq!(res.get_integer(), expected_int); - assert_eq!(res.get_fractional(), expected_frac); -} - -fn run_division_and_multiplication_test(numenator: u64, divisor: u128, mult: u128, expected_int: u128, expected_frac: u128) { - let divided = div_u64_by_u128(numenator, divisor); - let res = mul_fixed_point_by_u128(divided, mult); - - assert_eq!(res.get_integer(), expected_int); - assert_eq!(res.get_fractional(), expected_frac); -} - - -#[test] -fn test_division() { - run_division_test(1, 1000, 0, 0x4189374bc6a7ef9db22d0e56041893); - run_division_test(56, 7, 8, 0); - run_division_test(0, 7, 0, 0); - - run_division_test(0x6c9444e9af6eb21, 0xd0ba0d5da1c09d9e57d94820ec138cce, 0x0, 0x852bac969a2350b); - run_division_test(0xdcbfffffa6958e23, 0x4918d4829fcdf183d6d99f0570f1745, 0x0, 0x3051c1a9f372fbfd4a); - run_division_test(0xd426b156df76a5a0, 0x6a8f314198ecb47d43cd4aa0a9ca43d9, 0x0, 0x1fdacf0b6d911b54a); - run_division_test(0xab4741c332625aba, 0xb930f984727d03cbefad17a2e094607d, 0x0, 0xecc471b02bb827d2); - run_division_test(0xf456e2beec7fafa6, 0xc206153d5f83abbc774cf8bea8f0a27e, 0x0, 0x14263442f5d80ef3d); - run_division_test(0x85bc433c34460024, 0x8a748e982e70d29d3b59fd4736113ceb, 0x0, 0xf745e5b672b00d3a); - run_division_test(0xb1db6e9041be65b2, 0x9c7a91635d61b2bccf88160a2fd53dee, 0x0, 0x122f9a1641f14e42b); - run_division_test(0x2180c4fcbc0f47a3, 0xda8f12b54ad0f583da1417295e12b48a, 0x0, 0x273e0c487a908538); - run_division_test(0x966c069564807e34, 0xbd2087c1a063e4c9514889201f4293db, 0x0, 0xcb9bf9617ac98b00); - run_division_test(0xb117842a13d72f4, 0x8ba79e67edcfbd2bec7379332208587b, 0x0, 0x144a02a03bcb8771); - run_division_test(0x1f48f3feddc9b742, 0x2b409d77edb124901b6cca8b496cd3e0, 0x0, 0xb92af636e340d0d6); - - run_division_test(0x51cb6348d4f073eb, 0xae797e7d, 0x7803938f, 0xc7836eec158af9487775eb8656cf38fc); - run_division_test(0x62337e9d72ddfb51, 0x59214f47, 0x11a0dcac3, 0x1acf56e174d88003af63f46791468df3); - run_division_test(0xd5ecb2a682ba1fee, 0x865f3300, 0x1978f8bd2, 0x3354716b276ed7e6c759e14d58ab5767); - run_division_test(0xda53f5e167d39325, 0x491317ba, 0x2fcdca379, 0xcaab30305da3be6620c494367695761e); - run_division_test(0x22afcdd641467cd1, 0xdedbc9d, 0x27d85372e, 0x76e1c4fe6939db4dcd8b67f2def5c102); - run_division_test(0x37bc576886b36435, 0x9563dc74, 0x5f82b8e1, 0xc7c4701f73ebf57b4c59d2f357775a0d); - run_division_test(0x84bd569f3d7ce768, 0x555d2820, 0x18e1384ee, 0x596b1a4ce6995f4a1229e9f185ec0672); - run_division_test(0x46106cf2302ff8fa, 0xf6d765c9, 0x48a9ec96, 0x44eb3c9b3e7337e3750761dca19c8098); - run_division_test(0xe4311b247d0fccd4, 0x751c9792, 0x1f2d0b9ef, 0xb5e4969913d37a40ba6cc33b74a5ea6d); - run_division_test(0x7bc92347ffca33de, 0x73038c20, 0x113864700, 0xa7549c75ef40e011c8ab37b7f05a706d); - - run_division_test(0x72895698b8a67aa2, 0x6b27b2f8336c0bc, 0x11, 0x1a2768bb9a123be83f2c893726d5d4dc); - run_division_test(0xe43e43db78e75c8c, 0x3bafd02937f52e73, 0x3, 0xd2f2c717d8d53457b6c4d8a0c400ef74); - run_division_test(0x39a592b65c336682, 0x4067a77d6291d1db, 0x0, 0xe5232e91c70df514ac6a23241bf0e0d7); - run_division_test(0x13377d1615ab6af, 0x7418529253740f28, 0x0, 0x2a5feadf29edc86e96db0d477e3a85c); - run_division_test(0x7c688ba1d6c8ca69, 0xf62124b5d737632d, 0x0, 0x8165c4a3a8e578f36fcae7117511f4c3); - run_division_test(0x2b48a3776a88ed98, 0x5b21e1adfb41d0a, 0x7, 0x996a2c610569e5b72bcb8d97a3a58c09); - run_division_test(0x1f1b63f5e8fc99b, 0xa1d46df0d2da3b6c, 0x0, 0x31355ba6f51adc401cbe093d9b3a098); - run_division_test(0xabf999354e11bcbc, 0xcad7c0d69097196b, 0x0, 0xd90aff57100cf3ccf9fe2377d45d866c); - run_division_test(0xdca981c1e1cb4c1e, 0xf9c9c0425b10334d, 0x0, 0xe226541f530d56041cd7834f878fe4fa); - run_division_test(0x2c70aa99601005b4, 0xb2df598860e0ece4, 0x0, 0x3f9a241362c5996e29f7867cd85a8403); -} - -#[test] -fn test_division_by_fixed_point_and_rounding() { - let half = div_u64_by_u128(1_u64, 2_u128); - - assert_eq!(div_u64_by_fixed_point(0, half).round(), 0_u128); - assert_eq!(div_u64_by_fixed_point(1, half).round(), 2_u128); - assert_eq!(div_u64_by_fixed_point(50, half).round(), 100_u128); - - let one_over_thousand = div_u64_by_u128(1_u64, 1000_u128); - assert_eq!(div_u64_by_fixed_point(100, one_over_thousand).round(), 100000_u128); - assert_eq!(div_u64_by_fixed_point(200, one_over_thousand).round(), 200000_u128); - assert_eq!(div_u64_by_fixed_point(300, one_over_thousand).round(), 300000_u128); - assert_eq!(div_u64_by_fixed_point(400, one_over_thousand).round(), 400000_u128); - assert_eq!(div_u64_by_fixed_point(500, one_over_thousand).round(), 500000_u128); - assert_eq!(div_u64_by_fixed_point(600, one_over_thousand).round(), 600000_u128); - assert_eq!(div_u64_by_fixed_point(700, one_over_thousand).round(), 700000_u128); - assert_eq!(div_u64_by_fixed_point(800, one_over_thousand).round(), 800000_u128); - assert_eq!(div_u64_by_fixed_point(900, one_over_thousand).round(), 900000_u128); - - let one_over_four = div_u64_by_u128(1_u64, 4_u128); - assert_eq!(div_u64_by_fixed_point(1, one_over_four).round(), 4_u128); - assert_eq!(div_u64_by_fixed_point(2, one_over_four).round(), 8_u128); - assert_eq!(div_u64_by_fixed_point(3, one_over_four).round(), 12_u128); - assert_eq!(div_u64_by_fixed_point(4, one_over_four).round(), 16_u128); - - let six = div_u64_by_u128(6_u64, 1_u128); - - assert_eq!(div_u64_by_fixed_point(1, six).round(), 0_u128); - assert_eq!(div_u64_by_fixed_point(2, six).round(), 0_u128); - assert_eq!(div_u64_by_fixed_point(3, six).round(), 1_u128); - assert_eq!(div_u64_by_fixed_point(4, six).round(), 1_u128); - assert_eq!(div_u64_by_fixed_point(5, six).round(), 1_u128); - assert_eq!(div_u64_by_fixed_point(6, six).round(), 1_u128); - assert_eq!(div_u64_by_fixed_point(7, six).round(), 1_u128); - assert_eq!(div_u64_by_fixed_point(8, six).round(), 1_u128); - assert_eq!(div_u64_by_fixed_point(9, six).round(), 2_u128); - assert_eq!(div_u64_by_fixed_point(10, six).round(), 2_u128); -} - -#[test] -fn test_substraction() { - let one_over_four = div_u64_by_u128(1_u64, 4_u128); - let one_over_two = div_u64_by_u128(1_u64, 2_u128); - - assert_eq!(one_over_two - one_over_four, one_over_four); -} - - -#[test] -fn test_division_and_multiplication_by() { - run_division_and_multiplication_test(0x7fe6d7aa683992c1, 0xd59e88c2c2cf8b662b58477379ec3c5, 0x64566fdc35690d86, 0x3, 0xc136a7d74b488c082a89691a34cf83a4); - run_division_and_multiplication_test(0x49ccea818ca40f32, 0xb1a0b901dfe18783859c30601b5ccd8, 0x7adf8be5f20e22ee, 0x3, 0x30d1670656d38d261dcefab45ae2e238); - run_division_and_multiplication_test(0x8d7f9fbff6380515, 0xc373107fd425d17ceb8edd19f268fe9, 0x292f63f7f36a4983, 0x1, 0xdd10b15b82683a17543b3adbd9c52cc5); - run_division_and_multiplication_test(0xc3e18e7febc4e55b, 0xff4f8b2f5f1be3a5d749f67983c2e1f, 0xb6c6724185a770e8, 0x8, 0xc3adba3b09089fac88e8b3d30a8dd7d0); - run_division_and_multiplication_test(0x85c71ca825ca72c, 0xeeb4af4a991597e3e0d2ec08f7b1a8, 0x674999ae0e694004, 0x3, 0x9e2a7622fec270adc1ca417941e64cf0); - run_division_and_multiplication_test(0xb7ee4db7793d7767, 0x38c104ee49ba97de1a51f2a0f731348, 0x8769d0fa019d65ab, 0x1b, 0x6da8c217c5d246f3481b53d6ce321bf0); - run_division_and_multiplication_test(0x33841fabbb0ee95c, 0x2e8ab4d06279d72ccec95511f222293, 0xe8ac73c7c4d19b13, 0x10, 0x18a91a646eace61645b24e9207c37738); - run_division_and_multiplication_test(0xc107bce26acc446a, 0x8c724e14d0d54f3db76e6003f857fa8, 0x2c3ee7a6ca275beb, 0x3, 0xccfbe15105a920c68e486ad91439ec75); - run_division_and_multiplication_test(0xa753b4fb1f4f8828, 0x7254ab5927cb62d64986a1ff6240789, 0xa87210053c3749f7, 0xf, 0x686a030bba609986d0bd3707b995ea97); - run_division_and_multiplication_test(0xdc7a02e89502151c, 0x98072c1c94111cfd1ed52d396b35d1a, 0xd19d22fc5f018bc7, 0x12, 0xffd594dfa4115e06c34d6ca0416012b4); -} - -#[test] -#[should_panic(expected: 'FP_DIV_OVERFLOW')] -fn test_division_overflow() { - // 1 / 2**124 - let fp1 = div_u64_by_u128(1, MAX_INT); - div_u64_by_fixed_point(1, fp1); -} - -#[test] -#[should_panic(expected: 'DIVISION_BY_ZERO')] -fn test_division_by_zero() { - run_division_test(56, 0, 0, 0); -} - -#[test] -fn test_should_work_fine_with_zeroes() { - let f1 = div_u64_by_u128(5_u64, 2_u128).into(); - let f2: UFixedPoint124x128 = 0.into(); - - let res = f1 + f2; - assert_eq!(res, f1); - - let res = f1 - f2; - assert_eq!(res, f1); - - let res = f1 - f1; - assert_eq!(res, 0.into()); - - let res = f2 - f2; - assert_eq!(res, 0.into()); -} From c6102a19810fd830eaef530a2f9530d53436592a Mon Sep 17 00:00:00 2001 From: baitcode Date: Mon, 13 Jan 2025 22:32:14 +0300 Subject: [PATCH 34/40] Review fixes * uniform get_block_timestamp usage * store log_record timestamp in seconds * inline test assert for fp value * rename HALF to TWO_POW_127 --- src/staker.cairo | 16 ++++++++-------- src/staker_log.cairo | 11 ++++++----- src/staker_test.cairo | 43 ++++++++++++++++++------------------------- 3 files changed, 32 insertions(+), 38 deletions(-) diff --git a/src/staker.cairo b/src/staker.cairo index e953c16..ae48713 100644 --- a/src/staker.cairo +++ b/src/staker.cairo @@ -75,8 +75,6 @@ pub mod Staker { }; use super::{IStaker}; - - #[derive(Copy, Drop, PartialEq, Debug)] pub struct DelegatedSnapshot { pub timestamp: u64, @@ -86,7 +84,7 @@ pub mod Staker { const TWO_POW_64: u128 = 0x10000000000000000; const TWO_POW_192: u256 = 0x1000000000000000000000000000000000000000000000000; const TWO_POW_192_DIVISOR: NonZero = 0x1000000000000000000000000000000000000000000000000; - const HALF: u128 = 0x80000000000000000000000000000000_u128; + const TWO_POW_127: u128 = 0x80000000000000000000000000000000_u128; pub(crate) impl DelegatedSnapshotStorePacking of StorePacking { fn pack(value: DelegatedSnapshot) -> felt252 { @@ -266,7 +264,7 @@ pub mod Staker { .write(delegate, self.insert_snapshot(delegate, get_block_timestamp()) + amount); let total_staked = self.total_staked.read(); - assert(total_staked + amount >= total_staked, 'BAD AMOUNT'); + self.total_staked.write(total_staked + amount); self.staking_log.log_change(amount, total_staked); @@ -357,7 +355,9 @@ pub mod Staker { } fn get_cumulative_seconds_per_total_staked_at(self: @ContractState, timestamp: u64) -> u256 { - if let Option::Some((log_record, idx)) = self.staking_log.find_in_change_log(timestamp) { + let timestamp_seconds = timestamp / 1000; + + if let Option::Some((log_record, idx)) = self.staking_log.find_in_change_log(timestamp_seconds) { let total_staked = if (idx == self.staking_log.len() - 1) { // if last rescord found self.total_staked.read() @@ -373,7 +373,7 @@ pub mod Staker { return 0_u64.into(); } - let diff_seconds: u128 = ((next_log_record.timestamp - log_record.timestamp) / 1000).into(); + let diff_seconds: u128 = (next_log_record.timestamp - log_record.timestamp).into(); // Divide u64 by fixed point let (total_staked_fp_medium, _) = u512_safe_div_rem_by_u256( @@ -389,14 +389,14 @@ pub mod Staker { assert(total_staked_fp.high < MAX_FP, 'FP_OVERFLOW'); // round value - total_staked_fp.high + if (total_staked_fp.low >= HALF) { + total_staked_fp.high + if (total_staked_fp.low >= TWO_POW_127) { 1 } else { 0 } }; - let seconds_diff = (timestamp - log_record.timestamp) / 1000; + let seconds_diff = timestamp_seconds - log_record.timestamp; let staked_seconds: u256 = if total_staked == 0 { 0_u256 diff --git a/src/staker_log.cairo b/src/staker_log.cairo index d963719..7c857c6 100644 --- a/src/staker_log.cairo +++ b/src/staker_log.cairo @@ -22,7 +22,6 @@ pub(crate) struct StakingLogRecord { pub(crate) timestamp: u64, // Only 128+32=160 bits are used - // TODO: add validation checks pub(crate) cumulative_total_staked: u256, pub(crate) cumulative_seconds_per_total_staked: u256, } @@ -68,10 +67,12 @@ pub impl StakingLogOperations of LogOperations { fn log_change(self: StorageBase>, amount: u128, total_staked: u128) { let log = self.as_path(); + let block_timestamp = get_block_timestamp() / 1000; + if log.len() == 0 { log.append().write( StakingLogRecord { - timestamp: get_block_timestamp(), + timestamp: block_timestamp, cumulative_total_staked: 0_u256, cumulative_seconds_per_total_staked: 0_u64.into(), } @@ -84,7 +85,7 @@ pub impl StakingLogOperations of LogOperations { let mut last_record = last_record_ptr.read(); - let mut record = if last_record.timestamp == get_block_timestamp() { + let mut record = if last_record.timestamp == block_timestamp { // update record last_record_ptr } else { @@ -93,7 +94,7 @@ pub impl StakingLogOperations of LogOperations { }; // Might be zero - let seconds_diff = (get_block_timestamp() - last_record.timestamp) / 1000; + let seconds_diff = block_timestamp - last_record.timestamp; let total_staked_by_elapsed_seconds = total_staked.into() * seconds_diff.into(); @@ -108,7 +109,7 @@ pub impl StakingLogOperations of LogOperations { // Add a new record. record.write( StakingLogRecord { - timestamp: get_block_timestamp(), + timestamp: block_timestamp, cumulative_total_staked: last_record.cumulative_total_staked + total_staked_by_elapsed_seconds, diff --git a/src/staker_test.cairo b/src/staker_test.cairo index 3d55eea..3b9e597 100644 --- a/src/staker_test.cairo +++ b/src/staker_test.cairo @@ -411,8 +411,9 @@ mod staker_staked_seconds_per_total_staked_calculation { #[test] fn test_should_return_0_if_no_data_found() { let (staker, _) = setup(10000); - assert_fp(staker.get_cumulative_seconds_per_total_staked_at(0), 0, 0_u128.into()); - assert_fp(staker.get_cumulative_seconds_per_total_staked_at(1000), 0, 0_u128.into()); + + assert_eq!(staker.get_cumulative_seconds_per_total_staked_at(0), u256{ high: 0, low: 0_u128.into()}); + assert_eq!(staker.get_cumulative_seconds_per_total_staked_at(1000), u256{ high: 0, low: 0_u128.into()}); } #[test] @@ -441,11 +442,6 @@ mod staker_staked_seconds_per_total_staked_calculation { staker.withdraw_amount(delegatee, token_owner, 2000); } - fn assert_fp(value: u256, integer: u128, fractional: u128) { - assert_eq!(value.high, integer); - assert_eq!(value.low, fractional); - } - #[test] fn test_should_stake_10000_tokens_for_5_seconds_adding_10000_every_second_to_staked_seconds() { let (staker, token) = setup(1000); @@ -474,23 +470,20 @@ mod staker_staked_seconds_per_total_staked_calculation { token.approve(staker.contract_address, 7); staker.stake(delegatee); // Will transfer 7 token to contract account and setup delegatee - assert_fp(staker.get_cumulative_seconds_per_total_staked_at(0), 0, 0_u128); - assert_fp(staker.get_cumulative_seconds_per_total_staked_at(500), 0, 0_u128); - assert_fp(staker.get_cumulative_seconds_per_total_staked_at(999), 0, 0_u128); - - assert_fp(staker.get_cumulative_seconds_per_total_staked_at(1000), 0, 0x80000000000000000000000000000000_u128); - assert_fp(staker.get_cumulative_seconds_per_total_staked_at(2000), 1, 0_u128); - assert_fp(staker.get_cumulative_seconds_per_total_staked_at(3000), 1, 0x80000000000000000000000000000000_u128); - assert_fp(staker.get_cumulative_seconds_per_total_staked_at(4000), 2, 0_u128); - - // here value is undefined as nothing was staked. - assert_fp(staker.get_cumulative_seconds_per_total_staked_at(5000), 0, 0_u128); - assert_fp(staker.get_cumulative_seconds_per_total_staked_at(6000), 0, 0_u128); - assert_fp(staker.get_cumulative_seconds_per_total_staked_at(7000), 0, 0_u128); - assert_fp(staker.get_cumulative_seconds_per_total_staked_at(8000), 0, 0_u128); - assert_fp(staker.get_cumulative_seconds_per_total_staked_at(9000), 0, 0_u128); - assert_fp(staker.get_cumulative_seconds_per_total_staked_at(10000), 2, 0x80000000000000000000000000000000_u128); - assert_fp(staker.get_cumulative_seconds_per_total_staked_at(17000), 3, 0x80000000000000000000000000000000_u128); - assert_fp(staker.get_cumulative_seconds_per_total_staked_at(24000), 4, 0x80000000000000000000000000000000_u128); + assert_eq!(staker.get_cumulative_seconds_per_total_staked_at(0), u256 {high: 0, low: 0_u128}); + assert_eq!(staker.get_cumulative_seconds_per_total_staked_at(500), u256 {high: 0, low: 0_u128}); + assert_eq!(staker.get_cumulative_seconds_per_total_staked_at(999), u256 {high: 0, low: 0_u128}); + assert_eq!(staker.get_cumulative_seconds_per_total_staked_at(1000), u256 {high: 0, low: 0x80000000000000000000000000000000_u128}); + assert_eq!(staker.get_cumulative_seconds_per_total_staked_at(2000), u256 {high: 1, low: 0_u128}); + assert_eq!(staker.get_cumulative_seconds_per_total_staked_at(3000), u256 {high: 1, low: 0x80000000000000000000000000000000_u128}); + assert_eq!(staker.get_cumulative_seconds_per_total_staked_at(4000), u256 {high: 2, low: 0_u128}); + assert_eq!(staker.get_cumulative_seconds_per_total_staked_at(5000), u256 {high: 0, low: 0_u128}); + assert_eq!(staker.get_cumulative_seconds_per_total_staked_at(6000), u256 {high: 0, low: 0_u128}); + assert_eq!(staker.get_cumulative_seconds_per_total_staked_at(7000), u256 {high: 0, low: 0_u128}); + assert_eq!(staker.get_cumulative_seconds_per_total_staked_at(8000), u256 {high: 0, low: 0_u128}); + assert_eq!(staker.get_cumulative_seconds_per_total_staked_at(9000), u256 {high: 0, low: 0_u128}); + assert_eq!(staker.get_cumulative_seconds_per_total_staked_at(10000), u256 {high: 2, low: 0x80000000000000000000000000000000_u128}); + assert_eq!(staker.get_cumulative_seconds_per_total_staked_at(17000), u256 {high: 3, low: 0x80000000000000000000000000000000_u128}); + assert_eq!(staker.get_cumulative_seconds_per_total_staked_at(24000), u256 {high: 4, low: 0x80000000000000000000000000000000_u128}); } } \ No newline at end of file From e3067ab0b30fe433e1a8174abe47b9521f6ff137 Mon Sep 17 00:00:00 2001 From: baitcode Date: Mon, 13 Jan 2025 22:34:18 +0300 Subject: [PATCH 35/40] scarb fmt --- src/lib.cairo | 6 +- src/staker.cairo | 62 +++++++++++---------- src/staker_log.cairo | 114 ++++++++++++++++++++------------------ src/staker_log_test.cairo | 11 ++-- src/staker_test.cairo | 111 ++++++++++++++++++++++++------------- 5 files changed, 174 insertions(+), 130 deletions(-) diff --git a/src/lib.cairo b/src/lib.cairo index dde6f53..7d41603 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -16,18 +16,18 @@ pub mod governor; mod governor_test; pub mod staker; -#[cfg(test)] -mod staker_test; pub mod staker_log; #[cfg(test)] pub mod staker_log_test; +#[cfg(test)] +mod staker_test; mod interfaces { pub(crate) mod erc20; } mod utils { - pub(crate) mod exp2; + pub(crate) mod exp2; } #[cfg(test)] diff --git a/src/staker.cairo b/src/staker.cairo index ae48713..a90ef93 100644 --- a/src/staker.cairo +++ b/src/staker.cairo @@ -59,19 +59,19 @@ pub trait IStaker { #[starknet::contract] pub mod Staker { - use starknet::storage::VecTrait; - use core::num::traits::zero::{Zero}; use core::integer::{u512, u512_safe_div_rem_by_u256}; + use core::num::traits::zero::{Zero}; + use crate::staker_log::{LogOperations, MAX_FP, StakingLog}; use governance::interfaces::erc20::{IERC20Dispatcher, IERC20DispatcherTrait}; + use starknet::storage::VecTrait; use starknet::storage::{ Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess, }; - use crate::staker_log::{StakingLog, LogOperations, MAX_FP}; use starknet::{ - get_block_timestamp, get_caller_address, get_contract_address, - storage_access::{StorePacking}, ContractAddress, + ContractAddress, get_block_timestamp, get_caller_address, get_contract_address, + storage_access::{StorePacking}, }; use super::{IStaker}; @@ -84,7 +84,7 @@ pub mod Staker { const TWO_POW_64: u128 = 0x10000000000000000; const TWO_POW_192: u256 = 0x1000000000000000000000000000000000000000000000000; const TWO_POW_192_DIVISOR: NonZero = 0x1000000000000000000000000000000000000000000000000; - const TWO_POW_127: u128 = 0x80000000000000000000000000000000_u128; + const TWO_POW_127: u128 = 0x80000000000000000000000000000000_u128; pub(crate) impl DelegatedSnapshotStorePacking of StorePacking { fn pack(value: DelegatedSnapshot) -> felt252 { @@ -262,12 +262,12 @@ pub mod Staker { self .amount_delegated .write(delegate, self.insert_snapshot(delegate, get_block_timestamp()) + amount); - + let total_staked = self.total_staked.read(); self.total_staked.write(total_staked + amount); - self.staking_log.log_change(amount, total_staked); - + self.staking_log.log_change(amount, total_staked); + self.emit(Staked { from, delegate, amount }); } @@ -295,11 +295,11 @@ pub mod Staker { .amount_delegated .write(delegate, self.insert_snapshot(delegate, get_block_timestamp()) - amount); assert(self.token.read().transfer(recipient, amount.into()), 'TRANSFER_FAILED'); - - let total_staked = self.total_staked.read(); + + let total_staked = self.total_staked.read(); self.total_staked.write(total_staked - amount); self.staking_log.log_change(amount, total_staked); - + self.emit(Withdrawn { from, delegate, to: recipient, amount }); } @@ -354,36 +354,41 @@ pub mod Staker { self.get_average_delegated(delegate, now - period, now) } - fn get_cumulative_seconds_per_total_staked_at(self: @ContractState, timestamp: u64) -> u256 { + fn get_cumulative_seconds_per_total_staked_at( + self: @ContractState, timestamp: u64, + ) -> u256 { let timestamp_seconds = timestamp / 1000; - if let Option::Some((log_record, idx)) = self.staking_log.find_in_change_log(timestamp_seconds) { + if let Option::Some((log_record, idx)) = self + .staking_log + .find_in_change_log(timestamp_seconds) { let total_staked = if (idx == self.staking_log.len() - 1) { // if last rescord found self.total_staked.read() } else { // otherwise calculate using cumulative_seconds_per_total_staked difference - let next_log_record = self.staking_log.at(idx+1).read(); - + let next_log_record = self.staking_log.at(idx + 1).read(); + // substract fixed point values - let divisor = next_log_record.cumulative_seconds_per_total_staked - log_record.cumulative_seconds_per_total_staked; + let divisor = next_log_record.cumulative_seconds_per_total_staked + - log_record.cumulative_seconds_per_total_staked; assert(divisor.high < MAX_FP, 'FP_OVERFLOW'); - + if divisor.is_zero() { return 0_u64.into(); } - let diff_seconds: u128 = (next_log_record.timestamp - log_record.timestamp).into(); + let diff_seconds: u128 = (next_log_record.timestamp - log_record.timestamp) + .into(); // Divide u64 by fixed point let (total_staked_fp_medium, _) = u512_safe_div_rem_by_u256( u512 { limb0: 0, limb1: 0, limb2: 0, limb3: diff_seconds }, - divisor.try_into().unwrap() + divisor.try_into().unwrap(), ); - + let total_staked_fp = u256 { - low: total_staked_fp_medium.limb1, - high: total_staked_fp_medium.limb2, + low: total_staked_fp_medium.limb1, high: total_staked_fp_medium.limb2, }; assert(total_staked_fp.high < MAX_FP, 'FP_OVERFLOW'); @@ -395,24 +400,21 @@ pub mod Staker { 0 } }; - + let seconds_diff = timestamp_seconds - log_record.timestamp; - + let staked_seconds: u256 = if total_staked == 0 { 0_u256 } else { // Divide u64 by u128 - u256 { - low: 0, - high: seconds_diff.into() - } / total_staked.into() + u256 { low: 0, high: seconds_diff.into() } / total_staked.into() }; // Sum fixed posits let result = log_record.cumulative_seconds_per_total_staked + staked_seconds; assert(result.high < MAX_FP, 'FP_OVERFLOW'); return result; - } + } return 0_u256; } diff --git a/src/staker_log.cairo b/src/staker_log.cairo index 7c857c6..3be4972 100644 --- a/src/staker_log.cairo +++ b/src/staker_log.cairo @@ -1,15 +1,12 @@ use starknet::storage::MutableVecTrait; -use starknet::{get_block_timestamp}; -use starknet::storage_access::{StorePacking}; - -use starknet::storage::{ - Vec, VecTrait -}; use starknet::storage::{ - StorageBase, Mutable, StorageAsPath, - StoragePointerReadAccess, StoragePointerWriteAccess + Mutable, StorageAsPath, StorageBase, StoragePointerReadAccess, StoragePointerWriteAccess, }; +use starknet::storage::{Vec, VecTrait}; +use starknet::storage_access::{StorePacking}; +use starknet::{get_block_timestamp}; + pub type StakingLog = Vec; const TWO_POW_32: u64 = 0x100000000_u64; @@ -20,20 +17,20 @@ pub const MAX_FP: u128 = 0x8000000000000110000000000000000_u128; #[derive(Drop, Serde, Copy)] pub(crate) struct StakingLogRecord { pub(crate) timestamp: u64, - // Only 128+32=160 bits are used - pub(crate) cumulative_total_staked: u256, - pub(crate) cumulative_seconds_per_total_staked: u256, + pub(crate) cumulative_total_staked: u256, + pub(crate) cumulative_seconds_per_total_staked: u256, } #[generate_trait] pub impl StakingLogOperations of LogOperations { - fn get_total_staked(self: @StorageBase, timestamp: u64) -> Option { Option::Some(0) } - fn find_in_change_log(self: @StorageBase, timestamp: u64) -> Option<(StakingLogRecord, u64)> { + fn find_in_change_log( + self: @StorageBase, timestamp: u64, + ) -> Option<(StakingLogRecord, u64)> { let log = self.as_path(); if log.len() == 0 { return Option::None; @@ -43,12 +40,12 @@ pub impl StakingLogOperations of LogOperations { // To avoid reading from the storage multiple times. let mut result_ptr: Option<(StakingLogRecord, u64)> = Option::None; - + while (left <= right) { let center = (right + left) / 2; let record_ptr = log.at(center); let record = record_ptr.read(); - + if record.timestamp <= timestamp { result_ptr = Option::Some((record, center)); left = center + 1; @@ -60,7 +57,7 @@ pub impl StakingLogOperations of LogOperations { if let Option::Some((result, idx)) = result_ptr { return Option::Some((result, idx)); } - + return Option::None; } @@ -70,24 +67,26 @@ pub impl StakingLogOperations of LogOperations { let block_timestamp = get_block_timestamp() / 1000; if log.len() == 0 { - log.append().write( - StakingLogRecord { - timestamp: block_timestamp, - cumulative_total_staked: 0_u256, - cumulative_seconds_per_total_staked: 0_u64.into(), - } - ); - + log + .append() + .write( + StakingLogRecord { + timestamp: block_timestamp, + cumulative_total_staked: 0_u256, + cumulative_seconds_per_total_staked: 0_u64.into(), + }, + ); + return; } let last_record_ptr = log.at(log.len() - 1); - + let mut last_record = last_record_ptr.read(); let mut record = if last_record.timestamp == block_timestamp { // update record - last_record_ptr + last_record_ptr } else { // create new record log.append() @@ -95,7 +94,7 @@ pub impl StakingLogOperations of LogOperations { // Might be zero let seconds_diff = block_timestamp - last_record.timestamp; - + let total_staked_by_elapsed_seconds = total_staked.into() * seconds_diff.into(); let staked_seconds_per_total_staked: u256 = if total_staked == 0 { @@ -107,44 +106,50 @@ pub impl StakingLogOperations of LogOperations { }; // Add a new record. - record.write( - StakingLogRecord { - timestamp: block_timestamp, - - cumulative_total_staked: - last_record.cumulative_total_staked + total_staked_by_elapsed_seconds, - - cumulative_seconds_per_total_staked: - last_record.cumulative_seconds_per_total_staked + staked_seconds_per_total_staked, - } - ); + record + .write( + StakingLogRecord { + timestamp: block_timestamp, + cumulative_total_staked: last_record.cumulative_total_staked + + total_staked_by_elapsed_seconds, + cumulative_seconds_per_total_staked: last_record + .cumulative_seconds_per_total_staked + + staked_seconds_per_total_staked, + }, + ); } } -// +// // Storage layout for StakingLogRecord -// +// pub(crate) impl StakingLogRecordStorePacking of StorePacking { fn pack(value: StakingLogRecord) -> (felt252, felt252) { - let packed_ts_cumulative_total_staked: felt252 = - pack_u64_u256_tuple(value.timestamp, value.cumulative_total_staked); - - let cumulative_seconds_per_total_staked: felt252 = value.cumulative_seconds_per_total_staked + let packed_ts_cumulative_total_staked: felt252 = pack_u64_u256_tuple( + value.timestamp, value.cumulative_total_staked, + ); + + let cumulative_seconds_per_total_staked: felt252 = value + .cumulative_seconds_per_total_staked .try_into() .unwrap(); - + (packed_ts_cumulative_total_staked, cumulative_seconds_per_total_staked) } fn unpack(value: (felt252, felt252)) -> StakingLogRecord { let (packed_ts_cumulative_total_staked, cumulative_seconds_per_total_staked) = value; - let (timestamp, cumulative_total_staked) = unpack_u64_u256_tuple(packed_ts_cumulative_total_staked); - + let (timestamp, cumulative_total_staked) = unpack_u64_u256_tuple( + packed_ts_cumulative_total_staked, + ); + StakingLogRecord { timestamp: timestamp, cumulative_total_staked: cumulative_total_staked, - cumulative_seconds_per_total_staked: cumulative_seconds_per_total_staked.try_into().unwrap(), + cumulative_seconds_per_total_staked: cumulative_seconds_per_total_staked + .try_into() + .unwrap(), } } } @@ -154,19 +159,20 @@ pub(crate) fn pack_u64_u256_tuple(val1: u64, val2: u256) -> felt252 { u256 { high: val1.into() * TWO_POW_32.into() + cumulative_total_staked_high_32_bits.into(), low: val2.low, - }.try_into().unwrap() + } + .try_into() + .unwrap() } pub(crate) fn unpack_u64_u256_tuple(value: felt252) -> (u64, u256) { let packed_ts_total_staked_u256: u256 = value.into(); - + let cumulative_total_staked = u256 { - high: packed_ts_total_staked_u256.high & MASK_32_BITS, - low: packed_ts_total_staked_u256.low + high: packed_ts_total_staked_u256.high & MASK_32_BITS, low: packed_ts_total_staked_u256.low, }; - + return ( (packed_ts_total_staked_u256.high / TWO_POW_32.into()).try_into().unwrap(), - cumulative_total_staked + cumulative_total_staked, ); } diff --git a/src/staker_log_test.cairo b/src/staker_log_test.cairo index 45509e5..63e5934 100644 --- a/src/staker_log_test.cairo +++ b/src/staker_log_test.cairo @@ -1,7 +1,4 @@ -use crate::staker_log::{ - pack_u64_u256_tuple, - unpack_u64_u256_tuple, -}; +use crate::staker_log::{pack_u64_u256_tuple, unpack_u64_u256_tuple}; const MASK_32_BITS: u128 = 0x100000000_u128 - 1; const MASK_64_BITS: u128 = 0x10000000000000000_u128 - 1; @@ -20,7 +17,7 @@ fn assert_packs_and_unpacks(timestamp: u64, total_staked: u256) { assert_eq!(last_64_bits, timestamp); let (unpacked_timestamp, unpacked_cumulative_total_staked) = unpack_u64_u256_tuple( - packed.try_into().unwrap() + packed.try_into().unwrap(), ); assert_eq!(unpacked_timestamp, timestamp); assert_eq!(unpacked_cumulative_total_staked, total_staked); @@ -30,5 +27,7 @@ fn assert_packs_and_unpacks(timestamp: u64, total_staked: u256) { fn test_staking_log_packing() { assert_packs_and_unpacks(0_u64, 0_u256); assert_packs_and_unpacks(10_u64, 50_u256); - assert_packs_and_unpacks(0xffffffffffffffff_u64, 0xffffffffffffffffffffffffffffffffffffffff_u256) + assert_packs_and_unpacks( + 0xffffffffffffffff_u64, 0xffffffffffffffffffffffffffffffffffffffff_u256, + ) } diff --git a/src/staker_test.cairo b/src/staker_test.cairo index 3b9e597..6b42ad7 100644 --- a/src/staker_test.cairo +++ b/src/staker_test.cairo @@ -8,11 +8,7 @@ use governance::staker::{ use governance::test::test_token::{TestToken, deploy as deploy_token}; use starknet::testing::{pop_log, set_block_timestamp}; -use starknet::{ - contract_address_const, - get_contract_address, - syscalls::deploy_syscall -}; +use starknet::{contract_address_const, get_contract_address, syscalls::deploy_syscall}; pub(crate) fn setup(amount: u256) -> (IStakerDispatcher, IERC20Dispatcher) { let token = deploy_token(get_contract_address(), amount); @@ -403,35 +399,41 @@ fn test_delegate_undelegate() { mod staker_staked_seconds_per_total_staked_calculation { use starknet::{get_caller_address}; - use super::{ - setup, contract_address_const, set_block_timestamp, - IERC20DispatcherTrait, IStakerDispatcherTrait, + use super::{ + IERC20DispatcherTrait, IStakerDispatcherTrait, contract_address_const, set_block_timestamp, + setup, }; #[test] fn test_should_return_0_if_no_data_found() { let (staker, _) = setup(10000); - assert_eq!(staker.get_cumulative_seconds_per_total_staked_at(0), u256{ high: 0, low: 0_u128.into()}); - assert_eq!(staker.get_cumulative_seconds_per_total_staked_at(1000), u256{ high: 0, low: 0_u128.into()}); + assert_eq!( + staker.get_cumulative_seconds_per_total_staked_at(0), + u256 { high: 0, low: 0_u128.into() }, + ); + assert_eq!( + staker.get_cumulative_seconds_per_total_staked_at(1000), + u256 { high: 0, low: 0_u128.into() }, + ); } #[test] #[should_panic(expected: ('INSUFFICIENT_AMOUNT_STAKED', 'ENTRYPOINT_FAILED'))] fn test_raises_error_if_no_history_exists_and_withdrawal_happens() { - // TODO(biatcode): This test accidentally tests other + // TODO(biatcode): This test accidentally tests other // functionality and should be refactored - + let (staker, token) = setup(10000); // Caller is token owner let token_owner = get_caller_address(); - + // Adress to delegate tokens to let delegatee = contract_address_const::<1234567890>(); - token.approve(staker.contract_address, 10000); - + token.approve(staker.contract_address, 10000); + set_block_timestamp(1000); staker.stake_amount(delegatee, 1000); set_block_timestamp(2000); @@ -450,12 +452,12 @@ mod staker_staked_seconds_per_total_staked_calculation { let token_owner = get_caller_address(); // Allow staker contract to spend 2 tokens from owner account - token.approve(staker.contract_address, 2); + token.approve(staker.contract_address, 2); // Adress to delegate tokens to let delegatee = contract_address_const::<1234567890>(); - - set_block_timestamp(0); + + set_block_timestamp(0); staker.stake(delegatee); // Will transfer 2 token to contract account and setup delegatee set_block_timestamp(5000); // 5 seconds passed @@ -465,25 +467,60 @@ mod staker_staked_seconds_per_total_staked_calculation { staker.withdraw(delegatee, token_owner); // Will withdraw all 10 tokens back to owner assert(staker.get_staked(delegatee, token_owner) == 0, 'Not all tokens were withdrawn'); - + set_block_timestamp(10000); - token.approve(staker.contract_address, 7); + token.approve(staker.contract_address, 7); staker.stake(delegatee); // Will transfer 7 token to contract account and setup delegatee - - assert_eq!(staker.get_cumulative_seconds_per_total_staked_at(0), u256 {high: 0, low: 0_u128}); - assert_eq!(staker.get_cumulative_seconds_per_total_staked_at(500), u256 {high: 0, low: 0_u128}); - assert_eq!(staker.get_cumulative_seconds_per_total_staked_at(999), u256 {high: 0, low: 0_u128}); - assert_eq!(staker.get_cumulative_seconds_per_total_staked_at(1000), u256 {high: 0, low: 0x80000000000000000000000000000000_u128}); - assert_eq!(staker.get_cumulative_seconds_per_total_staked_at(2000), u256 {high: 1, low: 0_u128}); - assert_eq!(staker.get_cumulative_seconds_per_total_staked_at(3000), u256 {high: 1, low: 0x80000000000000000000000000000000_u128}); - assert_eq!(staker.get_cumulative_seconds_per_total_staked_at(4000), u256 {high: 2, low: 0_u128}); - assert_eq!(staker.get_cumulative_seconds_per_total_staked_at(5000), u256 {high: 0, low: 0_u128}); - assert_eq!(staker.get_cumulative_seconds_per_total_staked_at(6000), u256 {high: 0, low: 0_u128}); - assert_eq!(staker.get_cumulative_seconds_per_total_staked_at(7000), u256 {high: 0, low: 0_u128}); - assert_eq!(staker.get_cumulative_seconds_per_total_staked_at(8000), u256 {high: 0, low: 0_u128}); - assert_eq!(staker.get_cumulative_seconds_per_total_staked_at(9000), u256 {high: 0, low: 0_u128}); - assert_eq!(staker.get_cumulative_seconds_per_total_staked_at(10000), u256 {high: 2, low: 0x80000000000000000000000000000000_u128}); - assert_eq!(staker.get_cumulative_seconds_per_total_staked_at(17000), u256 {high: 3, low: 0x80000000000000000000000000000000_u128}); - assert_eq!(staker.get_cumulative_seconds_per_total_staked_at(24000), u256 {high: 4, low: 0x80000000000000000000000000000000_u128}); + + assert_eq!( + staker.get_cumulative_seconds_per_total_staked_at(0), u256 { high: 0, low: 0_u128 }, + ); + assert_eq!( + staker.get_cumulative_seconds_per_total_staked_at(500), u256 { high: 0, low: 0_u128 }, + ); + assert_eq!( + staker.get_cumulative_seconds_per_total_staked_at(999), u256 { high: 0, low: 0_u128 }, + ); + assert_eq!( + staker.get_cumulative_seconds_per_total_staked_at(1000), + u256 { high: 0, low: 0x80000000000000000000000000000000_u128 }, + ); + assert_eq!( + staker.get_cumulative_seconds_per_total_staked_at(2000), u256 { high: 1, low: 0_u128 }, + ); + assert_eq!( + staker.get_cumulative_seconds_per_total_staked_at(3000), + u256 { high: 1, low: 0x80000000000000000000000000000000_u128 }, + ); + assert_eq!( + staker.get_cumulative_seconds_per_total_staked_at(4000), u256 { high: 2, low: 0_u128 }, + ); + assert_eq!( + staker.get_cumulative_seconds_per_total_staked_at(5000), u256 { high: 0, low: 0_u128 }, + ); + assert_eq!( + staker.get_cumulative_seconds_per_total_staked_at(6000), u256 { high: 0, low: 0_u128 }, + ); + assert_eq!( + staker.get_cumulative_seconds_per_total_staked_at(7000), u256 { high: 0, low: 0_u128 }, + ); + assert_eq!( + staker.get_cumulative_seconds_per_total_staked_at(8000), u256 { high: 0, low: 0_u128 }, + ); + assert_eq!( + staker.get_cumulative_seconds_per_total_staked_at(9000), u256 { high: 0, low: 0_u128 }, + ); + assert_eq!( + staker.get_cumulative_seconds_per_total_staked_at(10000), + u256 { high: 2, low: 0x80000000000000000000000000000000_u128 }, + ); + assert_eq!( + staker.get_cumulative_seconds_per_total_staked_at(17000), + u256 { high: 3, low: 0x80000000000000000000000000000000_u128 }, + ); + assert_eq!( + staker.get_cumulative_seconds_per_total_staked_at(24000), + u256 { high: 4, low: 0x80000000000000000000000000000000_u128 }, + ); } -} \ No newline at end of file +} From f622f340b9b23891a10c33cb40e58c55c915a73f Mon Sep 17 00:00:00 2001 From: baitcode Date: Wed, 15 Jan 2025 00:37:19 +0300 Subject: [PATCH 36/40] Review fixes --- src/staker.cairo | 6 ++---- src/staker_log.cairo | 2 +- src/staker_test.cairo | 46 +++++++++++++++++++++---------------------- 3 files changed, 25 insertions(+), 29 deletions(-) diff --git a/src/staker.cairo b/src/staker.cairo index a90ef93..a070fa6 100644 --- a/src/staker.cairo +++ b/src/staker.cairo @@ -357,11 +357,10 @@ pub mod Staker { fn get_cumulative_seconds_per_total_staked_at( self: @ContractState, timestamp: u64, ) -> u256 { - let timestamp_seconds = timestamp / 1000; if let Option::Some((log_record, idx)) = self .staking_log - .find_in_change_log(timestamp_seconds) { + .find_in_change_log(timestamp) { let total_staked = if (idx == self.staking_log.len() - 1) { // if last rescord found self.total_staked.read() @@ -372,7 +371,6 @@ pub mod Staker { // substract fixed point values let divisor = next_log_record.cumulative_seconds_per_total_staked - log_record.cumulative_seconds_per_total_staked; - assert(divisor.high < MAX_FP, 'FP_OVERFLOW'); if divisor.is_zero() { return 0_u64.into(); @@ -401,7 +399,7 @@ pub mod Staker { } }; - let seconds_diff = timestamp_seconds - log_record.timestamp; + let seconds_diff = timestamp - log_record.timestamp; let staked_seconds: u256 = if total_staked == 0 { 0_u256 diff --git a/src/staker_log.cairo b/src/staker_log.cairo index 3be4972..b35b499 100644 --- a/src/staker_log.cairo +++ b/src/staker_log.cairo @@ -64,7 +64,7 @@ pub impl StakingLogOperations of LogOperations { fn log_change(self: StorageBase>, amount: u128, total_staked: u128) { let log = self.as_path(); - let block_timestamp = get_block_timestamp() / 1000; + let block_timestamp = get_block_timestamp(); if log.len() == 0 { log diff --git a/src/staker_test.cairo b/src/staker_test.cairo index 6b42ad7..a056748 100644 --- a/src/staker_test.cairo +++ b/src/staker_test.cairo @@ -434,13 +434,13 @@ mod staker_staked_seconds_per_total_staked_calculation { token.approve(staker.contract_address, 10000); - set_block_timestamp(1000); + set_block_timestamp(1); staker.stake_amount(delegatee, 1000); - set_block_timestamp(2000); + set_block_timestamp(2); staker.withdraw_amount(delegatee, token_owner, 500); - set_block_timestamp(3000); + set_block_timestamp(3); staker.stake_amount(delegatee, 1000); - set_block_timestamp(4000); + set_block_timestamp(4); staker.withdraw_amount(delegatee, token_owner, 2000); } @@ -460,7 +460,7 @@ mod staker_staked_seconds_per_total_staked_calculation { set_block_timestamp(0); staker.stake(delegatee); // Will transfer 2 token to contract account and setup delegatee - set_block_timestamp(5000); // 5 seconds passed + set_block_timestamp(5); // 5 seconds passed assert(staker.get_staked(token_owner, delegatee) == 2, 'Something went wrong'); @@ -468,58 +468,56 @@ mod staker_staked_seconds_per_total_staked_calculation { assert(staker.get_staked(delegatee, token_owner) == 0, 'Not all tokens were withdrawn'); - set_block_timestamp(10000); + set_block_timestamp(10); token.approve(staker.contract_address, 7); staker.stake(delegatee); // Will transfer 7 token to contract account and setup delegatee assert_eq!( staker.get_cumulative_seconds_per_total_staked_at(0), u256 { high: 0, low: 0_u128 }, ); + let z = staker.get_cumulative_seconds_per_total_staked_at(1); + assert_eq!(z.high, 0_u128); + assert_eq!(z.low, 0x80000000000000000000000000000000_u128); + assert_eq!( - staker.get_cumulative_seconds_per_total_staked_at(500), u256 { high: 0, low: 0_u128 }, - ); - assert_eq!( - staker.get_cumulative_seconds_per_total_staked_at(999), u256 { high: 0, low: 0_u128 }, - ); - assert_eq!( - staker.get_cumulative_seconds_per_total_staked_at(1000), + staker.get_cumulative_seconds_per_total_staked_at(1), u256 { high: 0, low: 0x80000000000000000000000000000000_u128 }, ); assert_eq!( - staker.get_cumulative_seconds_per_total_staked_at(2000), u256 { high: 1, low: 0_u128 }, + staker.get_cumulative_seconds_per_total_staked_at(2), u256 { high: 1, low: 0_u128 }, ); assert_eq!( - staker.get_cumulative_seconds_per_total_staked_at(3000), + staker.get_cumulative_seconds_per_total_staked_at(3), u256 { high: 1, low: 0x80000000000000000000000000000000_u128 }, ); assert_eq!( - staker.get_cumulative_seconds_per_total_staked_at(4000), u256 { high: 2, low: 0_u128 }, + staker.get_cumulative_seconds_per_total_staked_at(4), u256 { high: 2, low: 0_u128 }, ); assert_eq!( - staker.get_cumulative_seconds_per_total_staked_at(5000), u256 { high: 0, low: 0_u128 }, + staker.get_cumulative_seconds_per_total_staked_at(5), u256 { high: 0, low: 0_u128 }, ); assert_eq!( - staker.get_cumulative_seconds_per_total_staked_at(6000), u256 { high: 0, low: 0_u128 }, + staker.get_cumulative_seconds_per_total_staked_at(6), u256 { high: 0, low: 0_u128 }, ); assert_eq!( - staker.get_cumulative_seconds_per_total_staked_at(7000), u256 { high: 0, low: 0_u128 }, + staker.get_cumulative_seconds_per_total_staked_at(7), u256 { high: 0, low: 0_u128 }, ); assert_eq!( - staker.get_cumulative_seconds_per_total_staked_at(8000), u256 { high: 0, low: 0_u128 }, + staker.get_cumulative_seconds_per_total_staked_at(8), u256 { high: 0, low: 0_u128 }, ); assert_eq!( - staker.get_cumulative_seconds_per_total_staked_at(9000), u256 { high: 0, low: 0_u128 }, + staker.get_cumulative_seconds_per_total_staked_at(9), u256 { high: 0, low: 0_u128 }, ); assert_eq!( - staker.get_cumulative_seconds_per_total_staked_at(10000), + staker.get_cumulative_seconds_per_total_staked_at(10), u256 { high: 2, low: 0x80000000000000000000000000000000_u128 }, ); assert_eq!( - staker.get_cumulative_seconds_per_total_staked_at(17000), + staker.get_cumulative_seconds_per_total_staked_at(17), u256 { high: 3, low: 0x80000000000000000000000000000000_u128 }, ); assert_eq!( - staker.get_cumulative_seconds_per_total_staked_at(24000), + staker.get_cumulative_seconds_per_total_staked_at(24), u256 { high: 4, low: 0x80000000000000000000000000000000_u128 }, ); } From 56ec424795d2fa2344a296bfeb0acdc9e0f59e91 Mon Sep 17 00:00:00 2001 From: baitcode Date: Wed, 15 Jan 2025 00:37:55 +0300 Subject: [PATCH 37/40] scarb format --- src/staker.cairo | 1 - src/staker_test.cairo | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/staker.cairo b/src/staker.cairo index a070fa6..423d1ae 100644 --- a/src/staker.cairo +++ b/src/staker.cairo @@ -357,7 +357,6 @@ pub mod Staker { fn get_cumulative_seconds_per_total_staked_at( self: @ContractState, timestamp: u64, ) -> u256 { - if let Option::Some((log_record, idx)) = self .staking_log .find_in_change_log(timestamp) { diff --git a/src/staker_test.cairo b/src/staker_test.cairo index a056748..25b0792 100644 --- a/src/staker_test.cairo +++ b/src/staker_test.cairo @@ -477,7 +477,7 @@ mod staker_staked_seconds_per_total_staked_calculation { ); let z = staker.get_cumulative_seconds_per_total_staked_at(1); assert_eq!(z.high, 0_u128); - assert_eq!(z.low, 0x80000000000000000000000000000000_u128); + assert_eq!(z.low, 0x80000000000000000000000000000000_u128); assert_eq!( staker.get_cumulative_seconds_per_total_staked_at(1), From 974c68df7ba4b16d87596e6cc3ca9d2c1754f082 Mon Sep 17 00:00:00 2001 From: baitcode Date: Thu, 23 Jan 2025 23:58:53 +0300 Subject: [PATCH 38/40] * remove MAX_FP related code * find_in_change_log -> find_change_log_on_or_before_timestamp * log_change argument rename --- src/staker.cairo | 16 ++++++---------- src/staker_log.cairo | 12 +++++------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/staker.cairo b/src/staker.cairo index 423d1ae..7270440 100644 --- a/src/staker.cairo +++ b/src/staker.cairo @@ -61,7 +61,7 @@ pub trait IStaker { pub mod Staker { use core::integer::{u512, u512_safe_div_rem_by_u256}; use core::num::traits::zero::{Zero}; - use crate::staker_log::{LogOperations, MAX_FP, StakingLog}; + use crate::staker_log::{LogOperations, StakingLog}; use governance::interfaces::erc20::{IERC20Dispatcher, IERC20DispatcherTrait}; use starknet::storage::VecTrait; use starknet::storage::{ @@ -263,10 +263,10 @@ pub mod Staker { .amount_delegated .write(delegate, self.insert_snapshot(delegate, get_block_timestamp()) + amount); - let total_staked = self.total_staked.read(); + let current_total_staked = self.total_staked.read(); - self.total_staked.write(total_staked + amount); - self.staking_log.log_change(amount, total_staked); + self.total_staked.write(current_total_staked + amount); + self.staking_log.log_change(amount, current_total_staked); self.emit(Staked { from, delegate, amount }); } @@ -359,7 +359,7 @@ pub mod Staker { ) -> u256 { if let Option::Some((log_record, idx)) = self .staking_log - .find_in_change_log(timestamp) { + .find_change_log_on_or_before_timestamp(timestamp) { let total_staked = if (idx == self.staking_log.len() - 1) { // if last rescord found self.total_staked.read() @@ -388,8 +388,6 @@ pub mod Staker { low: total_staked_fp_medium.limb1, high: total_staked_fp_medium.limb2, }; - assert(total_staked_fp.high < MAX_FP, 'FP_OVERFLOW'); - // round value total_staked_fp.high + if (total_staked_fp.low >= TWO_POW_127) { 1 @@ -408,9 +406,7 @@ pub mod Staker { }; // Sum fixed posits - let result = log_record.cumulative_seconds_per_total_staked + staked_seconds; - assert(result.high < MAX_FP, 'FP_OVERFLOW'); - return result; + return log_record.cumulative_seconds_per_total_staked + staked_seconds; } return 0_u256; diff --git a/src/staker_log.cairo b/src/staker_log.cairo index b35b499..781ff8d 100644 --- a/src/staker_log.cairo +++ b/src/staker_log.cairo @@ -12,7 +12,6 @@ pub type StakingLog = Vec; const TWO_POW_32: u64 = 0x100000000_u64; const MASK_32_BITS: u128 = 0x100000000_u128 - 1; const TWO_POW_160: u256 = 0x10000000000000000000000000000000000000000; -pub const MAX_FP: u128 = 0x8000000000000110000000000000000_u128; #[derive(Drop, Serde, Copy)] pub(crate) struct StakingLogRecord { @@ -28,7 +27,7 @@ pub impl StakingLogOperations of LogOperations { Option::Some(0) } - fn find_in_change_log( + fn find_change_log_on_or_before_timestamp( self: @StorageBase, timestamp: u64, ) -> Option<(StakingLogRecord, u64)> { let log = self.as_path(); @@ -61,7 +60,7 @@ pub impl StakingLogOperations of LogOperations { return Option::None; } - fn log_change(self: StorageBase>, amount: u128, total_staked: u128) { + fn log_change(self: StorageBase>, amount: u128, total_staked_before_change: u128) { let log = self.as_path(); let block_timestamp = get_block_timestamp(); @@ -95,13 +94,12 @@ pub impl StakingLogOperations of LogOperations { // Might be zero let seconds_diff = block_timestamp - last_record.timestamp; - let total_staked_by_elapsed_seconds = total_staked.into() * seconds_diff.into(); + let total_staked_by_elapsed_seconds = total_staked_before_change.into() * seconds_diff.into(); - let staked_seconds_per_total_staked: u256 = if total_staked == 0 { + let staked_seconds_per_total_staked: u256 = if total_staked_before_change == 0 { 0_u64.into() } else { - let res = u256 { low: 0, high: seconds_diff.into() } / total_staked.into(); - assert(res.high < MAX_FP, 'FP_OVERFLOW'); + let res = u256 { low: 0, high: seconds_diff.into() } / total_staked_before_change.into(); res }; From 955ef24370b3ed6257f7e5977f99947b91d0a9c7 Mon Sep 17 00:00:00 2001 From: baitcode Date: Fri, 31 Jan 2025 04:48:12 +0300 Subject: [PATCH 39/40] simplified and renamed get_seconds_per_total_staked_sum_at added get_time_weighted_total_staked_sum_at added get_total_staked_at to staker added get_user_share_of_total_staked_over_period to staker added get_average_total_staked_over_period to staker --- src/lib.cairo | 12 +-- src/staker.cairo | 144 ++++++++++++++++++++--------- src/staker_log.cairo | 48 +++++----- src/staker_test.cairo | 207 +++++++++++++++++++++++++++++------------- 4 files changed, 278 insertions(+), 133 deletions(-) diff --git a/src/lib.cairo b/src/lib.cairo index 7d41603..15df3c3 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -1,7 +1,7 @@ pub mod airdrop; mod airdrop_claim_check; -#[cfg(test)] -mod airdrop_test; +// #[cfg(test)] +// mod airdrop_test; pub mod call_trait; #[cfg(test)] @@ -12,14 +12,14 @@ pub mod execution_state; mod execution_state_test; pub mod governor; -#[cfg(test)] -mod governor_test; +// #[cfg(test)] +// mod governor_test; pub mod staker; pub mod staker_log; -#[cfg(test)] -pub mod staker_log_test; +// #[cfg(test)] +// pub mod staker_log_test; #[cfg(test)] mod staker_test; diff --git a/src/staker.cairo b/src/staker.cairo index 7270440..fcc5cd3 100644 --- a/src/staker.cairo +++ b/src/staker.cairo @@ -53,13 +53,35 @@ pub trait IStaker { self: @TContractState, delegate: ContractAddress, period: u64, ) -> u128; - // Gets the cumulative staked amount * per second staked for the given timestamp and account. - fn get_cumulative_seconds_per_total_staked_at(self: @TContractState, timestamp: u64) -> u256; + // Calculates snapshot for seconds_per_total_staked_sum (val) at given timestamp (ts). + // If timestamp if before first record, returns 0. + // If timestamp is between records, calculates Δt = (ts - record.ts) where record is + // first record in log before timestamp, then calculates total amount using the + // weighted_total_staked diff diveded by time diff. + // If timestamp is after last record, calculates Δt = (ts - last_record.ts) and + // takes total_staked from storage and adds Δt / total_staked to accumulator. + // In case total_staked is 0 this method turns is to 1 to simplify calculations + // TODO: this should be a part of StakingLog + fn get_seconds_per_total_staked_sum_at(self: @TContractState, timestamp: u64) -> u256; + + // Calculates snapshot for time_weighted_total_staked (val) at given timestamp (ts). + // Does pretty much the same as `get_seconds_per_total_staked_sum_at` but simpler due to + // absence of FP division. + fn get_time_weighted_total_staked_sum_at(self: @TContractState, timestamp: u64) -> u256; + + fn get_total_staked_at(self: @TContractState, timestamp: u64) -> u128; + + fn get_average_total_staked_over_period( + self: @TContractState, start: u64, end: u64, + ) -> u128; + + fn get_user_share_of_total_staked_over_period( + self: @TContractState, staked: u128, start: u64, end: u64, + ) -> u128; } #[starknet::contract] pub mod Staker { - use core::integer::{u512, u512_safe_div_rem_by_u256}; use core::num::traits::zero::{Zero}; use crate::staker_log::{LogOperations, StakingLog}; use governance::interfaces::erc20::{IERC20Dispatcher, IERC20DispatcherTrait}; @@ -354,62 +376,100 @@ pub mod Staker { self.get_average_delegated(delegate, now - period, now) } - fn get_cumulative_seconds_per_total_staked_at( + // Check interface for detailed description. + fn get_seconds_per_total_staked_sum_at( self: @ContractState, timestamp: u64, ) -> u256 { - if let Option::Some((log_record, idx)) = self - .staking_log - .find_change_log_on_or_before_timestamp(timestamp) { + let record = self.staking_log.find_record_on_or_before_timestamp(timestamp); + + if let Option::Some((record, idx)) = record { let total_staked = if (idx == self.staking_log.len() - 1) { - // if last rescord found + // if last record found self.total_staked.read() } else { - // otherwise calculate using cumulative_seconds_per_total_staked difference - let next_log_record = self.staking_log.at(idx + 1).read(); - - // substract fixed point values - let divisor = next_log_record.cumulative_seconds_per_total_staked - - log_record.cumulative_seconds_per_total_staked; - - if divisor.is_zero() { - return 0_u64.into(); - } - - let diff_seconds: u128 = (next_log_record.timestamp - log_record.timestamp) - .into(); - - // Divide u64 by fixed point - let (total_staked_fp_medium, _) = u512_safe_div_rem_by_u256( - u512 { limb0: 0, limb1: 0, limb2: 0, limb3: diff_seconds }, - divisor.try_into().unwrap(), - ); - - let total_staked_fp = u256 { - low: total_staked_fp_medium.limb1, high: total_staked_fp_medium.limb2, - }; - - // round value - total_staked_fp.high + if (total_staked_fp.low >= TWO_POW_127) { - 1 - } else { - 0 - } + // This helps to avoid couple of FP divisions. + let next_record = self.staking_log.at(idx + 1).read(); + let time_weighted_total_staked_sum_diff = next_record.time_weighted_total_staked_sum - record.time_weighted_total_staked_sum; + let timestamp_diff = next_record.timestamp - record.timestamp; + (time_weighted_total_staked_sum_diff / timestamp_diff.into()).try_into().unwrap() }; - let seconds_diff = timestamp - log_record.timestamp; + let seconds_diff = timestamp - record.timestamp; - let staked_seconds: u256 = if total_staked == 0 { - 0_u256 + let seconds_per_total_staked: u256 = if total_staked == 0 { + seconds_diff.into() // as if total_staked is 1 } else { // Divide u64 by u128 u256 { low: 0, high: seconds_diff.into() } / total_staked.into() }; // Sum fixed posits - return log_record.cumulative_seconds_per_total_staked + staked_seconds; + return record.seconds_per_total_staked_sum + seconds_per_total_staked; } return 0_u256; } + + fn get_time_weighted_total_staked_sum_at(self: @ContractState, timestamp: u64) -> u256 { + let record = self.staking_log.find_record_on_or_before_timestamp(timestamp); + + if let Option::Some((record, idx)) = record { + let total_staked = if (idx == self.staking_log.len() - 1) { + // if last rescord found + self.total_staked.read() + } else { + let next_record = self.staking_log.at(idx + 1).read(); + let time_weighted_total_staked_sum_diff = next_record.time_weighted_total_staked_sum - record.time_weighted_total_staked_sum; + let timestamp_diff = next_record.timestamp - record.timestamp; + (time_weighted_total_staked_sum_diff / timestamp_diff.into()).try_into().unwrap() + }; + + let seconds_diff = timestamp - record.timestamp; + let time_weighted_total_staked: u256 = total_staked.into() * seconds_diff.into(); + + return record.time_weighted_total_staked_sum + time_weighted_total_staked; + } + + return 0_u256; + } + + fn get_total_staked_at(self: @ContractState, timestamp: u64) -> u128 { + let record = self.staking_log.find_record_on_or_before_timestamp(timestamp); + if let Option::Some((record, idx)) = record { + if (idx == self.staking_log.len() - 1) { + self.total_staked.read() + } else { + let next_record = self.staking_log.at(idx + 1).read(); + let time_weighted_total_staked_sum_diff = next_record.time_weighted_total_staked_sum - record.time_weighted_total_staked_sum; + let timestamp_diff = next_record.timestamp - record.timestamp; + (time_weighted_total_staked_sum_diff / timestamp_diff.into()).try_into().unwrap() + } + } else { + 0_u128 + } + } + + fn get_average_total_staked_over_period( + self: @ContractState, start: u64, end: u64, + ) -> u128 { + assert(end > start, 'ORDER'); + + let start_snapshot = self.get_time_weighted_total_staked_sum_at(start); + let end_snapshot = self.get_time_weighted_total_staked_sum_at(end); + let period_length = end - start; + + ((end_snapshot - start_snapshot) / period_length.into()).try_into().unwrap() + } + + fn get_user_share_of_total_staked_over_period( + self: @ContractState, staked: u128, start: u64, end: u64, + ) -> u128 { + assert(end > start, 'ORDER'); + + let start_snapshot = self.get_seconds_per_total_staked_sum_at(start); + let end_snapshot = self.get_seconds_per_total_staked_sum_at(end); + + staked * ((end_snapshot - start_snapshot) * 100).high / (end - start).into() + } } } diff --git a/src/staker_log.cairo b/src/staker_log.cairo index 781ff8d..19d513f 100644 --- a/src/staker_log.cairo +++ b/src/staker_log.cairo @@ -7,6 +7,7 @@ use starknet::storage::{Vec, VecTrait}; use starknet::storage_access::{StorePacking}; use starknet::{get_block_timestamp}; + pub type StakingLog = Vec; const TWO_POW_32: u64 = 0x100000000_u64; @@ -17,23 +18,25 @@ const TWO_POW_160: u256 = 0x10000000000000000000000000000000000000000; pub(crate) struct StakingLogRecord { pub(crate) timestamp: u64, // Only 128+32=160 bits are used - pub(crate) cumulative_total_staked: u256, - pub(crate) cumulative_seconds_per_total_staked: u256, + pub(crate) time_weighted_total_staked_sum: u256, + pub(crate) seconds_per_total_staked_sum: u256, } #[generate_trait] pub impl StakingLogOperations of LogOperations { - fn get_total_staked(self: @StorageBase, timestamp: u64) -> Option { - Option::Some(0) - } - fn find_change_log_on_or_before_timestamp( + fn find_record_on_or_before_timestamp( self: @StorageBase, timestamp: u64, ) -> Option<(StakingLogRecord, u64)> { let log = self.as_path(); if log.len() == 0 { return Option::None; } + // TODO: Discuss with Moody, maybe it's worth to store that one globally + if log.at(0).read().timestamp > timestamp { + return Option::None; + } + let mut left = 0; let mut right = log.len() - 1; @@ -71,8 +74,8 @@ pub impl StakingLogOperations of LogOperations { .write( StakingLogRecord { timestamp: block_timestamp, - cumulative_total_staked: 0_u256, - cumulative_seconds_per_total_staked: 0_u64.into(), + time_weighted_total_staked_sum: 0_u256, + seconds_per_total_staked_sum: 0_u64.into(), }, ); @@ -94,7 +97,7 @@ pub impl StakingLogOperations of LogOperations { // Might be zero let seconds_diff = block_timestamp - last_record.timestamp; - let total_staked_by_elapsed_seconds = total_staked_before_change.into() * seconds_diff.into(); + let time_weighted_total_staked = total_staked_before_change.into() * seconds_diff.into(); let staked_seconds_per_total_staked: u256 = if total_staked_before_change == 0 { 0_u64.into() @@ -108,10 +111,9 @@ pub impl StakingLogOperations of LogOperations { .write( StakingLogRecord { timestamp: block_timestamp, - cumulative_total_staked: last_record.cumulative_total_staked - + total_staked_by_elapsed_seconds, - cumulative_seconds_per_total_staked: last_record - .cumulative_seconds_per_total_staked + time_weighted_total_staked_sum: last_record.time_weighted_total_staked_sum + + time_weighted_total_staked, + seconds_per_total_staked_sum: last_record.seconds_per_total_staked_sum + staked_seconds_per_total_staked, }, ); @@ -124,28 +126,28 @@ pub impl StakingLogOperations of LogOperations { pub(crate) impl StakingLogRecordStorePacking of StorePacking { fn pack(value: StakingLogRecord) -> (felt252, felt252) { - let packed_ts_cumulative_total_staked: felt252 = pack_u64_u256_tuple( - value.timestamp, value.cumulative_total_staked, + let val1: felt252 = pack_u64_u256_tuple( + value.timestamp, value.time_weighted_total_staked_sum, ); - let cumulative_seconds_per_total_staked: felt252 = value - .cumulative_seconds_per_total_staked + let val2: felt252 = value + .seconds_per_total_staked_sum .try_into() .unwrap(); - (packed_ts_cumulative_total_staked, cumulative_seconds_per_total_staked) + (val1, val2) } fn unpack(value: (felt252, felt252)) -> StakingLogRecord { - let (packed_ts_cumulative_total_staked, cumulative_seconds_per_total_staked) = value; - let (timestamp, cumulative_total_staked) = unpack_u64_u256_tuple( - packed_ts_cumulative_total_staked, + let (packed_ts_time_weighted_total_staked, seconds_per_total_staked_sum) = value; + let (timestamp, time_weighted_total_staked_sum) = unpack_u64_u256_tuple( + packed_ts_time_weighted_total_staked, ); StakingLogRecord { timestamp: timestamp, - cumulative_total_staked: cumulative_total_staked, - cumulative_seconds_per_total_staked: cumulative_seconds_per_total_staked + time_weighted_total_staked_sum: time_weighted_total_staked_sum, + seconds_per_total_staked_sum: seconds_per_total_staked_sum .try_into() .unwrap(), } diff --git a/src/staker_test.cairo b/src/staker_test.cairo index 25b0792..54f5174 100644 --- a/src/staker_test.cairo +++ b/src/staker_test.cairo @@ -409,11 +409,11 @@ mod staker_staked_seconds_per_total_staked_calculation { let (staker, _) = setup(10000); assert_eq!( - staker.get_cumulative_seconds_per_total_staked_at(0), + staker.get_seconds_per_total_staked_sum_at(0), u256 { high: 0, low: 0_u128.into() }, ); assert_eq!( - staker.get_cumulative_seconds_per_total_staked_at(1000), + staker.get_seconds_per_total_staked_sum_at(1000), u256 { high: 0, low: 0_u128.into() }, ); } @@ -445,80 +445,163 @@ mod staker_staked_seconds_per_total_staked_calculation { } #[test] - fn test_should_stake_10000_tokens_for_5_seconds_adding_10000_every_second_to_staked_seconds() { + fn test_check_total_staked_calculations() { let (staker, token) = setup(1000); // Caller is token owner - let token_owner = get_caller_address(); + let delegatee = contract_address_const::<1234567890>(); - // Allow staker contract to spend 2 tokens from owner account - token.approve(staker.contract_address, 2); + assert_eq!(staker.get_total_staked_at(0), 0); + assert_eq!(staker.get_total_staked_at(100), 0); - // Adress to delegate tokens to - let delegatee = contract_address_const::<1234567890>(); + set_block_timestamp(10); + token.approve(staker.contract_address, 100); + staker.stake(delegatee); + + set_block_timestamp(15); + token.approve(staker.contract_address, 300); + staker.stake(delegatee); + + set_block_timestamp(20); + token.approve(staker.contract_address, 200); + staker.stake(delegatee); + + set_block_timestamp(40); + token.approve(staker.contract_address, 100); + staker.stake(delegatee); + + set_block_timestamp(65); + token.approve(staker.contract_address, 300); + staker.stake(delegatee); + + assert_eq!(staker.get_total_staked_at(0), 0); + assert_eq!(staker.get_total_staked_at(5), 0); + assert_eq!(staker.get_total_staked_at(9), 0); + assert_eq!(staker.get_total_staked_at(10), 100); + assert_eq!(staker.get_total_staked_at(11), 100); + assert_eq!(staker.get_total_staked_at(14), 100); + assert_eq!(staker.get_total_staked_at(15), 400); + assert_eq!(staker.get_total_staked_at(19), 400); + assert_eq!(staker.get_total_staked_at(20), 600); + assert_eq!(staker.get_total_staked_at(30), 600); + assert_eq!(staker.get_total_staked_at(39), 600); + assert_eq!(staker.get_total_staked_at(40), 700); + assert_eq!(staker.get_total_staked_at(64), 700); + assert_eq!(staker.get_total_staked_at(65), 1000); + assert_eq!(staker.get_total_staked_at(100), 1000); + } - set_block_timestamp(0); - staker.stake(delegatee); // Will transfer 2 token to contract account and setup delegatee + #[test] + fn test_get_time_weighted_total_staked_sum_at() { + let (staker, token) = setup(1000); - set_block_timestamp(5); // 5 seconds passed + // Caller is token owner + let token_owner = get_caller_address(); + let delegatee = contract_address_const::<1234567890>(); - assert(staker.get_staked(token_owner, delegatee) == 2, 'Something went wrong'); + assert_eq!(staker.get_time_weighted_total_staked_sum_at(0), 0); + assert_eq!(staker.get_time_weighted_total_staked_sum_at(100), 0); - staker.withdraw(delegatee, token_owner); // Will withdraw all 10 tokens back to owner + set_block_timestamp(10); + token.approve(staker.contract_address, 100); + staker.stake(delegatee); + + assert_eq!(staker.get_time_weighted_total_staked_sum_at(0), 0); + assert_eq!(staker.get_time_weighted_total_staked_sum_at(9), 0); + assert_eq!(staker.get_time_weighted_total_staked_sum_at(10), 0); + assert_eq!(staker.get_time_weighted_total_staked_sum_at(11), 100); + assert_eq!(staker.get_time_weighted_total_staked_sum_at(12), 200); + assert_eq!(staker.get_time_weighted_total_staked_sum_at(13), 300); + + set_block_timestamp(15); + token.approve(staker.contract_address, 300); + staker.stake(delegatee); + + set_block_timestamp(20); + token.approve(staker.contract_address, 200); + staker.stake(delegatee); + + set_block_timestamp(40); + token.approve(staker.contract_address, 100); + staker.stake(delegatee); + + set_block_timestamp(65); + token.approve(staker.contract_address, 300); + staker.stake(delegatee); + + set_block_timestamp(70); + + staker.withdraw(delegatee, token_owner); + + assert_eq!(staker.get_time_weighted_total_staked_sum_at(0), 0); + assert_eq!(staker.get_time_weighted_total_staked_sum_at(5), 0); + assert_eq!(staker.get_time_weighted_total_staked_sum_at(9), 0); + assert_eq!(staker.get_time_weighted_total_staked_sum_at(10), 0); // 100/s + assert_eq!(staker.get_time_weighted_total_staked_sum_at(11), 100); + assert_eq!(staker.get_time_weighted_total_staked_sum_at(14), 400); + assert_eq!(staker.get_time_weighted_total_staked_sum_at(15), 500); // 400/s + assert_eq!(staker.get_time_weighted_total_staked_sum_at(19), 2100); + assert_eq!(staker.get_time_weighted_total_staked_sum_at(20), 2500); // 600/s + assert_eq!(staker.get_time_weighted_total_staked_sum_at(30), 8500); + assert_eq!(staker.get_time_weighted_total_staked_sum_at(39), 13900); + assert_eq!(staker.get_time_weighted_total_staked_sum_at(40), 14500); // 700/s + assert_eq!(staker.get_time_weighted_total_staked_sum_at(64), 31300); + assert_eq!(staker.get_time_weighted_total_staked_sum_at(65), 32000); // 1000/s + assert_eq!(staker.get_time_weighted_total_staked_sum_at(70), 37000); // 0/s + assert_eq!(staker.get_time_weighted_total_staked_sum_at(100), 37000); // 0/s + assert_eq!(staker.get_time_weighted_total_staked_sum_at(200), 37000); // 0/s + } + + #[test] + fn test_should_stake_10000_tokens_for_5_seconds_adding_10000_every_second_to_staked_seconds() { + let (staker, token) = setup(1000); - assert(staker.get_staked(delegatee, token_owner) == 0, 'Not all tokens were withdrawn'); + // Caller is token owner + let token_owner = get_caller_address(); + let delegatee = contract_address_const::<1234567890>(); set_block_timestamp(10); - token.approve(staker.contract_address, 7); - staker.stake(delegatee); // Will transfer 7 token to contract account and setup delegatee + token.approve(staker.contract_address, 10); + staker.stake(delegatee); - assert_eq!( - staker.get_cumulative_seconds_per_total_staked_at(0), u256 { high: 0, low: 0_u128 }, - ); - let z = staker.get_cumulative_seconds_per_total_staked_at(1); - assert_eq!(z.high, 0_u128); - assert_eq!(z.low, 0x80000000000000000000000000000000_u128); + set_block_timestamp(15); + token.approve(staker.contract_address, 10); + staker.stake(delegatee); + set_block_timestamp(20); + staker.withdraw(delegatee, token_owner); + + set_block_timestamp(30); + token.approve(staker.contract_address, 30); + staker.stake(delegatee); + + set_block_timestamp(40); + staker.withdraw(delegatee, token_owner); + + assert_eq!(staker.get_seconds_per_total_staked_sum_at(0), 0); + assert_eq!(staker.get_seconds_per_total_staked_sum_at(10), 0); + assert_eq!(staker.get_seconds_per_total_staked_sum_at(15), u256 { + low: 0x80000000000000000000000000000000_u128, + high: 0_u128, + }); // 1/2 assert_eq!( - staker.get_cumulative_seconds_per_total_staked_at(1), - u256 { high: 0, low: 0x80000000000000000000000000000000_u128 }, - ); - assert_eq!( - staker.get_cumulative_seconds_per_total_staked_at(2), u256 { high: 1, low: 0_u128 }, - ); - assert_eq!( - staker.get_cumulative_seconds_per_total_staked_at(3), - u256 { high: 1, low: 0x80000000000000000000000000000000_u128 }, - ); - assert_eq!( - staker.get_cumulative_seconds_per_total_staked_at(4), u256 { high: 2, low: 0_u128 }, - ); - assert_eq!( - staker.get_cumulative_seconds_per_total_staked_at(5), u256 { high: 0, low: 0_u128 }, - ); - assert_eq!( - staker.get_cumulative_seconds_per_total_staked_at(6), u256 { high: 0, low: 0_u128 }, - ); - assert_eq!( - staker.get_cumulative_seconds_per_total_staked_at(7), u256 { high: 0, low: 0_u128 }, - ); - assert_eq!( - staker.get_cumulative_seconds_per_total_staked_at(8), u256 { high: 0, low: 0_u128 }, - ); - assert_eq!( - staker.get_cumulative_seconds_per_total_staked_at(9), u256 { high: 0, low: 0_u128 }, - ); - assert_eq!( - staker.get_cumulative_seconds_per_total_staked_at(10), - u256 { high: 2, low: 0x80000000000000000000000000000000_u128 }, - ); - assert_eq!( - staker.get_cumulative_seconds_per_total_staked_at(17), - u256 { high: 3, low: 0x80000000000000000000000000000000_u128 }, - ); - assert_eq!( - staker.get_cumulative_seconds_per_total_staked_at(24), - u256 { high: 4, low: 0x80000000000000000000000000000000_u128 }, - ); + staker.get_seconds_per_total_staked_sum_at(20), + u256 { + low: 0xC0000000000000000000000000000000, + high: 0_u128, + } + ); // 3/4 + assert_eq!(staker.get_seconds_per_total_staked_sum_at(30), + u256 { + low: 0xC0000000000000000000000000000000, + high: 0_u128, + } + ); // 3/4 + assert_eq!(staker.get_seconds_per_total_staked_sum_at(40), + u256 { + low: 0x15555555555555555555555555555555, + high: 1_u128, + } + ); // 1 + 1/12 } } From 83eb22db58c99f4aa9449b49305d8e746a30ed96 Mon Sep 17 00:00:00 2001 From: baitcode Date: Fri, 31 Jan 2025 04:50:54 +0300 Subject: [PATCH 40/40] uncomment tests + scarb format --- src/lib.cairo | 12 +++---- src/staker.cairo | 46 ++++++++++++++---------- src/staker_log.cairo | 17 ++++----- src/staker_test.cairo | 83 +++++++++++++++++++------------------------ 4 files changed, 77 insertions(+), 81 deletions(-) diff --git a/src/lib.cairo b/src/lib.cairo index 15df3c3..7d41603 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -1,7 +1,7 @@ pub mod airdrop; mod airdrop_claim_check; -// #[cfg(test)] -// mod airdrop_test; +#[cfg(test)] +mod airdrop_test; pub mod call_trait; #[cfg(test)] @@ -12,14 +12,14 @@ pub mod execution_state; mod execution_state_test; pub mod governor; -// #[cfg(test)] -// mod governor_test; +#[cfg(test)] +mod governor_test; pub mod staker; pub mod staker_log; -// #[cfg(test)] -// pub mod staker_log_test; +#[cfg(test)] +pub mod staker_log_test; #[cfg(test)] mod staker_test; diff --git a/src/staker.cairo b/src/staker.cairo index fcc5cd3..dd8e7e4 100644 --- a/src/staker.cairo +++ b/src/staker.cairo @@ -55,10 +55,10 @@ pub trait IStaker { // Calculates snapshot for seconds_per_total_staked_sum (val) at given timestamp (ts). // If timestamp if before first record, returns 0. - // If timestamp is between records, calculates Δt = (ts - record.ts) where record is - // first record in log before timestamp, then calculates total amount using the + // If timestamp is between records, calculates Δt = (ts - record.ts) where record is + // first record in log before timestamp, then calculates total amount using the // weighted_total_staked diff diveded by time diff. - // If timestamp is after last record, calculates Δt = (ts - last_record.ts) and + // If timestamp is after last record, calculates Δt = (ts - last_record.ts) and // takes total_staked from storage and adds Δt / total_staked to accumulator. // In case total_staked is 0 this method turns is to 1 to simplify calculations // TODO: this should be a part of StakingLog @@ -71,9 +71,7 @@ pub trait IStaker { fn get_total_staked_at(self: @TContractState, timestamp: u64) -> u128; - fn get_average_total_staked_over_period( - self: @TContractState, start: u64, end: u64, - ) -> u128; + fn get_average_total_staked_over_period(self: @TContractState, start: u64, end: u64) -> u128; fn get_user_share_of_total_staked_over_period( self: @TContractState, staked: u128, start: u64, end: u64, @@ -377,11 +375,9 @@ pub mod Staker { } // Check interface for detailed description. - fn get_seconds_per_total_staked_sum_at( - self: @ContractState, timestamp: u64, - ) -> u256 { + fn get_seconds_per_total_staked_sum_at(self: @ContractState, timestamp: u64) -> u256 { let record = self.staking_log.find_record_on_or_before_timestamp(timestamp); - + if let Option::Some((record, idx)) = record { let total_staked = if (idx == self.staking_log.len() - 1) { // if last record found @@ -389,9 +385,13 @@ pub mod Staker { } else { // This helps to avoid couple of FP divisions. let next_record = self.staking_log.at(idx + 1).read(); - let time_weighted_total_staked_sum_diff = next_record.time_weighted_total_staked_sum - record.time_weighted_total_staked_sum; + let time_weighted_total_staked_sum_diff = next_record + .time_weighted_total_staked_sum + - record.time_weighted_total_staked_sum; let timestamp_diff = next_record.timestamp - record.timestamp; - (time_weighted_total_staked_sum_diff / timestamp_diff.into()).try_into().unwrap() + (time_weighted_total_staked_sum_diff / timestamp_diff.into()) + .try_into() + .unwrap() }; let seconds_diff = timestamp - record.timestamp; @@ -412,21 +412,25 @@ pub mod Staker { fn get_time_weighted_total_staked_sum_at(self: @ContractState, timestamp: u64) -> u256 { let record = self.staking_log.find_record_on_or_before_timestamp(timestamp); - + if let Option::Some((record, idx)) = record { let total_staked = if (idx == self.staking_log.len() - 1) { // if last rescord found self.total_staked.read() } else { let next_record = self.staking_log.at(idx + 1).read(); - let time_weighted_total_staked_sum_diff = next_record.time_weighted_total_staked_sum - record.time_weighted_total_staked_sum; + let time_weighted_total_staked_sum_diff = next_record + .time_weighted_total_staked_sum + - record.time_weighted_total_staked_sum; let timestamp_diff = next_record.timestamp - record.timestamp; - (time_weighted_total_staked_sum_diff / timestamp_diff.into()).try_into().unwrap() + (time_weighted_total_staked_sum_diff / timestamp_diff.into()) + .try_into() + .unwrap() }; let seconds_diff = timestamp - record.timestamp; let time_weighted_total_staked: u256 = total_staked.into() * seconds_diff.into(); - + return record.time_weighted_total_staked_sum + time_weighted_total_staked; } @@ -440,9 +444,13 @@ pub mod Staker { self.total_staked.read() } else { let next_record = self.staking_log.at(idx + 1).read(); - let time_weighted_total_staked_sum_diff = next_record.time_weighted_total_staked_sum - record.time_weighted_total_staked_sum; + let time_weighted_total_staked_sum_diff = next_record + .time_weighted_total_staked_sum + - record.time_weighted_total_staked_sum; let timestamp_diff = next_record.timestamp - record.timestamp; - (time_weighted_total_staked_sum_diff / timestamp_diff.into()).try_into().unwrap() + (time_weighted_total_staked_sum_diff / timestamp_diff.into()) + .try_into() + .unwrap() } } else { 0_u128 @@ -470,6 +478,6 @@ pub mod Staker { let end_snapshot = self.get_seconds_per_total_staked_sum_at(end); staked * ((end_snapshot - start_snapshot) * 100).high / (end - start).into() - } + } } } diff --git a/src/staker_log.cairo b/src/staker_log.cairo index 19d513f..c804f3f 100644 --- a/src/staker_log.cairo +++ b/src/staker_log.cairo @@ -24,7 +24,6 @@ pub(crate) struct StakingLogRecord { #[generate_trait] pub impl StakingLogOperations of LogOperations { - fn find_record_on_or_before_timestamp( self: @StorageBase, timestamp: u64, ) -> Option<(StakingLogRecord, u64)> { @@ -63,7 +62,9 @@ pub impl StakingLogOperations of LogOperations { return Option::None; } - fn log_change(self: StorageBase>, amount: u128, total_staked_before_change: u128) { + fn log_change( + self: StorageBase>, amount: u128, total_staked_before_change: u128, + ) { let log = self.as_path(); let block_timestamp = get_block_timestamp(); @@ -102,7 +103,8 @@ pub impl StakingLogOperations of LogOperations { let staked_seconds_per_total_staked: u256 = if total_staked_before_change == 0 { 0_u64.into() } else { - let res = u256 { low: 0, high: seconds_diff.into() } / total_staked_before_change.into(); + let res = u256 { low: 0, high: seconds_diff.into() } + / total_staked_before_change.into(); res }; @@ -130,10 +132,7 @@ pub(crate) impl StakingLogRecordStorePacking of StorePacking