-
Notifications
You must be signed in to change notification settings - Fork 32
Feat staker v2 no fp and subpointers #67
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 35 commits
cb3e4dd
01b3f73
924aef3
88f8db8
9553035
aedb403
9ee9324
c3c0244
692c36a
349919e
d6504e9
5f32326
b5ac50b
2f9516a
bb4f99a
1a2d6fc
b18d702
7fdfb19
b82953f
80cf041
f507631
164f64f
6e931c6
fafba15
b55b320
a28971c
aa07801
61f085c
f52fff1
4cc807e
7e35137
815d539
284ae39
c6102a1
e3067ab
f622f34
56ec424
974c68d
955ef24
83eb22d
f6ee4fb
0e8b8a6
1acb535
204fa73
49ee037
04a4edb
08dd762
65dda9b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,7 @@ | ||
| .env | ||
| .vscode | ||
| .starkli | ||
| .DS_Store | ||
| ####### | ||
| Scarb | ||
| ####### | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -52,22 +52,28 @@ pub trait IStaker<TContractState> { | |
| 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_cumulative_seconds_per_total_staked_at(self: @TContractState, timestamp: u64) -> u256; | ||
| } | ||
|
|
||
| #[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, MAX_FP, StakingLog}; | ||
| use governance::interfaces::erc20::{IERC20Dispatcher, IERC20DispatcherTrait}; | ||
| use starknet::storage::VecTrait; | ||
| use starknet::storage::{ | ||
| Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePathEntry, | ||
| StoragePointerReadAccess, StoragePointerWriteAccess, | ||
| }; | ||
|
|
||
| use starknet::{ | ||
| get_block_timestamp, get_caller_address, get_contract_address, | ||
| ContractAddress, get_block_timestamp, get_caller_address, get_contract_address, | ||
| storage_access::{StorePacking}, | ||
| }; | ||
| use super::{ContractAddress, IStaker}; | ||
|
|
||
| use super::{IStaker}; | ||
|
|
||
| #[derive(Copy, Drop, PartialEq, Debug)] | ||
| pub struct DelegatedSnapshot { | ||
|
|
@@ -78,6 +84,7 @@ pub mod Staker { | |
| const TWO_POW_64: u128 = 0x10000000000000000; | ||
| const TWO_POW_192: u256 = 0x1000000000000000000000000000000000000000000000000; | ||
| const TWO_POW_192_DIVISOR: NonZero<u256> = 0x1000000000000000000000000000000000000000000000000; | ||
| const TWO_POW_127: u128 = 0x80000000000000000000000000000000_u128; | ||
|
|
||
| pub(crate) impl DelegatedSnapshotStorePacking of StorePacking<DelegatedSnapshot, felt252> { | ||
| fn pack(value: DelegatedSnapshot) -> felt252 { | ||
|
|
@@ -104,6 +111,8 @@ pub mod Staker { | |
| amount_delegated: Map<ContractAddress, u128>, | ||
| delegated_cumulative_num_snapshots: Map<ContractAddress, u64>, | ||
| delegated_cumulative_snapshot: Map<ContractAddress, Map<u64, DelegatedSnapshot>>, | ||
| total_staked: u128, | ||
| staking_log: StakingLog, | ||
| } | ||
|
|
||
| #[constructor] | ||
|
|
@@ -253,6 +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.emit(Staked { from, delegate, amount }); | ||
| } | ||
|
|
||
|
|
@@ -280,6 +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(); | ||
| self.total_staked.write(total_staked - amount); | ||
| self.staking_log.log_change(amount, total_staked); | ||
|
|
||
| self.emit(Withdrawn { from, delegate, to: recipient, amount }); | ||
| } | ||
|
|
||
|
|
@@ -333,5 +353,70 @@ pub mod Staker { | |
| let now = get_block_timestamp(); | ||
| self.get_average_delegated(delegate, now - period, now) | ||
| } | ||
|
|
||
| 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) { | ||
| 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(); | ||
|
|
||
| // 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 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, | ||
| }; | ||
|
|
||
| assert(total_staked_fp.high < MAX_FP, 'FP_OVERFLOW'); | ||
|
|
||
| // round value | ||
| total_staked_fp.high + if (total_staked_fp.low >= TWO_POW_127) { | ||
| 1 | ||
| } else { | ||
| 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() | ||
| }; | ||
|
|
||
| // 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; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,178 @@ | ||
| use starknet::storage::MutableVecTrait; | ||
| use starknet::storage::{ | ||
| Mutable, StorageAsPath, StorageBase, StoragePointerReadAccess, StoragePointerWriteAccess, | ||
| }; | ||
|
|
||
| use starknet::storage::{Vec, VecTrait}; | ||
| use starknet::storage_access::{StorePacking}; | ||
| use starknet::{get_block_timestamp}; | ||
|
|
||
| pub type StakingLog = Vec<StakingLogRecord>; | ||
|
|
||
| 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 { | ||
| 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, | ||
| } | ||
|
|
||
| #[generate_trait] | ||
| pub impl StakingLogOperations of LogOperations { | ||
| fn get_total_staked(self: @StorageBase<StakingLog>, timestamp: u64) -> Option<u128> { | ||
| Option::Some(0) | ||
| } | ||
|
|
||
| fn find_in_change_log( | ||
| self: @StorageBase<StakingLog>, 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<(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; | ||
| } else { | ||
| right = center - 1; | ||
| }; | ||
| }; | ||
|
|
||
| if let Option::Some((result, idx)) = result_ptr { | ||
| return Option::Some((result, idx)); | ||
| } | ||
|
|
||
| return Option::None; | ||
| } | ||
|
|
||
| fn log_change(self: StorageBase<Mutable<StakingLog>>, 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: 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 | ||
| } else { | ||
| // create new record | ||
| log.append() | ||
| }; | ||
|
|
||
| // 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 { | ||
| 0_u64.into() | ||
| } else { | ||
| let res = u256 { low: 0, high: seconds_diff.into() } / total_staked.into(); | ||
| assert(res.high < MAX_FP, 'FP_OVERFLOW'); | ||
| res | ||
| }; | ||
|
|
||
| // 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, | ||
| }, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| // | ||
| // Storage layout for StakingLogRecord | ||
| // | ||
|
|
||
| pub(crate) impl StakingLogRecordStorePacking of StorePacking<StakingLogRecord, (felt252, felt252)> { | ||
| 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 | ||
| .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, | ||
| ); | ||
|
|
||
| StakingLogRecord { | ||
| timestamp: timestamp, | ||
| cumulative_total_staked: cumulative_total_staked, | ||
| cumulative_seconds_per_total_staked: cumulative_seconds_per_total_staked | ||
| .try_into() | ||
| .unwrap(), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| 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) 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, | ||
| }; | ||
|
|
||
| return ( | ||
| (packed_ts_total_staked_u256.high / TWO_POW_32.into()).try_into().unwrap(), | ||
| cumulative_total_staked, | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| 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; | ||
| const MASK_160_BITS: u256 = 0x10000000000000000000000000000000000000000 - 1; | ||
|
|
||
|
|
||
| fn assert_packs_and_unpacks(timestamp: u64, total_staked: u256) { | ||
| 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_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] | ||
| 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, | ||
| ) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this comment accurate? there were 2 diff quantities we wanted to be able to compute:
average total staked for a period of time
seconds passed * total_staked, a 160+ bit wide value(snapshotB-snapshotA) / periodLengthuser's share of total stake over a period
seconds passed / total staked, a 160+ bit wide valuestakedAmount * snapshotStakeEnd - snapshotStakeStartThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No it's not. It's outdated. I've updated it. This method calculates shapshots for "user's share of total stake over a period"
Fixed. That one. Will add other snapshots soon. Then add methods for 2 diff quantities.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@moodysalem
Formula for
user's shareseems incorrect:seconds. which raises suspicion.I think that correct one is: stakedAmount * (snapshotStakeEnd - snapshotStakeStart) / (snapshotTimestampEnd-snapshotTimestampStart)
Can you please confirm that this is correct?