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]