Skip to content

Commit

Permalink
Feat staker v2 no fp and subpointers (#67)
Browse files Browse the repository at this point in the history
* Initial implementation of staked seconds calculation

* a bit of naming

* added upgrade method. NOTE: constructor changed. Staker now requires governor.

* fix import

* TODO: removal

* remove todo

* test

* local settings

* revert upgrade code for now

* removed upgrade due to cyclic dependency with governor

* finalise

* typo

* 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

* removed old tests

* * 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

* some comment fixes

* Extracted staker storage into separate file due to large amount of boilerplate.
Implemented custom packing and custom data SubPointers.

* rename staker_storage -> staker_log. moved all relevant logic there

* Simplified Fixed Point operations library. Reduced internal storage size.
Added convienience method for u64 / u128 case.

* simplified more. better naming

* clean up bitshifts from tests

* overflow checks

* 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.

* Debugging, cleaning up, fixing missed issues

* final fix

* final debugging

* cleanup

* cleanup

* removing UFixedPoint124x128 struct

* BugFix for a case when fetl252 is overflown

* remove fixed point lib
inline everything remove storage read optimisations

* small fix

* lost deletions

* 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

* scarb fmt

* Review fixes

* scarb format

* * remove MAX_FP related code
* find_in_change_log -> find_change_log_on_or_before_timestamp
* log_change argument rename

* 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

* uncomment tests + scarb format

* Review fix:
- reverted changes to staker
- moved changes to staker_v2
- changed delegated_cumulative_snapshot to Map<ContractAddress, Vec<DelegatedSnapshot>>

* fixup

* fixup

* remove Timestamped

* remove TODO

* * rewrite while loop as recursion

* make search private

---------

Co-authored-by: Moody Salem <moody.salem@gmail.com>
  • Loading branch information
baitcode and moodysalem authored Feb 19, 2025
1 parent 475f33f commit eac52cc
Show file tree
Hide file tree
Showing 6 changed files with 1,305 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
.env
.vscode
.starkli
.DS_Store
#######
Scarb
#######
Expand Down
8 changes: 8 additions & 0 deletions src/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ pub mod staker;
#[cfg(test)]
mod staker_test;

pub mod staker_v2;
#[cfg(test)]
mod staker_v2_test;

pub mod staker_log;
#[cfg(test)]
pub mod staker_log_test;

mod interfaces {
pub(crate) mod erc20;
}
Expand Down
178 changes: 178 additions & 0 deletions src/staker_log.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
use starknet::storage::{
Mutable, StorageAsPath, StorageBase, StoragePointerReadAccess, StoragePointerWriteAccess,
Vec, VecTrait, MutableVecTrait
};

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;

#[derive(Drop, Serde, Copy)]
pub(crate) struct StakingLogRecord {
pub(crate) timestamp: u64,
// Only 128+32=160 bits are used
pub(crate) time_weighted_total_staked_sum: u256,
pub(crate) seconds_per_total_staked_sum: u256,
}

#[generate_trait]
impl InternalStakingLogOperations of InternalLogOperations {
fn search_recursive(
self: @StorageBase<StakingLog>, timestamp: u64, left: u64, right: u64,
) -> Option<(StakingLogRecord, u64)> {
let log = self.as_path();

if left > right {
return Option::None;
}

let center = (right + left) / 2;
let record = log.at(center).read();

if record.timestamp <= timestamp {
let res = self
.search_recursive(timestamp, center + 1, right)
.unwrap_or((record, center));

Option::Some(res)
} else {
self.search_recursive(timestamp, left, center - 1)
}
}
}

#[generate_trait]
pub impl StakingLogOperations of LogOperations {

fn find_record_on_or_before_timestamp(
self: @StorageBase<StakingLog>, timestamp: u64,
) -> Option<(StakingLogRecord, u64)> {
let log = self.as_path();
if log.len() == 0 {
return Option::None;
}

if log.at(0).read().timestamp > timestamp {
return Option::None;
}

return self.search_recursive(timestamp, 0, log.len() - 1);
}

fn log_change(
self: StorageBase<Mutable<StakingLog>>, amount: u128, total_staked_before_change: u128,
) {
let log = self.as_path();

let block_timestamp = get_block_timestamp();

if log.len() == 0 {
log
.append()
.write(
StakingLogRecord {
timestamp: block_timestamp,
time_weighted_total_staked_sum: 0_u256,
seconds_per_total_staked_sum: 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 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()
} else {
let res = u256 { low: 0, high: seconds_diff.into() }
/ total_staked_before_change.into();
res
};

// Add a new record.
record
.write(
StakingLogRecord {
timestamp: block_timestamp,
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,
},
);
}
}

//
// Storage layout for StakingLogRecord
//

pub(crate) impl StakingLogRecordStorePacking of StorePacking<StakingLogRecord, (felt252, felt252)> {
fn pack(value: StakingLogRecord) -> (felt252, felt252) {
let val1: felt252 = pack_u64_u256_tuple(
value.timestamp, value.time_weighted_total_staked_sum,
);

let val2: felt252 = value.seconds_per_total_staked_sum.try_into().unwrap();

(val1, val2)
}

fn unpack(value: (felt252, felt252)) -> StakingLogRecord {
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,
time_weighted_total_staked_sum: time_weighted_total_staked_sum,
seconds_per_total_staked_sum: seconds_per_total_staked_sum.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,
);
}
33 changes: 33 additions & 0 deletions src/staker_log_test.cairo
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,
)
}
Loading

0 comments on commit eac52cc

Please sign in to comment.