From c016a764cdc3bb46b780a1ba0d4ce198ac59c6dd Mon Sep 17 00:00:00 2001 From: Malachi Nguyen <123039533+scobi7@users.noreply.github.com> Date: Wed, 26 Jun 2024 06:20:29 -0700 Subject: [PATCH 01/34] Create streaming.cairo --- src/streaming.cairo | 178 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 src/streaming.cairo diff --git a/src/streaming.cairo b/src/streaming.cairo new file mode 100644 index 00000000..43efa47b --- /dev/null +++ b/src/streaming.cairo @@ -0,0 +1,178 @@ +use starknet::ContractAddress; + +// To add new vesting schedules, this Component should be part of a Custom proposal which calls add_linear_vesting_schedule or a vesting milestone. +// This will execute the code from this component in the context of the contract. Only vest() should be exported externally from the contract. +#[starknet::interface] +trait IStreaming{ + fn add_new_stream( + ref self: TContractState, + recipient: ContractAddress, + start_time: u64, + end_time: u64, + total_amount: u128 + ); + + fn claim_stream( + ref self: TContractState, + streamer: ContractAddress, + recipient: ContractAddress, + start_time: u64, + end_time: u64 + ); + + fn cancel_stream( + ref self: TContractState, + recipient: ContractAddress, + start_time: u64, + end_time: u64 + ); +} + +#[starknet::component] +mod streaming { + use konoha::contract::Governance; + use konoha::contract::{IGovernanceDispatcher, IGovernanceDispatcherTrait}; + use konoha::traits::{IGovernanceTokenDispatcher, IGovernanceTokenDispatcherTrait}; + use starknet::ContractAddress; + use starknet::{get_block_timestamp, get_caller_address, get_contract_address}; + + #[storage] + struct Storage{ + streams: LegacyMap::<(ContractAddress, ContractAddress, u64, u64), (u128, u128)> // (claimed_amount, total_amount) + } + + #[derive(starknet::Event, Drop, Serde)] + #[event] + enum Event{ + StreamCreated: StreamCreated, + StreamClaimed: StreamClaimed, + StreamCanceled: StreamCanceled + } + + #[derive(starknet::Event, Drop, Serde)] + struct StreamCreated{ + streamer: ContractAddress, + recipient: ContractAddress, + start_time: u64, + end_time: u64, + total_amount: u128 + } + + #[derive(starknet::Event, Drop, Serde)] + struct StreamClaimed{ + streamer: ContractAddress, + claimed_amount: u128 + } + + #[derive(starknet::Event, Drop, Serde)] + struct StreamCanceled{ + streamer: ContractAddress, + recipient: ContractAddress, + start_time: u64, + end_time: u64, + reclaimed_amount: u128 + } + //TODO: + #[embeddable_as(StreamingImpl)] + impl Streaming< + TContractState, +HasComponent + > of super::IStreaming> { + fn add_new_stream( + ref self: ComponentState, + recipient: ContractAddress, + start_time: u64, + end_time: u64, + total_amount: u128 + ){ + assert(get_caller_address()== get_contract_address(), 'not self-call'); + assert(start_time < end_time, 'starts first'); + let key = (get_caller_address(), recipient, end_time, start_time); + self.streams.write(key, (0, total_amount)); + + self.emit(StreamCreated{ + streamer: get_caller_address(), + recipient: recipient, + start_time: start_time, + end_time: end_time, + total_amount: total_amount + }) + } + + fn claim_stream( + ref self: ComponentState, + streamer: ContractAddress, + recipient: ContractAddress, + start_time: u64, + end_time: u64 + ){ + let current_time = get_block_timestamp(); + + let key: (ContractAddress, ContractAddress, u64, u64) = ( + streamer, + recipient, + start_time, + end_time, + ); + + let (claimed_amount, total_amount): (u128, u128) = self.streams.read(key); + + assert(current_time > start_time, 'stream has not started'); + + let elapsed_time = if current_time > end_time { + end_time - start_time + } else{ + current_time - start_time + }; + + let stream_duration = (end_time - start_time); + + let claimable_amount = (total_amount * elapsed_time.into()) / stream_duration.into(); + let amount_to_claim = claimable_amount - claimed_amount; + assert(amount_to_claim > 0, 'nothing to claim'); + self.streams.write(key, (claimable_amount, total_amount));//claimable shld be (claimable_amount + amount_to_claim) + + let self_dsp = IGovernanceDispatcher { contract_address: get_contract_address() }; + IGovernanceTokenDispatcher { contract_address: self_dsp.get_governance_token_address() } + .mint(recipient, amount_to_claim.into()); + + self.emit(StreamClaimed{ + streamer: streamer, + claimed_amount: amount_to_claim, + }) + } + + fn cancel_stream( + ref self: ComponentState, + recipient: ContractAddress, + start_time: u64, + end_time: u64 + ){ + let key: (ContractAddress, ContractAddress, u64, u64) = ( + get_caller_address(), + recipient, + start_time, + end_time, + ); + + // Read from the streams LegacyMap + let (claimed_amount, total_amount): (u128, u128) = self.streams.read(key); + + let unclaimed_amount = total_amount - claimed_amount; + //cancel stream + + self.streams.write(key, (0,0)); + + let self_dsp = IGovernanceDispatcher { contract_address: get_contract_address() }; + IGovernanceTokenDispatcher { contract_address: self_dsp.get_governance_token_address() } + .mint(get_caller_address(), unclaimed_amount.into()); + + self.emit(StreamCanceled{ + streamer: get_caller_address(), + recipient: recipient, + start_time: start_time, + end_time: end_time, + reclaimed_amount: unclaimed_amount, + }) + } + } +} From e160e76c7397a0cf7fdb0ee6f1ddc182c13ba7d6 Mon Sep 17 00:00:00 2001 From: Malachi Nguyen <123039533+scobi7@users.noreply.github.com> Date: Wed, 26 Jun 2024 06:21:09 -0700 Subject: [PATCH 02/34] Push test_streaming.cairo --- tests/test_streaming.cairo | 61 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 tests/test_streaming.cairo diff --git a/tests/test_streaming.cairo b/tests/test_streaming.cairo new file mode 100644 index 00000000..fc88a074 --- /dev/null +++ b/tests/test_streaming.cairo @@ -0,0 +1,61 @@ +use array::ArrayTrait; +use core::option::OptionTrait; +use core::result::ResultTrait; +use core::traits::TryInto; +use debug::PrintTrait; + +use konoha::vesting::{IVestingDispatcher, IVestingDispatcherTrait, IVesting}; +use konoha::streaming::{IStreamingDispatcher, IStreamingDispatcherTrait, IStreaming}; + +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +use snforge_std::{ + BlockId, declare, ContractClassTrait, ContractClass, start_prank, start_warp, CheatTarget +}; + +use starknet::ContractAddress; + +// returns gov addr, token addr +fn test_setup() -> (ContractAddress, ContractAddress) { + let new_gov_contract: ContractClass = declare("Governance") + .expect('unable to declare Governance'); + let new_token_contract: ContractClass = declare("FloatingToken").expect('unable to declare FloatingToken'); + let new_gov_addr: ContractAddress = + 0x001405ab78ab6ec90fba09e6116f373cda53b0ba557789a4578d8c1ec374ba0f + .try_into() + .unwrap(); + let mut token_constructor = ArrayTrait::new(); + token_constructor.append(new_gov_addr.into()); // Owner + let (token_address, _) = new_token_contract + .deploy(@token_constructor) + .expect('unable to deploy token'); + let mut gov_constructor: Array = ArrayTrait::new(); + gov_constructor.append(token_address.into()); + let (gov_address, _) = new_gov_contract + .deploy_at(@gov_constructor, new_gov_addr) + .expect('unable to deploy gov'); + + (gov_address, token_address) +} + +#[test] +fn test_add_new_stream() { + let (gov_address, _) = test_setup(); + + let streaming = IStreamingDispatcher { contract_address: gov_address }; + + start_warp(CheatTarget::All, 1); + + let recipient: ContractAddress = 0x2.try_into().unwrap(); + let start_time: u64 = 100; + let end_time: u64 = 200; + let total_amount: u128 = 100000; + + streaming.add_new_stream(recipient, start_time, end_time, total_amount); +} + + + + + + + From d71c24382f9c681783f4029f22be8260392d12c4 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 26 Jun 2024 15:08:32 +0000 Subject: [PATCH 03/34] used setup.cairo, still param issues --- tests/test_streaming.cairo | 55 ++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/tests/test_streaming.cairo b/tests/test_streaming.cairo index fc88a074..78c172c7 100644 --- a/tests/test_streaming.cairo +++ b/tests/test_streaming.cairo @@ -13,49 +13,46 @@ use snforge_std::{ }; use starknet::ContractAddress; +use super::setup::deploy_governance; +use super::setup::deploy_and_distribute_gov_tokens; + +const GOV_TOKEN_INITIAL_SUPPLY: u256 = 1000000000000000000; +const first_address: felt252 = 0x1; +const second_address: felt252 = 0x2; +const admin_addr: felt252 = 0x3; +const governance_address: felt252 = 0x99999; // returns gov addr, token addr + fn test_setup() -> (ContractAddress, ContractAddress) { - let new_gov_contract: ContractClass = declare("Governance") - .expect('unable to declare Governance'); - let new_token_contract: ContractClass = declare("FloatingToken").expect('unable to declare FloatingToken'); - let new_gov_addr: ContractAddress = - 0x001405ab78ab6ec90fba09e6116f373cda53b0ba557789a4578d8c1ec374ba0f - .try_into() - .unwrap(); - let mut token_constructor = ArrayTrait::new(); - token_constructor.append(new_gov_addr.into()); // Owner - let (token_address, _) = new_token_contract - .deploy(@token_constructor) - .expect('unable to deploy token'); - let mut gov_constructor: Array = ArrayTrait::new(); - gov_constructor.append(token_address.into()); - let (gov_address, _) = new_gov_contract - .deploy_at(@gov_constructor, new_gov_addr) - .expect('unable to deploy gov'); + // Deploy governance contract + let gov_dispatcher = deploy_governance(governance_address.try_into().unwrap()); + let gov_address = gov_dispatcher.contract_address; + + // Deploy and distribute governance tokens + let token_dispatcher = deploy_and_distribute_gov_tokens(admin_addr.try_into().unwrap()); + let token_address = token_dispatcher.contract_address; (gov_address, token_address) } #[test] fn test_add_new_stream() { + // Setup phase: Deploy governance and distribute tokens let (gov_address, _) = test_setup(); - let streaming = IStreamingDispatcher { contract_address: gov_address }; + // Interaction phase: Use the deployed contracts + let streaming_dispatcher = IStreamingDispatcher { contract_address: gov_address }; - start_warp(CheatTarget::All, 1); + // Simulate a contract function call with correct parameters + let recipient: ContractAddress = 0x2.try_into().unwrap(); // Example recipient address + let start_time: u64 = 100; // Example start time + let end_time: u64 = 200; // Example end time + let total_amount: u128 = 100000; // Example total amount - let recipient: ContractAddress = 0x2.try_into().unwrap(); - let start_time: u64 = 100; - let end_time: u64 = 200; - let total_amount: u128 = 100000; - - streaming.add_new_stream(recipient, start_time, end_time, total_amount); + // Call the add_new_stream function of the streaming contract + streaming_dispatcher.add_new_stream(recipient, start_time, end_time, total_amount); } - - - - From 3556f91cc8f1a4ef3eee62857f2bc96f9071f253 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 27 Jun 2024 17:36:57 +0000 Subject: [PATCH 04/34] 0 progress... --- tests/test_streaming.cairo | 81 +++++++++++++++++++++++--------------- 1 file changed, 49 insertions(+), 32 deletions(-) diff --git a/tests/test_streaming.cairo b/tests/test_streaming.cairo index 78c172c7..e4c30b3d 100644 --- a/tests/test_streaming.cairo +++ b/tests/test_streaming.cairo @@ -4,55 +4,72 @@ use core::result::ResultTrait; use core::traits::TryInto; use debug::PrintTrait; +use konoha::contract::Governance; +use konoha::contract::{IGovernanceDispatcher, IGovernanceDispatcherTrait}; +use konoha::traits::{IGovernanceTokenDispatcher, IGovernanceTokenDispatcherTrait}; +use starknet::{get_block_timestamp, get_caller_address, get_contract_address, ContractAddress}; use konoha::vesting::{IVestingDispatcher, IVestingDispatcherTrait, IVesting}; use konoha::streaming::{IStreamingDispatcher, IStreamingDispatcherTrait, IStreaming}; use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; use snforge_std::{ - BlockId, declare, ContractClassTrait, ContractClass, start_prank, start_warp, CheatTarget + BlockId, declare, ContractClassTrait, ContractClass, start_prank, start_warp, CheatTarget, + prank, CheatSpan }; -use starknet::ContractAddress; -use super::setup::deploy_governance; -use super::setup::deploy_and_distribute_gov_tokens; +use super::setup::{admin_addr, governance_address, deploy_governance, deploy_governance_and_both_tokens, deploy_and_distribute_gov_tokens}; -const GOV_TOKEN_INITIAL_SUPPLY: u256 = 1000000000000000000; -const first_address: felt252 = 0x1; -const second_address: felt252 = 0x2; -const admin_addr: felt252 = 0x3; -const governance_address: felt252 = 0x99999; +fn start_stream(gov: ContractAddress){ + prank(CheatTarget::One(gov), gov, CheatSpan::TargetCalls(4)); + let streaming = IStreamingDispatcher { contract_address: gov }; + streaming.add_new_stream(0x2.try_into().unwrap(), 100, 200, 100000); +} -// returns gov addr, token addr -fn test_setup() -> (ContractAddress, ContractAddress) { - // Deploy governance contract - let gov_dispatcher = deploy_governance(governance_address.try_into().unwrap()); - let gov_address = gov_dispatcher.contract_address; +fn test_add_new_stream() { + let (gov, _, _) = deploy_governance_and_both_tokens(); - // Deploy and distribute governance tokens - let token_dispatcher = deploy_and_distribute_gov_tokens(admin_addr.try_into().unwrap()); - let token_address = token_dispatcher.contract_address; + let streaming = IStreamingDispatcher { contract_address: gov.contract_address }; - (gov_address, token_address) -} + let recipient = 0x2.try_into().unwrap(); + let start_time: u64 = 100; + let end_time: u64 = 200; + let total_amount: u128 = 100000; + + streaming.add_new_stream(recipient, start_time, end_time, total_amount); + + let key = (get_caller_address(), recipient, end_time, start_time); -#[test] -fn test_add_new_stream() { - // Setup phase: Deploy governance and distribute tokens - let (gov_address, _) = test_setup(); + let (claimed_amount, stored_total_amount) = streaming.get_stream_info( + get_caller_address(), // streamer + recipient, + start_time, + end_time, + ); - // Interaction phase: Use the deployed contracts - let streaming_dispatcher = IStreamingDispatcher { contract_address: gov_address }; + // Assert the values to validate correctness + //assert_eq!(claimed_amount, 0, "Incorrect claimed amount after stream creation"); + //assert_eq!(stored_total_amount, total_amount, "Incorrect total amount stored"); + println!("Stream Info for key {:?}: claimed_amount={}, stored_total_amount={}", key, claimed_amount, stored_total_amount); - // Simulate a contract function call with correct parameters - let recipient: ContractAddress = 0x2.try_into().unwrap(); // Example recipient address - let start_time: u64 = 100; // Example start time - let end_time: u64 = 200; // Example end time - let total_amount: u128 = 100000; // Example total amount +} - // Call the add_new_stream function of the streaming contract - streaming_dispatcher.add_new_stream(recipient, start_time, end_time, total_amount); +fn main(){ + test_add_new_stream(); } +//fn test_deploy() -> ContractAddress { +// let (gov_dispatcher, _, _) = deploy_governance_and_both_tokens(); +// gov_dispatcher.contract_address +//} +// +//#[test] +//fn test_add_new_stream() { +// let gov_address = test_deploy(); +// let streaming_dispatcher = IStreamingDispatcher { contract_address: gov_address }; +// +// start_warp(CheatTarget::All, 1); +// streaming_dispatcher.add_new_stream(0x2.try_into().unwrap(), 100, 200, 100000); +//} From 84630230e26c5b7d68de14539b9ef28b6fd12e99 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 28 Jun 2024 09:54:36 +0000 Subject: [PATCH 05/34] added a streamer ID --- src/streaming.cairo | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/streaming.cairo b/src/streaming.cairo index 43efa47b..de5edbde 100644 --- a/src/streaming.cairo +++ b/src/streaming.cairo @@ -6,6 +6,7 @@ use starknet::ContractAddress; trait IStreaming{ fn add_new_stream( ref self: TContractState, + streamer: ContractAddress, recipient: ContractAddress, start_time: u64, end_time: u64, @@ -26,6 +27,14 @@ trait IStreaming{ start_time: u64, end_time: u64 ); + + fn get_stream_info( + ref self: TContractState, //TContractState? + streamer: ContractAddress, + recipient: ContractAddress, + start_time: u64, + end_time: u64, + ) -> (u128, u128); } #[starknet::component] @@ -79,6 +88,7 @@ mod streaming { > of super::IStreaming> { fn add_new_stream( ref self: ComponentState, + streamer: ContractAddress, recipient: ContractAddress, start_time: u64, end_time: u64, @@ -86,7 +96,8 @@ mod streaming { ){ assert(get_caller_address()== get_contract_address(), 'not self-call'); assert(start_time < end_time, 'starts first'); - let key = (get_caller_address(), recipient, end_time, start_time); + + let key = (get_caller_address(), recipient, start_time, end_time); self.streams.write(key, (0, total_amount)); self.emit(StreamCreated{ @@ -129,7 +140,9 @@ mod streaming { let claimable_amount = (total_amount * elapsed_time.into()) / stream_duration.into(); let amount_to_claim = claimable_amount - claimed_amount; assert(amount_to_claim > 0, 'nothing to claim'); - self.streams.write(key, (claimable_amount, total_amount));//claimable shld be (claimable_amount + amount_to_claim) + + self.streams.write(key, (claimable_amount + amount_to_claim, total_amount));//claimable shld be (claimable_amount + amount_to_claim) + self.streams.read(key); let self_dsp = IGovernanceDispatcher { contract_address: get_contract_address() }; IGovernanceTokenDispatcher { contract_address: self_dsp.get_governance_token_address() } @@ -174,5 +187,21 @@ mod streaming { reclaimed_amount: unclaimed_amount, }) } + fn get_stream_info( + ref self: ComponentState, + streamer: ContractAddress, + recipient: ContractAddress, + start_time: u64, + end_time: u64, + ) -> (u128, u128) { + let key: (ContractAddress, ContractAddress, u64, u64) = ( + streamer, + recipient, + start_time, + end_time, + ); + self.streams.read(key) + } + } } From bfccc8478b481aee0f6f6819039e449188ccf13e Mon Sep 17 00:00:00 2001 From: root Date: Fri, 28 Jun 2024 09:55:23 +0000 Subject: [PATCH 06/34] trying to fix address mismatch --- tests/test_streaming.cairo | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/tests/test_streaming.cairo b/tests/test_streaming.cairo index e4c30b3d..71bd3bc1 100644 --- a/tests/test_streaming.cairo +++ b/tests/test_streaming.cairo @@ -22,36 +22,35 @@ use super::setup::{admin_addr, governance_address, deploy_governance, deploy_gov fn start_stream(gov: ContractAddress){ prank(CheatTarget::One(gov), gov, CheatSpan::TargetCalls(4)); let streaming = IStreamingDispatcher { contract_address: gov }; - streaming.add_new_stream(0x2.try_into().unwrap(), 100, 200, 100000); + streaming.add_new_stream(get_caller_address(), 0x2.try_into().unwrap(), 100, 200, 100000); } - +#[test] fn test_add_new_stream() { let (gov, _, _) = deploy_governance_and_both_tokens(); - + start_stream(gov.contract_address); let streaming = IStreamingDispatcher { contract_address: gov.contract_address }; + let streamer = get_caller_address(); let recipient = 0x2.try_into().unwrap(); let start_time: u64 = 100; let end_time: u64 = 200; let total_amount: u128 = 100000; - streaming.add_new_stream(recipient, start_time, end_time, total_amount); + streaming.add_new_stream(streamer, recipient, start_time, end_time, total_amount); let key = (get_caller_address(), recipient, end_time, start_time); let (claimed_amount, stored_total_amount) = streaming.get_stream_info( - get_caller_address(), // streamer + get_contract_address(), // streamer recipient, start_time, end_time, ); - // Assert the values to validate correctness - //assert_eq!(claimed_amount, 0, "Incorrect claimed amount after stream creation"); - //assert_eq!(stored_total_amount, total_amount, "Incorrect total amount stored"); + assert_eq!(claimed_amount, 0, "Incorrect claimed amount after stream creation"); + assert_eq!(stored_total_amount, total_amount, "Incorrect total amount stored"); println!("Stream Info for key {:?}: claimed_amount={}, stored_total_amount={}", key, claimed_amount, stored_total_amount); - } fn main(){ From d7a192a64ef670b92747fa98ae6de5cb76a69cb8 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 28 Jun 2024 10:06:38 +0000 Subject: [PATCH 07/34] formatting --- tests/test_streaming.cairo | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/tests/test_streaming.cairo b/tests/test_streaming.cairo index 71bd3bc1..54dcf1c6 100644 --- a/tests/test_streaming.cairo +++ b/tests/test_streaming.cairo @@ -39,10 +39,10 @@ fn test_add_new_stream() { streaming.add_new_stream(streamer, recipient, start_time, end_time, total_amount); - let key = (get_caller_address(), recipient, end_time, start_time); + //let key = (get_caller_address(), recipient, end_time, start_time); let (claimed_amount, stored_total_amount) = streaming.get_stream_info( - get_contract_address(), // streamer + streamer, recipient, start_time, end_time, @@ -50,25 +50,5 @@ fn test_add_new_stream() { assert_eq!(claimed_amount, 0, "Incorrect claimed amount after stream creation"); assert_eq!(stored_total_amount, total_amount, "Incorrect total amount stored"); - println!("Stream Info for key {:?}: claimed_amount={}, stored_total_amount={}", key, claimed_amount, stored_total_amount); } -fn main(){ - test_add_new_stream(); -} - - -//fn test_deploy() -> ContractAddress { -// let (gov_dispatcher, _, _) = deploy_governance_and_both_tokens(); -// gov_dispatcher.contract_address -//} -// -//#[test] -//fn test_add_new_stream() { -// let gov_address = test_deploy(); -// let streaming_dispatcher = IStreamingDispatcher { contract_address: gov_address }; -// -// start_warp(CheatTarget::All, 1); -// streaming_dispatcher.add_new_stream(0x2.try_into().unwrap(), 100, 200, 100000); -//} - From 60ebf09a4d7cc74e8876ac2a4872ebb7f36235d5 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 29 Jun 2024 14:30:19 +0000 Subject: [PATCH 08/34] added all tests- need to fix unidentifiable address issue --- tests/test_streaming.cairo | 72 +++++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/tests/test_streaming.cairo b/tests/test_streaming.cairo index 54dcf1c6..adfea444 100644 --- a/tests/test_streaming.cairo +++ b/tests/test_streaming.cairo @@ -38,7 +38,6 @@ fn test_add_new_stream() { let total_amount: u128 = 100000; streaming.add_new_stream(streamer, recipient, start_time, end_time, total_amount); - //let key = (get_caller_address(), recipient, end_time, start_time); let (claimed_amount, stored_total_amount) = streaming.get_stream_info( @@ -52,3 +51,74 @@ fn test_add_new_stream() { assert_eq!(stored_total_amount, total_amount, "Incorrect total amount stored"); } +#[test] +fn test_claim_stream() { + let (gov, _, _) = deploy_governance_and_both_tokens(); + start_stream(gov.contract_address); + let streaming = IStreamingDispatcher { contract_address: gov.contract_address }; + + let streamer = get_caller_address(); + let recipient = 0x2.try_into().unwrap(); + let start_time: u64 = 100; + let end_time: u64 = 200; + let total_amount: u128 = 100000; + + streaming.add_new_stream(streamer, recipient, start_time, end_time, total_amount); + + start_warp(CheatTarget::One(gov.contract_address), 150);// middle of stream + + // Claim the stream + streaming.claim_stream(streamer, recipient, start_time, end_time); + + let (claimed_amount, stored_total_amount) = streaming.get_stream_info(streamer, recipient, start_time, end_time); + + let expected_claimed_amount = (total_amount * 50) / 100; //should be 50% since middle of stream + + assert_eq!(claimed_amount, expected_claimed_amount, "Incorrect claimed amount after claiming the stream"); + assert_eq!(stored_total_amount, total_amount, "Incorrect total amount stored after claiming the stream"); + + streaming.claim_stream(streamer, recipient, start_time, end_time); + let (claimed_amount, stored_total_amount) = streaming.get_stream_info(streamer, recipient, start_time, end_time); + + assert_eq!(claimed_amount, expected_claimed_amount, "Claimed amount should remain the same after second claim attempt"); + assert_eq!(stored_total_amount, total_amount, "Total amount stored should remain the same after second claim attempt"); +} + +#[test] +fn test_cancel_stream() { + let (gov, _, _) = deploy_governance_and_both_tokens(); + start_stream(gov.contract_address); + let streaming = IStreamingDispatcher { contract_address: gov.contract_address }; + + let streamer = get_caller_address(); + let recipient = 0x2.try_into().unwrap(); + let start_time: u64 = 100; + let end_time: u64 = 200; + let total_amount: u128 = 100000; + + streaming.add_new_stream(streamer, recipient, start_time, end_time, total_amount); + + start_warp(CheatTarget::One(gov.contract_address), 150); + + streaming.cancel_stream(recipient, start_time, end_time); + + let (claimed_amount, stored_total_amount) = streaming.get_stream_info( + streamer, + recipient, + start_time, + end_time); + + assert_eq!(claimed_amount, 0, "Claimed amount should be 0 after canceling the stream"); + assert_eq!(stored_total_amount, 0, "Total amount should be 0 after canceling the stream"); + + let unclaimed_amount = claimed_amount - total_amount; + let self_dsp = IGovernanceDispatcher { contract_address: gov.contract_address }; + let token_address = self_dsp.get_governance_token_address(); + let erc20 = IERC20Dispatcher { contract_address: token_address }; + + // Check the balance of the streamer (caller address) with ERC_20, I couldnt use balance_of + let balance = erc20.balance_of(get_caller_address()); + + assert_eq!(balance, unclaimed_amount.into(), "Unclaimed amount should be reclaimed correctly"); +} + From e7a34fad47872633c2693083b89adec0d9ce328c Mon Sep 17 00:00:00 2001 From: Malachi Nguyen <123039533+scobi7@users.noreply.github.com> Date: Sat, 29 Jun 2024 07:42:43 -0700 Subject: [PATCH 09/34] Update streaming.cairo --- src/streaming.cairo | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/streaming.cairo b/src/streaming.cairo index de5edbde..12a03402 100644 --- a/src/streaming.cairo +++ b/src/streaming.cairo @@ -1,7 +1,5 @@ use starknet::ContractAddress; -// To add new vesting schedules, this Component should be part of a Custom proposal which calls add_linear_vesting_schedule or a vesting milestone. -// This will execute the code from this component in the context of the contract. Only vest() should be exported externally from the contract. #[starknet::interface] trait IStreaming{ fn add_new_stream( From de6c07e57999421bcb33a6b7e6c32760f5e55b5b Mon Sep 17 00:00:00 2001 From: Malachi Nguyen <123039533+scobi7@users.noreply.github.com> Date: Mon, 1 Jul 2024 03:36:40 -0700 Subject: [PATCH 10/34] fixed vesting tests, used setup.cairo and fixed test case syntax --- tests/vesting.cairo | 74 ++++++++++++++++++++------------------------- 1 file changed, 33 insertions(+), 41 deletions(-) diff --git a/tests/vesting.cairo b/tests/vesting.cairo index 8c8f19b5..a7733cc8 100644 --- a/tests/vesting.cairo +++ b/tests/vesting.cairo @@ -7,54 +7,47 @@ use debug::PrintTrait; use konoha::vesting::{IVestingDispatcher, IVestingDispatcherTrait, IVesting}; use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; use snforge_std::{ - BlockId, declare, ContractClassTrait, ContractClass, start_prank, start_warp, CheatTarget + BlockId, declare, ContractClassTrait, ContractClass, start_prank, start_warp, CheatTarget, + prank, CheatSpan }; -use starknet::ContractAddress; - -// returns gov addr, token addr -fn test_setup() -> (ContractAddress, ContractAddress) { - let new_gov_contract: ContractClass = declare("Governance") - .expect('unable to declare Governance'); - let new_token_contract: ContractClass = declare("MyToken").expect('unable to declare MyToken'); - let new_gov_addr: ContractAddress = - 0x001405ab78ab6ec90fba09e6116f373cda53b0ba557789a4578d8c1ec374ba0f - .try_into() - .unwrap(); - let mut token_constructor = ArrayTrait::new(); - token_constructor.append(new_gov_addr.into()); // Owner - let (token_address, _) = new_token_contract - .deploy(@token_constructor) - .expect('unable to deploy token'); - let mut gov_constructor: Array = ArrayTrait::new(); - gov_constructor.append(token_address.into()); - let (gov_address, _) = new_gov_contract - .deploy_at(@gov_constructor, new_gov_addr) - .expect('unable to deploy gov'); - - (gov_address, token_address) +use starknet::{ContractAddress, get_caller_address}; +use super::setup::{deploy_governance_and_both_tokens}; + +fn test_setup(gov: ContractAddress) { + let grantee: ContractAddress = 0x1.try_into().unwrap(); + prank(CheatTarget::One(gov), gov, CheatSpan::TargetCalls(4)); + let gov_vesting = IVestingDispatcher { contract_address: gov }; + gov_vesting.add_linear_vesting_schedule(10, 10, 10, 1000000, grantee); } #[test] -#[should_panic(expected: ('not self-call',))] +#[should_panic(expected : ('not self-call',))] fn test_unauthorized_add_vesting_schedule() { - let (gov_address, _) = test_setup(); + let (gov, _, _) = deploy_governance_and_both_tokens(); + test_setup(gov.contract_address); - let gov_vesting = IVestingDispatcher { contract_address: gov_address }; + let gov_vesting = IVestingDispatcher { contract_address: gov.contract_address }; + + let caller = get_caller_address(); start_warp(CheatTarget::All, 1); + start_prank(CheatTarget::One(gov.contract_address), caller); - gov_vesting.add_linear_vesting_schedule(10, 10, 10, 1000000, 0x1.try_into().unwrap()); + let grantee: ContractAddress = 0x1.try_into().unwrap(); + + gov_vesting.add_linear_vesting_schedule(10, 10, 10, 1000000, grantee); } #[test] #[should_panic(expected: ('not yet eligible',))] fn test_unauthorized_vest_early() { - let (gov_address, _) = test_setup(); + let (gov, _, _) = deploy_governance_and_both_tokens(); + test_setup(gov.contract_address); - let gov_vesting = IVestingDispatcher { contract_address: gov_address }; + let gov_vesting = IVestingDispatcher { contract_address: gov.contract_address }; start_warp(CheatTarget::All, 1); - start_prank(CheatTarget::One(gov_address), gov_address); + start_prank(CheatTarget::One(gov.contract_address), gov.contract_address); let grantee: ContractAddress = 0x1.try_into().unwrap(); @@ -66,12 +59,13 @@ fn test_unauthorized_vest_early() { #[test] #[should_panic(expected: ('nothing to vest',))] fn test_vest_twice() { - let (gov_address, _) = test_setup(); + let (gov, _, _) = deploy_governance_and_both_tokens(); + test_setup(gov.contract_address); - let gov_vesting = IVestingDispatcher { contract_address: gov_address }; + let gov_vesting = IVestingDispatcher { contract_address: gov.contract_address }; start_warp(CheatTarget::All, 1); - start_prank(CheatTarget::One(gov_address), gov_address); + start_prank(CheatTarget::One(gov.contract_address), gov.contract_address); let grantee: ContractAddress = 0x1.try_into().unwrap(); @@ -85,13 +79,11 @@ fn test_vest_twice() { #[test] fn test_add_simple_vesting_schedule() { - let (gov_address, token_address) = test_setup(); + let (gov, token_address, _) = deploy_governance_and_both_tokens(); + test_setup(gov.contract_address); - let gov_vesting = IVestingDispatcher { contract_address: gov_address }; - let tok = IERC20Dispatcher { contract_address: token_address }; - - start_warp(CheatTarget::All, 1); - start_prank(CheatTarget::One(gov_address), gov_address); + let gov_vesting = IVestingDispatcher { contract_address: gov.contract_address }; + let tok = IERC20Dispatcher { contract_address: token_address.contract_address }; let grantee: ContractAddress = 0x1.try_into().unwrap(); gov_vesting.add_linear_vesting_schedule(10, 10, 10, 1000001, grantee); @@ -102,7 +94,7 @@ fn test_add_simple_vesting_schedule() { assert(tok.balance_of(grantee) == 100000, 'vesting unsuccessful'); // grantee themselves can claim too - start_prank(CheatTarget::One(gov_address), grantee); + start_prank(CheatTarget::One(gov.contract_address), grantee); start_warp(CheatTarget::All, 21); // past second vest gov_vesting.vest(grantee, 20); assert(tok.balance_of(grantee) == 200000, 'vesting unsuccessful'); From 75f0138a790abe5ef6b0dd93e11eca27fd501752 Mon Sep 17 00:00:00 2001 From: Malachi Nguyen <123039533+scobi7@users.noreply.github.com> Date: Mon, 1 Jul 2024 03:38:29 -0700 Subject: [PATCH 11/34] added streaming to the contract --- src/contract.cairo | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/contract.cairo b/src/contract.cairo index 4f203d6d..3b88aaf9 100644 --- a/src/contract.cairo +++ b/src/contract.cairo @@ -40,6 +40,7 @@ mod Governance { use konoha::types::VoteStatus; use konoha::upgrades::upgrades as upgrades_component; use konoha::vesting::vesting as vesting_component; + use konoha::streaming::streaming as streaming_component; use starknet::get_contract_address; use starknet::syscalls::deploy_syscall; @@ -53,6 +54,8 @@ mod Governance { component!(path: upgrades_component, storage: upgrades, event: UpgradesEvent); component!(path: discussion_component, storage: discussions, event: DiscussionEvent); component!(path: staking_component, storage: staking, event: StakingEvent); + component!(path: streaming_component, storage: streaming, event: StreamingEvent); + #[abi(embed_v0)] impl Airdrop = airdrop_component::AirdropImpl; @@ -71,6 +74,9 @@ mod Governance { #[abi(embed_v0)] impl Staking = staking_component::StakingImpl; + #[abi(embed_v0)] + impl Streaming = streaming_component::StreamingImpl; + #[storage] struct Storage { proposal_initializer_run: LegacyMap::, @@ -87,6 +93,9 @@ mod Governance { discussions: discussion_component::Storage, #[substorage(v0)] staking: staking_component::Storage, + #[substorage(v0)] + streaming: streaming_component::Storage, + } // PROPOSALS @@ -116,6 +125,7 @@ mod Governance { UpgradesEvent: upgrades_component::Event, DiscussionEvent: discussion_component::Event, StakingEvent: staking_component::Event, + StreamingEvent: streaming_component::Event, } #[constructor] From 78d41e17e163bd903b6f96ebad32a30d9fdc3272 Mon Sep 17 00:00:00 2001 From: Malachi Nguyen <123039533+scobi7@users.noreply.github.com> Date: Tue, 2 Jul 2024 01:00:11 -0700 Subject: [PATCH 12/34] fixed vesting tests #112 From 09f1790d0915419f986ba7fd8335a67cd80b8c1a Mon Sep 17 00:00:00 2001 From: Malachi Nguyen <123039533+scobi7@users.noreply.github.com> Date: Tue, 2 Jul 2024 01:01:11 -0700 Subject: [PATCH 13/34] fixed vesting tests (CarmineOptions #112) --- tests/vesting.cairo | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/vesting.cairo b/tests/vesting.cairo index a7733cc8..85519f1c 100644 --- a/tests/vesting.cairo +++ b/tests/vesting.cairo @@ -20,6 +20,7 @@ fn test_setup(gov: ContractAddress) { gov_vesting.add_linear_vesting_schedule(10, 10, 10, 1000000, grantee); } +//passing! #[test] #[should_panic(expected : ('not self-call',))] fn test_unauthorized_add_vesting_schedule() { From cca4b0f00ae3c9a25b938a6bffafe2482fb5dc25 Mon Sep 17 00:00:00 2001 From: Malachi Nguyen <123039533+scobi7@users.noreply.github.com> Date: Tue, 2 Jul 2024 01:01:47 -0700 Subject: [PATCH 14/34] fixed vesting tests (#112) --- tests/vesting.cairo | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/vesting.cairo b/tests/vesting.cairo index 85519f1c..a7733cc8 100644 --- a/tests/vesting.cairo +++ b/tests/vesting.cairo @@ -20,7 +20,6 @@ fn test_setup(gov: ContractAddress) { gov_vesting.add_linear_vesting_schedule(10, 10, 10, 1000000, grantee); } -//passing! #[test] #[should_panic(expected : ('not self-call',))] fn test_unauthorized_add_vesting_schedule() { From 499329296503b3ab51af2281d8d95e95b891b55e Mon Sep 17 00:00:00 2001 From: Malachi Nguyen <123039533+scobi7@users.noreply.github.com> Date: Tue, 2 Jul 2024 01:02:41 -0700 Subject: [PATCH 15/34] added streaming component (#112) --- src/contract.cairo | 1 - 1 file changed, 1 deletion(-) diff --git a/src/contract.cairo b/src/contract.cairo index 3b88aaf9..d2d99c60 100644 --- a/src/contract.cairo +++ b/src/contract.cairo @@ -44,7 +44,6 @@ mod Governance { use starknet::get_contract_address; use starknet::syscalls::deploy_syscall; - use starknet::{ContractAddress, ClassHash}; From 924145dc4987f2f6b343412817ca457c52748921 Mon Sep 17 00:00:00 2001 From: Malachi Nguyen <123039533+scobi7@users.noreply.github.com> Date: Tue, 2 Jul 2024 01:05:33 -0700 Subject: [PATCH 16/34] Add Streaming Component (#112) --- src/contract.cairo | 1 + 1 file changed, 1 insertion(+) diff --git a/src/contract.cairo b/src/contract.cairo index d2d99c60..3b88aaf9 100644 --- a/src/contract.cairo +++ b/src/contract.cairo @@ -44,6 +44,7 @@ mod Governance { use starknet::get_contract_address; use starknet::syscalls::deploy_syscall; + use starknet::{ContractAddress, ClassHash}; From a2da88d0047165ada4f9615cff4bc0229634384f Mon Sep 17 00:00:00 2001 From: Malachi Nguyen <123039533+scobi7@users.noreply.github.com> Date: Tue, 2 Jul 2024 01:06:59 -0700 Subject: [PATCH 17/34] Fix Vesting Tests (#112) --- tests/test_streaming.cairo | 150 ++++++++++++++++--------------------- 1 file changed, 66 insertions(+), 84 deletions(-) diff --git a/tests/test_streaming.cairo b/tests/test_streaming.cairo index adfea444..a7733cc8 100644 --- a/tests/test_streaming.cairo +++ b/tests/test_streaming.cairo @@ -4,121 +4,103 @@ use core::result::ResultTrait; use core::traits::TryInto; use debug::PrintTrait; -use konoha::contract::Governance; -use konoha::contract::{IGovernanceDispatcher, IGovernanceDispatcherTrait}; -use konoha::traits::{IGovernanceTokenDispatcher, IGovernanceTokenDispatcherTrait}; -use starknet::{get_block_timestamp, get_caller_address, get_contract_address, ContractAddress}; use konoha::vesting::{IVestingDispatcher, IVestingDispatcherTrait, IVesting}; -use konoha::streaming::{IStreamingDispatcher, IStreamingDispatcherTrait, IStreaming}; - use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; use snforge_std::{ BlockId, declare, ContractClassTrait, ContractClass, start_prank, start_warp, CheatTarget, prank, CheatSpan }; +use starknet::{ContractAddress, get_caller_address}; +use super::setup::{deploy_governance_and_both_tokens}; -use super::setup::{admin_addr, governance_address, deploy_governance, deploy_governance_and_both_tokens, deploy_and_distribute_gov_tokens}; - -fn start_stream(gov: ContractAddress){ +fn test_setup(gov: ContractAddress) { + let grantee: ContractAddress = 0x1.try_into().unwrap(); prank(CheatTarget::One(gov), gov, CheatSpan::TargetCalls(4)); - let streaming = IStreamingDispatcher { contract_address: gov }; - streaming.add_new_stream(get_caller_address(), 0x2.try_into().unwrap(), 100, 200, 100000); + let gov_vesting = IVestingDispatcher { contract_address: gov }; + gov_vesting.add_linear_vesting_schedule(10, 10, 10, 1000000, grantee); } #[test] -fn test_add_new_stream() { +#[should_panic(expected : ('not self-call',))] +fn test_unauthorized_add_vesting_schedule() { let (gov, _, _) = deploy_governance_and_both_tokens(); - start_stream(gov.contract_address); - let streaming = IStreamingDispatcher { contract_address: gov.contract_address }; - - let streamer = get_caller_address(); - let recipient = 0x2.try_into().unwrap(); - let start_time: u64 = 100; - let end_time: u64 = 200; - let total_amount: u128 = 100000; - - streaming.add_new_stream(streamer, recipient, start_time, end_time, total_amount); - //let key = (get_caller_address(), recipient, end_time, start_time); - - let (claimed_amount, stored_total_amount) = streaming.get_stream_info( - streamer, - recipient, - start_time, - end_time, - ); - - assert_eq!(claimed_amount, 0, "Incorrect claimed amount after stream creation"); - assert_eq!(stored_total_amount, total_amount, "Incorrect total amount stored"); -} + test_setup(gov.contract_address); -#[test] -fn test_claim_stream() { - let (gov, _, _) = deploy_governance_and_both_tokens(); - start_stream(gov.contract_address); - let streaming = IStreamingDispatcher { contract_address: gov.contract_address }; + let gov_vesting = IVestingDispatcher { contract_address: gov.contract_address }; + + let caller = get_caller_address(); - let streamer = get_caller_address(); - let recipient = 0x2.try_into().unwrap(); - let start_time: u64 = 100; - let end_time: u64 = 200; - let total_amount: u128 = 100000; + start_warp(CheatTarget::All, 1); + start_prank(CheatTarget::One(gov.contract_address), caller); - streaming.add_new_stream(streamer, recipient, start_time, end_time, total_amount); + let grantee: ContractAddress = 0x1.try_into().unwrap(); - start_warp(CheatTarget::One(gov.contract_address), 150);// middle of stream + gov_vesting.add_linear_vesting_schedule(10, 10, 10, 1000000, grantee); +} - // Claim the stream - streaming.claim_stream(streamer, recipient, start_time, end_time); +#[test] +#[should_panic(expected: ('not yet eligible',))] +fn test_unauthorized_vest_early() { + let (gov, _, _) = deploy_governance_and_both_tokens(); + test_setup(gov.contract_address); - let (claimed_amount, stored_total_amount) = streaming.get_stream_info(streamer, recipient, start_time, end_time); + let gov_vesting = IVestingDispatcher { contract_address: gov.contract_address }; - let expected_claimed_amount = (total_amount * 50) / 100; //should be 50% since middle of stream + start_warp(CheatTarget::All, 1); + start_prank(CheatTarget::One(gov.contract_address), gov.contract_address); - assert_eq!(claimed_amount, expected_claimed_amount, "Incorrect claimed amount after claiming the stream"); - assert_eq!(stored_total_amount, total_amount, "Incorrect total amount stored after claiming the stream"); + let grantee: ContractAddress = 0x1.try_into().unwrap(); - streaming.claim_stream(streamer, recipient, start_time, end_time); - let (claimed_amount, stored_total_amount) = streaming.get_stream_info(streamer, recipient, start_time, end_time); + gov_vesting.add_linear_vesting_schedule(10, 10, 10, 1000000, grantee); - assert_eq!(claimed_amount, expected_claimed_amount, "Claimed amount should remain the same after second claim attempt"); - assert_eq!(stored_total_amount, total_amount, "Total amount stored should remain the same after second claim attempt"); + gov_vesting.vest(grantee, 10); } #[test] -fn test_cancel_stream() { +#[should_panic(expected: ('nothing to vest',))] +fn test_vest_twice() { let (gov, _, _) = deploy_governance_and_both_tokens(); - start_stream(gov.contract_address); - let streaming = IStreamingDispatcher { contract_address: gov.contract_address }; - - let streamer = get_caller_address(); - let recipient = 0x2.try_into().unwrap(); - let start_time: u64 = 100; - let end_time: u64 = 200; - let total_amount: u128 = 100000; - - streaming.add_new_stream(streamer, recipient, start_time, end_time, total_amount); + test_setup(gov.contract_address); - start_warp(CheatTarget::One(gov.contract_address), 150); + let gov_vesting = IVestingDispatcher { contract_address: gov.contract_address }; - streaming.cancel_stream(recipient, start_time, end_time); + start_warp(CheatTarget::All, 1); + start_prank(CheatTarget::One(gov.contract_address), gov.contract_address); - let (claimed_amount, stored_total_amount) = streaming.get_stream_info( - streamer, - recipient, - start_time, - end_time); + let grantee: ContractAddress = 0x1.try_into().unwrap(); - assert_eq!(claimed_amount, 0, "Claimed amount should be 0 after canceling the stream"); - assert_eq!(stored_total_amount, 0, "Total amount should be 0 after canceling the stream"); + gov_vesting.add_linear_vesting_schedule(10, 10, 10, 1000000, grantee); - let unclaimed_amount = claimed_amount - total_amount; - let self_dsp = IGovernanceDispatcher { contract_address: gov.contract_address }; - let token_address = self_dsp.get_governance_token_address(); - let erc20 = IERC20Dispatcher { contract_address: token_address }; + start_warp(CheatTarget::All, 11); - // Check the balance of the streamer (caller address) with ERC_20, I couldnt use balance_of - let balance = erc20.balance_of(get_caller_address()); - - assert_eq!(balance, unclaimed_amount.into(), "Unclaimed amount should be reclaimed correctly"); + gov_vesting.vest(grantee, 10); + gov_vesting.vest(grantee, 10); } +#[test] +fn test_add_simple_vesting_schedule() { + let (gov, token_address, _) = deploy_governance_and_both_tokens(); + test_setup(gov.contract_address); + + let gov_vesting = IVestingDispatcher { contract_address: gov.contract_address }; + let tok = IERC20Dispatcher { contract_address: token_address.contract_address }; + + let grantee: ContractAddress = 0x1.try_into().unwrap(); + gov_vesting.add_linear_vesting_schedule(10, 10, 10, 1000001, grantee); + + start_warp(CheatTarget::All, 11); // past first vest + // anyone can claim for the grantee + gov_vesting.vest(grantee, 10); + assert(tok.balance_of(grantee) == 100000, 'vesting unsuccessful'); + + // grantee themselves can claim too + start_prank(CheatTarget::One(gov.contract_address), grantee); + start_warp(CheatTarget::All, 21); // past second vest + gov_vesting.vest(grantee, 20); + assert(tok.balance_of(grantee) == 200000, 'vesting unsuccessful'); + + start_warp(CheatTarget::All, 101); // past last vest. no requirement to vest in order + gov_vesting.vest(grantee, 100); + // leftover tokens are included in last vest. (remainder after division) + assert(tok.balance_of(grantee) == 300001, 'vesting unsuccessful'); +} From f7fd1defd9121bfa8192a97a4b03526d0e717b37 Mon Sep 17 00:00:00 2001 From: Malachi Nguyen <123039533+scobi7@users.noreply.github.com> Date: Tue, 2 Jul 2024 04:15:46 -0700 Subject: [PATCH 18/34] Fix claim_stream (#112) --- src/streaming.cairo | 72 +++++++++++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 29 deletions(-) diff --git a/src/streaming.cairo b/src/streaming.cairo index 12a03402..92cfb680 100644 --- a/src/streaming.cairo +++ b/src/streaming.cairo @@ -16,7 +16,7 @@ trait IStreaming{ streamer: ContractAddress, recipient: ContractAddress, start_time: u64, - end_time: u64 + end_time: u64, ); fn cancel_stream( @@ -27,7 +27,7 @@ trait IStreaming{ ); fn get_stream_info( - ref self: TContractState, //TContractState? + ref self: TContractState, streamer: ContractAddress, recipient: ContractAddress, start_time: u64, @@ -68,7 +68,10 @@ mod streaming { #[derive(starknet::Event, Drop, Serde)] struct StreamClaimed{ streamer: ContractAddress, - claimed_amount: u128 + recipient: ContractAddress, + start_time: u64, + end_time: u64, + total_amount: u128 } #[derive(starknet::Event, Drop, Serde)] @@ -77,13 +80,15 @@ mod streaming { recipient: ContractAddress, start_time: u64, end_time: u64, - reclaimed_amount: u128 + reclaimed_amount: u256 } + //TODO: #[embeddable_as(StreamingImpl)] impl Streaming< TContractState, +HasComponent > of super::IStreaming> { + fn add_new_stream( ref self: ComponentState, streamer: ContractAddress, @@ -91,20 +96,22 @@ mod streaming { start_time: u64, end_time: u64, total_amount: u128 - ){ - assert(get_caller_address()== get_contract_address(), 'not self-call'); - assert(start_time < end_time, 'starts first'); - + ) { let key = (get_caller_address(), recipient, start_time, end_time); - self.streams.write(key, (0, total_amount)); + assert(get_caller_address() == get_contract_address(), 'not self-call'); + assert(start_time < end_time, 'starts first'); + + let mut claimable_amount = 0; + self.streams.write(key, (claimable_amount, total_amount)); + self.emit(StreamCreated{ streamer: get_caller_address(), recipient: recipient, start_time: start_time, end_time: end_time, total_amount: total_amount - }) + }); } fn claim_stream( @@ -112,43 +119,49 @@ mod streaming { streamer: ContractAddress, recipient: ContractAddress, start_time: u64, - end_time: u64 + end_time: u64, + ){ let current_time = get_block_timestamp(); - let key: (ContractAddress, ContractAddress, u64, u64) = ( - streamer, + let key = ( + get_caller_address(), recipient, start_time, end_time, ); - let (claimed_amount, total_amount): (u128, u128) = self.streams.read(key); - + let (mut claimed_amount, total_amount): (u128, u128) = self.streams.read(key); assert(current_time > start_time, 'stream has not started'); let elapsed_time = if current_time > end_time { end_time - start_time - } else{ + } else{ current_time - start_time }; + let stream_duration = end_time - start_time; - let stream_duration = (end_time - start_time); - - let claimable_amount = (total_amount * elapsed_time.into()) / stream_duration.into(); + + let claimable_amount = (total_amount * elapsed_time.into() / stream_duration.into()); let amount_to_claim = claimable_amount - claimed_amount; + + assert(amount_to_claim > 0, 'nothing to claim'); + claimed_amount += amount_to_claim; - self.streams.write(key, (claimable_amount + amount_to_claim, total_amount));//claimable shld be (claimable_amount + amount_to_claim) - self.streams.read(key); - let self_dsp = IGovernanceDispatcher { contract_address: get_contract_address() }; IGovernanceTokenDispatcher { contract_address: self_dsp.get_governance_token_address() } - .mint(recipient, amount_to_claim.into()); + .mint(recipient, claimed_amount.into()); + + self.streams.write(key, (claimable_amount, total_amount));//claimable shld be (token_to_claim + amount_to_claim)? + self.emit(StreamClaimed{ - streamer: streamer, - claimed_amount: amount_to_claim, + streamer: get_caller_address(), + recipient: recipient, + start_time: start_time, + end_time: end_time, + total_amount: total_amount }) } @@ -167,10 +180,9 @@ mod streaming { // Read from the streams LegacyMap let (claimed_amount, total_amount): (u128, u128) = self.streams.read(key); + let unclaimed_amount: u256 = total_amount.into() - claimed_amount.into(); - let unclaimed_amount = total_amount - claimed_amount; //cancel stream - self.streams.write(key, (0,0)); let self_dsp = IGovernanceDispatcher { contract_address: get_contract_address() }; @@ -193,13 +205,15 @@ mod streaming { end_time: u64, ) -> (u128, u128) { let key: (ContractAddress, ContractAddress, u64, u64) = ( - streamer, + get_caller_address(), recipient, start_time, end_time, ); - self.streams.read(key) + let (mut claimable_amount, mut total_amount) : (u128, u128) = self.streams.read(key); + (claimable_amount, total_amount) } + } } From 05f4cb25634780a9311749455dcecf414c0defaa Mon Sep 17 00:00:00 2001 From: Malachi Nguyen <123039533+scobi7@users.noreply.github.com> Date: Tue, 2 Jul 2024 04:16:21 -0700 Subject: [PATCH 19/34] Fix test_add_new_stream (#112) --- tests/test_streaming.cairo | 204 ++++++++++++++++++++++++++----------- 1 file changed, 143 insertions(+), 61 deletions(-) diff --git a/tests/test_streaming.cairo b/tests/test_streaming.cairo index a7733cc8..7f863170 100644 --- a/tests/test_streaming.cairo +++ b/tests/test_streaming.cairo @@ -4,103 +4,185 @@ use core::result::ResultTrait; use core::traits::TryInto; use debug::PrintTrait; +use konoha::contract::Governance; +use konoha::contract::{IGovernanceDispatcher, IGovernanceDispatcherTrait}; +use konoha::traits::{IGovernanceTokenDispatcher, IGovernanceTokenDispatcherTrait}; +use starknet::{get_block_timestamp, get_caller_address, get_contract_address, ContractAddress}; use konoha::vesting::{IVestingDispatcher, IVestingDispatcherTrait, IVesting}; +use konoha::streaming::{IStreamingDispatcher, IStreamingDispatcherTrait, IStreaming}; + use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; use snforge_std::{ BlockId, declare, ContractClassTrait, ContractClass, start_prank, start_warp, CheatTarget, prank, CheatSpan }; -use starknet::{ContractAddress, get_caller_address}; + use super::setup::{deploy_governance_and_both_tokens}; -fn test_setup(gov: ContractAddress) { - let grantee: ContractAddress = 0x1.try_into().unwrap(); +fn start_stream(gov: ContractAddress){ prank(CheatTarget::One(gov), gov, CheatSpan::TargetCalls(4)); - let gov_vesting = IVestingDispatcher { contract_address: gov }; - gov_vesting.add_linear_vesting_schedule(10, 10, 10, 1000000, grantee); + let streaming = IStreamingDispatcher { contract_address: gov }; + streaming.add_new_stream(get_caller_address(), 0x2.try_into().unwrap(), 100, 200, 100000); } +//passing! #[test] -#[should_panic(expected : ('not self-call',))] -fn test_unauthorized_add_vesting_schedule() { +fn test_add_new_stream() { let (gov, _, _) = deploy_governance_and_both_tokens(); - test_setup(gov.contract_address); + start_stream(gov.contract_address); + let streaming = IStreamingDispatcher { contract_address: gov.contract_address }; + + let streamer = get_caller_address(); + let recipient = 0x2.try_into().unwrap(); + let start_time: u64 = 100; + let end_time: u64 = 200; + let total_amount: u128 = 100000; + + streaming.add_new_stream(streamer, recipient, start_time, end_time, total_amount); + //let key = (get_caller_address(), recipient, end_time, start_time); + + let (claimed_amount, stored_total_amount) = streaming.get_stream_info( + streamer, + recipient, + start_time, + end_time, + ); + + assert_eq!(streamer, get_caller_address(), "Incorrect streamer addr"); + assert_eq!(recipient, 0x2.try_into().unwrap(), "Incorrect streamer addr"); + assert_eq!(start_time, 100, "Incorrect start time"); + assert_eq!(end_time, 200, "Incorrect end time"); + assert_eq!(claimed_amount, 0, "Incorrect claimed amount after stream creation"); + assert_eq!(stored_total_amount, 100000, "Incorrect total amount stored"); +} - let gov_vesting = IVestingDispatcher { contract_address: gov.contract_address }; +//passing! +#[test] +#[should_panic(expected: ('starts first',))] +fn test_valid_stream_time(){ + let (gov, _, _) = deploy_governance_and_both_tokens(); + start_stream(gov.contract_address); + let streaming = IStreamingDispatcher { contract_address: gov.contract_address }; - let caller = get_caller_address(); + let streamer = get_caller_address(); + let recipient = 0x2.try_into().unwrap(); + let start_time: u64 = 200; + let end_time: u64 = 100; + let total_amount: u128 = 100000; - start_warp(CheatTarget::All, 1); - start_prank(CheatTarget::One(gov.contract_address), caller); + streaming.add_new_stream(streamer, recipient, start_time, end_time, total_amount); +} - let grantee: ContractAddress = 0x1.try_into().unwrap(); +//passing! +#[test] +#[should_panic(expected: ('nothing to claim',))] +fn test_claimed_amount(){ + let (gov, _, _) = deploy_governance_and_both_tokens(); + start_stream(gov.contract_address); + let streaming = IStreamingDispatcher { contract_address: gov.contract_address }; + + let streamer = get_caller_address(); + let recipient = 0x2.try_into().unwrap(); + let start_time: u64 = 100; + let end_time: u64 = 200; + let total_amount: u128 = 0; + streaming.add_new_stream(streamer, recipient, start_time, end_time, total_amount); + + start_warp(CheatTarget::One(gov.contract_address), 150); + //shouldn't have anything to claim + streaming.claim_stream(streamer, recipient, start_time, end_time); +} - gov_vesting.add_linear_vesting_schedule(10, 10, 10, 1000000, grantee); +//passing! +#[test] +#[should_panic(expected: ('stream has not started',))] +fn test_stream_started(){ + let (gov, _, _) = deploy_governance_and_both_tokens(); + start_stream(gov.contract_address); + let streaming = IStreamingDispatcher { contract_address: gov.contract_address }; + let streamer = get_caller_address(); + let recipient = 0x2.try_into().unwrap(); + let start_time: u64 = 100; + let end_time: u64 = 200; + let total_amount: u128 = 100000; + streaming.add_new_stream(streamer, recipient, start_time, end_time, total_amount); + start_warp(CheatTarget::One(gov.contract_address), 50);// before of stream + + streaming.claim_stream(streamer, recipient, start_time, end_time); } #[test] -#[should_panic(expected: ('not yet eligible',))] -fn test_unauthorized_vest_early() { +fn test_claim_stream() { let (gov, _, _) = deploy_governance_and_both_tokens(); - test_setup(gov.contract_address); + start_stream(gov.contract_address); + let streaming = IStreamingDispatcher { contract_address: gov.contract_address }; + + let streamer = get_caller_address(); + let recipient = 0x2.try_into().unwrap(); + let start_time: u64 = 100; + let end_time: u64 = 200; + let total_amount: u128 = 100000; - let gov_vesting = IVestingDispatcher { contract_address: gov.contract_address }; + streaming.add_new_stream(streamer, recipient, start_time, end_time, total_amount); + let (claimable_amount, total_amount) = streaming.get_stream_info( + streamer, + recipient, + start_time, + end_time, + ); + start_warp(CheatTarget::One(gov.contract_address), 150); - start_warp(CheatTarget::All, 1); - start_prank(CheatTarget::One(gov.contract_address), gov.contract_address); + streaming.claim_stream(streamer, recipient, start_time, end_time); + - let grantee: ContractAddress = 0x1.try_into().unwrap(); - gov_vesting.add_linear_vesting_schedule(10, 10, 10, 1000000, grantee); + let expected_claimed_amount = (100000 * 100/ 100); //should be 50% since middle of stream + assert_eq!(total_amount, 100000, "Incorrect total amount after claiming the stream"); + assert_eq!(claimable_amount, expected_claimed_amount, "Incorrect claimed amount after claiming the stream"); - gov_vesting.vest(grantee, 10); + streaming.claim_stream(streamer, recipient, start_time, end_time); + let (claimable_amount, total_amount) = streaming.get_stream_info(streamer, recipient, start_time, end_time); + + assert_eq!(claimable_amount, expected_claimed_amount, "Claimed amount should remain the same after second claim attempt"); + assert_eq!(total_amount, total_amount, "Total amount stored should remain the same after second claim attempt"); } #[test] -#[should_panic(expected: ('nothing to vest',))] -fn test_vest_twice() { +fn test_cancel_stream() { let (gov, _, _) = deploy_governance_and_both_tokens(); - test_setup(gov.contract_address); + start_stream(gov.contract_address); + let streaming = IStreamingDispatcher { contract_address: gov.contract_address }; - let gov_vesting = IVestingDispatcher { contract_address: gov.contract_address }; + let streamer = get_caller_address(); + let recipient = 0x2.try_into().unwrap(); + let start_time: u64 = 100; + let end_time: u64 = 200; + let total_amount: u128 = 100000; - start_warp(CheatTarget::All, 1); - start_prank(CheatTarget::One(gov.contract_address), gov.contract_address); + streaming.add_new_stream(streamer, recipient, start_time, end_time, total_amount); - let grantee: ContractAddress = 0x1.try_into().unwrap(); + start_warp(CheatTarget::One(gov.contract_address), 150); - gov_vesting.add_linear_vesting_schedule(10, 10, 10, 1000000, grantee); + //test cancel_stream + streaming.cancel_stream(recipient, start_time, end_time); - start_warp(CheatTarget::All, 11); + let (claimed_amount, stored_total_amount) = streaming.get_stream_info( + streamer, + recipient, + start_time, + end_time); - gov_vesting.vest(grantee, 10); - gov_vesting.vest(grantee, 10); -} + assert_eq!(claimed_amount, 0, "Claimed amount should be 0 after canceling the stream"); + assert_eq!(stored_total_amount, 0, "Total amount should be 0 after canceling the stream"); -#[test] -fn test_add_simple_vesting_schedule() { - let (gov, token_address, _) = deploy_governance_and_both_tokens(); - test_setup(gov.contract_address); - - let gov_vesting = IVestingDispatcher { contract_address: gov.contract_address }; - let tok = IERC20Dispatcher { contract_address: token_address.contract_address }; - - let grantee: ContractAddress = 0x1.try_into().unwrap(); - gov_vesting.add_linear_vesting_schedule(10, 10, 10, 1000001, grantee); - - start_warp(CheatTarget::All, 11); // past first vest - // anyone can claim for the grantee - gov_vesting.vest(grantee, 10); - assert(tok.balance_of(grantee) == 100000, 'vesting unsuccessful'); - - // grantee themselves can claim too - start_prank(CheatTarget::One(gov.contract_address), grantee); - start_warp(CheatTarget::All, 21); // past second vest - gov_vesting.vest(grantee, 20); - assert(tok.balance_of(grantee) == 200000, 'vesting unsuccessful'); - - start_warp(CheatTarget::All, 101); // past last vest. no requirement to vest in order - gov_vesting.vest(grantee, 100); - // leftover tokens are included in last vest. (remainder after division) - assert(tok.balance_of(grantee) == 300001, 'vesting unsuccessful'); + let unclaimed_amount: u256 = total_amount.into() - claimed_amount.into(); //100000 + let self_dsp = IGovernanceDispatcher { contract_address: gov.contract_address }; + let token_address = self_dsp.get_governance_token_address(); + let erc20 = IERC20Dispatcher { contract_address: token_address }; + + // Check the balance of the streamer (caller address) with ERC_20, I couldnt use balance_of + let balance = erc20.balance_of(get_caller_address()); + + assert_eq!(unclaimed_amount.into(), 100000, "Unclaimed amount should be reclaimed correctly"); + assert_eq!(balance, 0, "balance"); } From dd8f9b58ac03ebef7b3c8d0b45a9eb8c3fc83e8e Mon Sep 17 00:00:00 2001 From: Malachi Nguyen <123039533+scobi7@users.noreply.github.com> Date: Tue, 2 Jul 2024 04:50:27 -0700 Subject: [PATCH 20/34] Fix all test cases (#112) --- tests/test_streaming.cairo | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_streaming.cairo b/tests/test_streaming.cairo index 7f863170..2cb8dff8 100644 --- a/tests/test_streaming.cairo +++ b/tests/test_streaming.cairo @@ -133,18 +133,18 @@ fn test_claim_stream() { start_warp(CheatTarget::One(gov.contract_address), 150); streaming.claim_stream(streamer, recipient, start_time, end_time); - - - let expected_claimed_amount = (100000 * 100/ 100); //should be 50% since middle of stream + let expected_claimed_amount = (100000 * 50/ 100); //should be 50% since middle of stream assert_eq!(total_amount, 100000, "Incorrect total amount after claiming the stream"); - assert_eq!(claimable_amount, expected_claimed_amount, "Incorrect claimed amount after claiming the stream"); + assert_eq!(claimable_amount, 0, "Incorrect claimed amount after claiming the stream"); - streaming.claim_stream(streamer, recipient, start_time, end_time); - let (claimable_amount, total_amount) = streaming.get_stream_info(streamer, recipient, start_time, end_time); + let self_dsp = IGovernanceDispatcher { contract_address: gov.contract_address }; + let token_address = self_dsp.get_governance_token_address(); + let erc20 = IERC20Dispatcher { contract_address: token_address }; + + let balance = erc20.balance_of(recipient); - assert_eq!(claimable_amount, expected_claimed_amount, "Claimed amount should remain the same after second claim attempt"); - assert_eq!(total_amount, total_amount, "Total amount stored should remain the same after second claim attempt"); + assert_eq!(balance, expected_claimed_amount, "Balance should match the expected claimed amount"); } #[test] From d4ee4c466885f78194f31ec84ab992eb99e862b8 Mon Sep 17 00:00:00 2001 From: Malachi Nguyen <123039533+scobi7@users.noreply.github.com> Date: Tue, 2 Jul 2024 04:53:11 -0700 Subject: [PATCH 21/34] Sort module level items with scarb fmt (#112) --- src/lib.cairo | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.cairo b/src/lib.cairo index 4f36a85b..20ba0ca1 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -5,6 +5,7 @@ mod discussion; mod merkle_tree; mod proposals; mod staking; +mod streaming; mod token; mod traits; mod treasury; From bd746aadd700f92960a0365a0f20d3b5b1131577 Mon Sep 17 00:00:00 2001 From: Malachi Nguyen <123039533+scobi7@users.noreply.github.com> Date: Tue, 2 Jul 2024 04:54:25 -0700 Subject: [PATCH 22/34] Sort module level items with scarb fmt (#118) --- tests/lib.cairo | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/lib.cairo b/tests/lib.cairo index 222fa353..a7df965e 100644 --- a/tests/lib.cairo +++ b/tests/lib.cairo @@ -4,5 +4,7 @@ mod proposals_tests; mod setup; mod staking_tests; mod test_storage_pack; +mod test_streaming; mod test_treasury; mod upgrades_tests; +mod vesting; From 82eadce814aab02700807722bcd325f9460dc407 Mon Sep 17 00:00:00 2001 From: Malachi Nguyen <123039533+scobi7@users.noreply.github.com> Date: Tue, 2 Jul 2024 04:55:15 -0700 Subject: [PATCH 23/34] Sort module level items with scarb fmt (#112) From cc31c38c59735141546b73667f86a810d5c030a4 Mon Sep 17 00:00:00 2001 From: Malachi Nguyen <123039533+scobi7@users.noreply.github.com> Date: Tue, 2 Jul 2024 04:55:31 -0700 Subject: [PATCH 24/34] Update lib.cairo --- tests/lib.cairo | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/lib.cairo b/tests/lib.cairo index a7df965e..6d57963b 100644 --- a/tests/lib.cairo +++ b/tests/lib.cairo @@ -7,4 +7,3 @@ mod test_storage_pack; mod test_streaming; mod test_treasury; mod upgrades_tests; -mod vesting; From a86734d4429e61a6b978c00d63ad59aaf9a17b67 Mon Sep 17 00:00:00 2001 From: Malachi Nguyen <123039533+scobi7@users.noreply.github.com> Date: Tue, 2 Jul 2024 04:55:43 -0700 Subject: [PATCH 25/34] Sort module level items with scarb fmt (#112) --- tests/lib.cairo | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/lib.cairo b/tests/lib.cairo index 6d57963b..a7df965e 100644 --- a/tests/lib.cairo +++ b/tests/lib.cairo @@ -7,3 +7,4 @@ mod test_storage_pack; mod test_streaming; mod test_treasury; mod upgrades_tests; +mod vesting; From 430f6666fdc10f733c8f0dda3812ff0a87ca9cb3 Mon Sep 17 00:00:00 2001 From: Malachi Nguyen <123039533+scobi7@users.noreply.github.com> Date: Tue, 2 Jul 2024 05:15:12 -0700 Subject: [PATCH 26/34] Update Sepolia URL (#112) not sure which version this is but ill let Ondrej know in discord that there is a new URL for sepolia --- Scarb.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Scarb.toml b/Scarb.toml index 5a4fc115..84ab01bd 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -28,7 +28,7 @@ block_id.tag = "Latest" [[tool.snforge.fork]] name = "SEPOLIA" -url = "http://34.22.208.73:6062/v0_7" +url = "http://178.32.172.148:6062/v0_7" block_id.tag = "Latest" RUST_BACKTRACE = 1 From fb947ae9c1f4469ca044c15f38a4e369f262ec3c Mon Sep 17 00:00:00 2001 From: root Date: Sun, 7 Jul 2024 13:32:07 +0000 Subject: [PATCH 27/34] implement new staking protocol, incomplete (#82) --- src/staking.cairo | 494 +++++++++++++++++++++------------------------- 1 file changed, 228 insertions(+), 266 deletions(-) diff --git a/src/staking.cairo b/src/staking.cairo index cfaea115..8974f1f8 100644 --- a/src/staking.cairo +++ b/src/staking.cairo @@ -1,286 +1,176 @@ use starknet::ContractAddress; -// This component should not be used along with delegation, as when the tokens are unstaked, they are not automatically undelegated. - #[starknet::interface] -trait IStaking { - fn stake(ref self: TContractState, length: u64, amount: u128) -> u32; // returns stake ID - fn unstake(ref self: TContractState, id: u32); - fn unstake_airdrop(ref self: TContractState, amount: u128); - - fn set_curve_point(ref self: TContractState, length: u64, conversion_rate: u16); - fn set_floating_token_address(ref self: TContractState, address: ContractAddress); - - fn get_floating_token_address(self: @TContractState) -> ContractAddress; - fn get_stake(self: @TContractState, address: ContractAddress, stake_id: u32) -> staking::Stake; - fn get_total_voting_power(self: @TContractState, address: ContractAddress) -> u128; +trait IVotingEscrow { + fn create_lock(ref self: TContractState, value: u128, unlock_time: u64); //creates lock -> tokens staked, for how long (the timestamp until which the tokens are locked.) + fn increase_amount(ref self: TContractState, value: u128); + fn increase_unlock_time(ref self: TContractState, unlock_time: u64); + fn withdraw(ref self: TContractState); + + fn balance_of(self: @TContractState, addr: ContractAddress) -> u128; + fn balance_of_at(self: @TContractState, addr: ContractAddress, block: u64) -> u128; + fn total_supply(self: @TContractState) -> u128; + fn total_supply_at(self: @TContractState, block: u64) -> u128; + fn locked(self: @TContractState, addr: ContractAddress) -> (u128, u64); } #[starknet::component] -mod staking { - use konoha::traits::{ - get_governance_token_address_self, IERC20Dispatcher, IERC20DispatcherTrait - }; +mod voting_escrow { + use core::traits::Into; +use super::IVotingEscrow; + use konoha::traits::{IERC20Dispatcher, IERC20DispatcherTrait}; use starknet::{ - ContractAddress, get_block_timestamp, get_caller_address, get_contract_address, StorePacking + ContractAddress, get_block_timestamp, get_block_number, get_caller_address, get_contract_address }; - use zeroable::NonZero; - use zeroable::NonZeroIntoImpl; - - #[derive(Copy, Drop, Serde)] - struct Stake { - amount_staked: u128, - amount_voting_token: u128, - start_date: u64, - length: u64, - withdrawn: bool - } - const TWO_POW_64: u128 = 0x10000000000000000; - const TWO_POW_128: felt252 = 0x100000000000000000000000000000000; - const TWO_POW_192: felt252 = 0x1000000000000000000000000000000000000000000000000; - - impl StakeStorePacking of StorePacking { - fn pack(value: Stake) -> (felt252, felt252) { - let fst = value.amount_staked.into() + value.start_date.into() * TWO_POW_128; - let snd = value.amount_voting_token.into() - + value.length.into() * TWO_POW_128 - + value.withdrawn.into() * TWO_POW_192; - (fst.into(), snd.into()) - } + const WEEK: u64 = 7 * 86400; // 7 days in seconds + const MAXTIME: u64 = 4 * 365 * 86400; // 4 years in seconds + const MULTIPLIER: u128 = 10_000_000_000; // To handle decimals + + //deposit types + const DEPOSIT_TYPE_CREATE: u8 = 0; + const DEPOSIT_TYPE_INCREASE_AMOUNT: u8 = 1; + const DEPOSIT_TYPE_INCREASE_TIME: u8 = 2; + + #[derive(starknet::Event, Drop, Serde)] + struct Point { + bias: u128, //token amount + slope: u128, //decay rate (token amount / stake time) + ts: u64, //time stamp + blk: u64, //block number + } - fn unpack(value: (felt252, felt252)) -> Stake { - let (fst, snd) = value; - let fst: u256 = fst.into(); - let amount_staked = fst.low; - let start_date = fst.high; - let snd: u256 = snd.into(); - let amount_voting_token = snd.low; - let two_pow_64: NonZero = TWO_POW_64.try_into().unwrap(); - let (withdrawn, length) = DivRem::div_rem(snd.high, two_pow_64); - assert(withdrawn == 0 || withdrawn == 1, 'wrong val: withdrawn'); - Stake { - amount_staked, - amount_voting_token, - start_date: start_date.try_into().expect('unwrap fail start_date'), - length: length.try_into().expect('unpack fail length'), - withdrawn: withdrawn != 0 - } - } + #[derive(starknet::Event, Drop, Serde)] + struct LockedBalance { + amount: u128, + end: u64, } #[storage] struct Storage { - stake: LegacyMap::< - (ContractAddress, u32), Stake - >, // STAKE(address, ID) → Stake{amount staked, amount voting token, start date, length of stake, withdrawn} - curve: LegacyMap::< - u64, u16 - >, // length of stake > CARM to veCARM conversion rate (conversion rate is expressed in % – 2:1 is 200) - floating_token_address: ContractAddress + token: ContractAddress, //locked ERC20 token address + epoch: u64, //change epochs, incrememnts by one every change + point_history: LegacyMap::, //voting power history (global) + user_point_history: LegacyMap::<(ContractAddress, u64), Point>, //voting power history (user) + user_point_epoch: LegacyMap::, //latest epoch number for user + slope_changes: LegacyMap::, //scheduled change in slope + locked: LegacyMap::, //locked amount } - #[derive(starknet::Event, Drop)] - struct Staked { - user: ContractAddress, - stake_id: u32, - amount: u128, - amount_voting_token: u128, - start_date: u64, - length: u64 + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + Deposit: Deposit, + Withdraw: Withdraw, } - #[derive(starknet::Event, Drop)] - struct Unstaked { - user: ContractAddress, - stake_id: u32, - amount: u128, - amount_voting_token: u128, - start_date: u64, - length: u64 + #[derive(starknet::Event, Drop, Serde)] + struct Deposit { + provider: ContractAddress, + value: u128, + locktime: u64, + type_: u8, + ts: u64, } - #[derive(starknet::Event, Drop)] - struct UnstakedAirdrop { - user: ContractAddress, - amount: u128 + #[derive(starknet::Event, Drop, Serde)] + struct Withdraw { + provider: ContractAddress, + value: u128, + ts: u64, } - #[derive(starknet::Event, Drop)] - #[event] - enum Event { - Staked: Staked, - Unstaked: Unstaked, - UnstakedAirdrop: UnstakedAirdrop - } + #[embeddable_as(VotingEscrowImpl)] + impl VotingEscrow< + TContractState, +HasComponent + > of super::IVotingEscrow> { + fn balance_of(self: @ComponentState, addr: ContractAddress) -> u128 { + self._balance_of(addr, get_block_timestamp()) + } - #[embeddable_as(StakingImpl)] - impl Staking< - TContractState, +HasComponent, - > of super::IStaking> { - fn stake(ref self: ComponentState, length: u64, amount: u128) -> u32 { - let caller = get_caller_address(); + fn balance_of_at( + self: @ComponentState, addr: ContractAddress, block: u64 + ) -> u128 { + self._balance_of_at(addr, block) + } - assert(amount != 0, 'amount to stake is zero'); - let conversion_rate: u16 = self.curve.read(length); - assert(conversion_rate != 0, 'unsupported stake length'); - - let floating_token = IERC20Dispatcher { - contract_address: self.floating_token_address.read() - }; - floating_token.transfer_from(caller, get_contract_address(), amount.into()); - - let (amount_voting_token, _) = DivRem::div_rem((amount * conversion_rate.into()), 100); - let free_id = self.get_free_stake_id(caller); - - self - .stake - .write( - (caller, free_id), - Stake { - amount_staked: amount, - amount_voting_token, - start_date: get_block_timestamp(), - length, - withdrawn: false - } - ); + fn total_supply(self: @ComponentState) -> u128 { + self._supply_at(get_block_timestamp()) + } - let voting_token = IERC20Dispatcher { - contract_address: get_governance_token_address_self() - }; - voting_token.mint(caller, amount_voting_token.into()); - self - .emit( - Staked { - user: caller, - stake_id: free_id, - amount, - amount_voting_token, - start_date: get_block_timestamp(), - length - } - ); - free_id + fn total_supply_at(self: @ComponentState, block: u64) -> u128 { + self._supply_at(block) } - fn unstake(ref self: ComponentState, id: u32) { - let caller = get_caller_address(); - let res: Stake = self.stake.read((caller, id)); - - assert(!res.withdrawn, 'stake withdrawn already'); - - assert(res.amount_staked != 0, 'no stake found, check stake id'); - let unlock_date = res.start_date + res.length; - assert(get_block_timestamp() > unlock_date, 'unlock time not yet reached'); - - let voting_token = IERC20Dispatcher { - contract_address: get_governance_token_address_self() - }; - voting_token.burn(caller, res.amount_voting_token.into()); - - let floating_token = IERC20Dispatcher { - contract_address: self.floating_token_address.read() - }; - // user gets back the same amount of tokens they put in. - // the payoff is in holding voting tokens, which make the user eligible for distributions of protocol revenue - // works for tokens with fixed max float - floating_token.transfer(caller, res.amount_staked.into()); - self - .stake - .write( - (caller, id), - Stake { - amount_staked: res.amount_staked, - amount_voting_token: res.amount_voting_token, - start_date: res.start_date, - length: res.length, - withdrawn: true - } - ); - self - .emit( - Unstaked { - user: caller, - stake_id: id, - amount: res.amount_staked, - amount_voting_token: res.amount_voting_token, - start_date: res.start_date, - length: res.length - } - ); + fn locked(self: @ComponentState, addr: ContractAddress) -> (u128, u64) { + let locked = self.locked.read(addr); + (locked.amount, locked.end) } - fn unstake_airdrop(ref self: ComponentState, amount: u128) { - let caller = get_caller_address(); + //using create_lock, locking 4000veCRM for 4 years - let total_staked = self.get_total_staked_accounted(caller); // manually staked tokens - let voting_token = IERC20Dispatcher { - contract_address: get_governance_token_address_self() - }; - let voting_token_balance = voting_token.balance_of(caller).try_into().unwrap(); - assert( - voting_token_balance > total_staked, 'no extra tokens to unstake' - ); // potentially unnecessary (underflow checks), but provides for a better error message - let to_unstake = voting_token_balance - total_staked; - - // burn voting token, mint floating token - let voting_token = IERC20Dispatcher { - contract_address: get_governance_token_address_self() - }; - voting_token.burn(caller, to_unstake.into()); - let floating_token = IERC20Dispatcher { - contract_address: self.floating_token_address.read() - }; - floating_token.transfer(caller, to_unstake.into()); - self.emit(UnstakedAirdrop { user: caller, amount: to_unstake }); - } + // const FOUR_YEARS_IN_SECONDS: u64 = 4 * 365 * 24 * 60 * 60; // 126144000 seconds + // let amount = 4000 * 10_u128.pow(18); // Assuming 18 decimal places + // let current_time = get_block_timestamp(); + // let unlock_time = current_time + FOUR_YEARS_IN_SECONDS; + //create_lock(amount, unlock_time); + + fn create_lock( + ref self: ComponentState, value: u128, unlock_time: u64 + ){ //users will create a lock with the amount of tokens and time to stake it in (seconds)? - fn set_curve_point( - ref self: ComponentState, length: u64, conversion_rate: u16 - ) { let caller = get_caller_address(); - let myaddr = get_contract_address(); - assert(caller == myaddr, 'can only call from proposal'); - self.curve.write(length, conversion_rate); + + let locked = self.locked.read(caller); + assert(locked == 0, 'Withdraw old tokens first'); + assert(value > 0, 'Need non-zero value'); + assert_gt!(unlock_time, get_block_timestamp(), "can only lock in the future"); + assert(unlock_time <= get_block_timestamp() + MAXTIME, 'Voting lock can be 4 years max'); + + self._deposit_for(caller, value, unlock_time, locked, DEPOSIT_TYPE_CREATE); } - fn set_floating_token_address( - ref self: ComponentState, address: ContractAddress - ) { + fn increase_amount(ref self: ComponentState, value: u128) { let caller = get_caller_address(); - let myaddr = get_contract_address(); - assert(caller == myaddr, 'can only call from proposal'); - self.floating_token_address.write(address); - } + let locked = self.locked.read(caller); + assert(value > 0, 'Need non-zero value'); + assert(locked.amount > 0, 'No existing lock found'); + assert(locked.end > get_block_timestamp(), 'Cannot add to expired lock'); - fn get_floating_token_address(self: @ComponentState) -> ContractAddress { - self.floating_token_address.read() + self._deposit_for(caller, value, 0, locked, DEPOSIT_TYPE_INCREASE_AMOUNT); } - fn get_stake( - self: @ComponentState, address: ContractAddress, stake_id: u32 - ) -> Stake { - self.stake.read((address, stake_id)) + fn increase_unlock_time(ref self: ComponentState, unlock_time: u64) { + let caller = get_caller_address(); + let locked = self.locked.read(caller); + assert(locked > 0, 'No existing lock found'); + assert(locked.end > get_block_timestamp(), 'Lock expired'); + assert(unlock_time > locked.end, 'Can only increase lock duration'); + assert(unlock_time <= get_block_timestamp() + MAXTIME, 'Voting lock can be 4 years max'); + + self._deposit_for(caller, 0, unlock_time, locked, DEPOSIT_TYPE_INCREASE_TIME); } - fn get_total_voting_power( - self: @ComponentState, address: ContractAddress - ) -> u128 { - let mut id = 0; - let mut acc = 0; - let currtime = get_block_timestamp(); - loop { - let res: Stake = self.stake.read((address, id)); - if (res.amount_voting_token == 0) { - break acc; - } - id += 1; - let not_expired: bool = currtime < (res.length + res.start_date); - if (not_expired) { - acc += res.amount_voting_token; - } - } + fn withdraw(ref self: ComponentState) { + let caller = get_caller_address(); + let locked = self.locked.read(caller); + assert(get_block_timestamp() >= locked.end, 'The lock did not expire'); + let value = locked; + + self.locked.write(caller, LockedBalance { amount: 0, end: 0 }); + let user_epoch = self.user_point_epoch.read(caller); + self.user_point_epoch.write(caller, user_epoch + 1); + self.user_point_history.write( + (caller, user_epoch + 1), + Point { bias: 0, slope: 0, ts: get_block_timestamp(), blk: get_block_number() } + ); + self._checkpoint(caller, locked, LockedBalance { amount: 0, end: 0 }); + + assert(value > 0, 'Withdrawing zero amount'); + let token = IERC20Dispatcher { contract_address: self.token.read() }; + token.transfer(caller, value); + + self.emit(Withdraw { provider: caller, value, ts: get_block_timestamp() }); } } @@ -288,37 +178,109 @@ mod staking { impl InternalImpl< TContractState, +HasComponent > of InternalTrait { - fn get_free_stake_id( - self: @ComponentState, address: ContractAddress - ) -> u32 { - self._get_free_stake_id(address, 0) + fn _deposit_for( + ref self: ComponentState, + addr: ContractAddress, + value: u128, + unlock_time: u64, + locked_balance: u128, + type_: u8 + ) { + let _type = type_; + let mut locked = locked_balance; + let supply_before = self._supply_at(get_block_timestamp()); + + self.token.read().transfer_from(addr, get_contract_address(), value); + + let mut amount = locked.amount + value; + assert(amount > 0, 'Insufficient deposit'); + + let unlock_time = if unlock_time == 0 { locked.end } else { unlock_time }; + assert(unlock_time > get_block_timestamp(), 'Can only lock until time in the future'); + assert(unlock_time <= get_block_timestamp() + MAXTIME, 'Voting lock can be 4 years max'); + + self.locked.write(addr, LockedBalance { amount, end: unlock_time }); + + // Record global and per-user point + self._checkpoint(addr, locked, LockedBalance { amount, end: unlock_time }); + + if value != 0 { + self.emit(Deposit { + provider: addr, value, locktime: unlock_time, type_: _type, ts: get_block_timestamp() + }); + } } - fn _get_free_stake_id( - self: @ComponentState, address: ContractAddress, id: u32 - ) -> u32 { - let res: Stake = self.stake.read((address, id)); - if (res.amount_staked == 0) { - id + fn _checkpoint( + ref self: ComponentState, + addr: ContractAddress, + old_locked: LockedBalance, + new_locked: LockedBalance + ) { + let mut u_old = old_locked; + let mut u_new = new_locked; + let mut epoch = self.epoch.read(); + + self.point_history.write( + epoch, + Point { + bias: 0, + slope: 0, + ts: get_block_timestamp(), + blk: get_block_number() + } + ); + + if addr != starknet::contract_address_const::<0>() { + let mut user_epoch = self.user_point_epoch.read(addr); + self.user_point_epoch.write(addr, user_epoch + 1); + self.user_point_history.write( + (addr, user_epoch + 1), + Point { + bias: 0, + slope: 0, + ts: get_block_timestamp(), + blk: get_block_number() + } + ); + } + + self.epoch.write(epoch + 1); + } + + fn _balance_of(self: @ComponentState, addr: ContractAddress, t: u64) -> u128 { + let _t = t; + let locked = self.locked.read(addr); + if _t >= locked.end { + 0 } else { - self._get_free_stake_id(address, id + 1) + locked.amount * (_t.into() - locked.end.into()) / MAXTIME.into(); } } - fn get_total_staked_accounted( - self: @ComponentState, address: ContractAddress + fn _balance_of_at( + self: @ComponentState, addr: ContractAddress, block: u64 ) -> u128 { - let mut id = 0; - let mut acc = 0; - loop { - let res: Stake = self.stake.read((address, id)); - if (res.amount_voting_token == 0) { - break acc; - } - id += 1; - if (!res.withdrawn) { - acc += res.amount_voting_token; + // TODO: Implement historic balance calculation + 0 + } + + fn _supply_at(self: @ComponentState, t: u64) -> u128 { + let _t = t; + let epoch = self.epoch.read(); + let last_point = self.point_history.read(epoch); + + if _t > last_point.ts { + last_point.bias + } else { + let mut point = last_point; + let mut d_block = 0; + let mut d_t = 0; + if _t > point.ts { + d_block = block - point.blk; + d_t = _t - point.ts; } + point.bias - point.slope * d_t.into(); } } } From aa4b921466bcbe91ab4a2bc3c37174d08e089adc Mon Sep 17 00:00:00 2001 From: root Date: Sun, 7 Jul 2024 14:14:56 +0000 Subject: [PATCH 28/34] Revert "implement new staking protocol, incomplete (#82)" This reverts commit fb947ae9c1f4469ca044c15f38a4e369f262ec3c. --- src/staking.cairo | 494 +++++++++++++++++++++++++--------------------- 1 file changed, 266 insertions(+), 228 deletions(-) diff --git a/src/staking.cairo b/src/staking.cairo index 8974f1f8..cfaea115 100644 --- a/src/staking.cairo +++ b/src/staking.cairo @@ -1,176 +1,286 @@ use starknet::ContractAddress; +// This component should not be used along with delegation, as when the tokens are unstaked, they are not automatically undelegated. + #[starknet::interface] -trait IVotingEscrow { - fn create_lock(ref self: TContractState, value: u128, unlock_time: u64); //creates lock -> tokens staked, for how long (the timestamp until which the tokens are locked.) - fn increase_amount(ref self: TContractState, value: u128); - fn increase_unlock_time(ref self: TContractState, unlock_time: u64); - fn withdraw(ref self: TContractState); - - fn balance_of(self: @TContractState, addr: ContractAddress) -> u128; - fn balance_of_at(self: @TContractState, addr: ContractAddress, block: u64) -> u128; - fn total_supply(self: @TContractState) -> u128; - fn total_supply_at(self: @TContractState, block: u64) -> u128; - fn locked(self: @TContractState, addr: ContractAddress) -> (u128, u64); +trait IStaking { + fn stake(ref self: TContractState, length: u64, amount: u128) -> u32; // returns stake ID + fn unstake(ref self: TContractState, id: u32); + fn unstake_airdrop(ref self: TContractState, amount: u128); + + fn set_curve_point(ref self: TContractState, length: u64, conversion_rate: u16); + fn set_floating_token_address(ref self: TContractState, address: ContractAddress); + + fn get_floating_token_address(self: @TContractState) -> ContractAddress; + fn get_stake(self: @TContractState, address: ContractAddress, stake_id: u32) -> staking::Stake; + fn get_total_voting_power(self: @TContractState, address: ContractAddress) -> u128; } #[starknet::component] -mod voting_escrow { - use core::traits::Into; -use super::IVotingEscrow; - use konoha::traits::{IERC20Dispatcher, IERC20DispatcherTrait}; +mod staking { + use konoha::traits::{ + get_governance_token_address_self, IERC20Dispatcher, IERC20DispatcherTrait + }; use starknet::{ - ContractAddress, get_block_timestamp, get_block_number, get_caller_address, get_contract_address + ContractAddress, get_block_timestamp, get_caller_address, get_contract_address, StorePacking }; - - const WEEK: u64 = 7 * 86400; // 7 days in seconds - const MAXTIME: u64 = 4 * 365 * 86400; // 4 years in seconds - const MULTIPLIER: u128 = 10_000_000_000; // To handle decimals - - //deposit types - const DEPOSIT_TYPE_CREATE: u8 = 0; - const DEPOSIT_TYPE_INCREASE_AMOUNT: u8 = 1; - const DEPOSIT_TYPE_INCREASE_TIME: u8 = 2; - - #[derive(starknet::Event, Drop, Serde)] - struct Point { - bias: u128, //token amount - slope: u128, //decay rate (token amount / stake time) - ts: u64, //time stamp - blk: u64, //block number + use zeroable::NonZero; + use zeroable::NonZeroIntoImpl; + + #[derive(Copy, Drop, Serde)] + struct Stake { + amount_staked: u128, + amount_voting_token: u128, + start_date: u64, + length: u64, + withdrawn: bool } - #[derive(starknet::Event, Drop, Serde)] - struct LockedBalance { - amount: u128, - end: u64, + const TWO_POW_64: u128 = 0x10000000000000000; + const TWO_POW_128: felt252 = 0x100000000000000000000000000000000; + const TWO_POW_192: felt252 = 0x1000000000000000000000000000000000000000000000000; + + impl StakeStorePacking of StorePacking { + fn pack(value: Stake) -> (felt252, felt252) { + let fst = value.amount_staked.into() + value.start_date.into() * TWO_POW_128; + let snd = value.amount_voting_token.into() + + value.length.into() * TWO_POW_128 + + value.withdrawn.into() * TWO_POW_192; + (fst.into(), snd.into()) + } + + fn unpack(value: (felt252, felt252)) -> Stake { + let (fst, snd) = value; + let fst: u256 = fst.into(); + let amount_staked = fst.low; + let start_date = fst.high; + let snd: u256 = snd.into(); + let amount_voting_token = snd.low; + let two_pow_64: NonZero = TWO_POW_64.try_into().unwrap(); + let (withdrawn, length) = DivRem::div_rem(snd.high, two_pow_64); + assert(withdrawn == 0 || withdrawn == 1, 'wrong val: withdrawn'); + Stake { + amount_staked, + amount_voting_token, + start_date: start_date.try_into().expect('unwrap fail start_date'), + length: length.try_into().expect('unpack fail length'), + withdrawn: withdrawn != 0 + } + } } #[storage] struct Storage { - token: ContractAddress, //locked ERC20 token address - epoch: u64, //change epochs, incrememnts by one every change - point_history: LegacyMap::, //voting power history (global) - user_point_history: LegacyMap::<(ContractAddress, u64), Point>, //voting power history (user) - user_point_epoch: LegacyMap::, //latest epoch number for user - slope_changes: LegacyMap::, //scheduled change in slope - locked: LegacyMap::, //locked amount + stake: LegacyMap::< + (ContractAddress, u32), Stake + >, // STAKE(address, ID) → Stake{amount staked, amount voting token, start date, length of stake, withdrawn} + curve: LegacyMap::< + u64, u16 + >, // length of stake > CARM to veCARM conversion rate (conversion rate is expressed in % – 2:1 is 200) + floating_token_address: ContractAddress } - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - Deposit: Deposit, - Withdraw: Withdraw, + #[derive(starknet::Event, Drop)] + struct Staked { + user: ContractAddress, + stake_id: u32, + amount: u128, + amount_voting_token: u128, + start_date: u64, + length: u64 } - #[derive(starknet::Event, Drop, Serde)] - struct Deposit { - provider: ContractAddress, - value: u128, - locktime: u64, - type_: u8, - ts: u64, + #[derive(starknet::Event, Drop)] + struct Unstaked { + user: ContractAddress, + stake_id: u32, + amount: u128, + amount_voting_token: u128, + start_date: u64, + length: u64 } - #[derive(starknet::Event, Drop, Serde)] - struct Withdraw { - provider: ContractAddress, - value: u128, - ts: u64, + #[derive(starknet::Event, Drop)] + struct UnstakedAirdrop { + user: ContractAddress, + amount: u128 } - #[embeddable_as(VotingEscrowImpl)] - impl VotingEscrow< - TContractState, +HasComponent - > of super::IVotingEscrow> { - fn balance_of(self: @ComponentState, addr: ContractAddress) -> u128 { - self._balance_of(addr, get_block_timestamp()) - } + #[derive(starknet::Event, Drop)] + #[event] + enum Event { + Staked: Staked, + Unstaked: Unstaked, + UnstakedAirdrop: UnstakedAirdrop + } - fn balance_of_at( - self: @ComponentState, addr: ContractAddress, block: u64 - ) -> u128 { - self._balance_of_at(addr, block) - } + #[embeddable_as(StakingImpl)] + impl Staking< + TContractState, +HasComponent, + > of super::IStaking> { + fn stake(ref self: ComponentState, length: u64, amount: u128) -> u32 { + let caller = get_caller_address(); - fn total_supply(self: @ComponentState) -> u128 { - self._supply_at(get_block_timestamp()) - } + assert(amount != 0, 'amount to stake is zero'); + let conversion_rate: u16 = self.curve.read(length); + assert(conversion_rate != 0, 'unsupported stake length'); + + let floating_token = IERC20Dispatcher { + contract_address: self.floating_token_address.read() + }; + floating_token.transfer_from(caller, get_contract_address(), amount.into()); + + let (amount_voting_token, _) = DivRem::div_rem((amount * conversion_rate.into()), 100); + let free_id = self.get_free_stake_id(caller); + + self + .stake + .write( + (caller, free_id), + Stake { + amount_staked: amount, + amount_voting_token, + start_date: get_block_timestamp(), + length, + withdrawn: false + } + ); - fn total_supply_at(self: @ComponentState, block: u64) -> u128 { - self._supply_at(block) + let voting_token = IERC20Dispatcher { + contract_address: get_governance_token_address_self() + }; + voting_token.mint(caller, amount_voting_token.into()); + self + .emit( + Staked { + user: caller, + stake_id: free_id, + amount, + amount_voting_token, + start_date: get_block_timestamp(), + length + } + ); + free_id } - fn locked(self: @ComponentState, addr: ContractAddress) -> (u128, u64) { - let locked = self.locked.read(addr); - (locked.amount, locked.end) + fn unstake(ref self: ComponentState, id: u32) { + let caller = get_caller_address(); + let res: Stake = self.stake.read((caller, id)); + + assert(!res.withdrawn, 'stake withdrawn already'); + + assert(res.amount_staked != 0, 'no stake found, check stake id'); + let unlock_date = res.start_date + res.length; + assert(get_block_timestamp() > unlock_date, 'unlock time not yet reached'); + + let voting_token = IERC20Dispatcher { + contract_address: get_governance_token_address_self() + }; + voting_token.burn(caller, res.amount_voting_token.into()); + + let floating_token = IERC20Dispatcher { + contract_address: self.floating_token_address.read() + }; + // user gets back the same amount of tokens they put in. + // the payoff is in holding voting tokens, which make the user eligible for distributions of protocol revenue + // works for tokens with fixed max float + floating_token.transfer(caller, res.amount_staked.into()); + self + .stake + .write( + (caller, id), + Stake { + amount_staked: res.amount_staked, + amount_voting_token: res.amount_voting_token, + start_date: res.start_date, + length: res.length, + withdrawn: true + } + ); + self + .emit( + Unstaked { + user: caller, + stake_id: id, + amount: res.amount_staked, + amount_voting_token: res.amount_voting_token, + start_date: res.start_date, + length: res.length + } + ); } - //using create_lock, locking 4000veCRM for 4 years - - // const FOUR_YEARS_IN_SECONDS: u64 = 4 * 365 * 24 * 60 * 60; // 126144000 seconds - // let amount = 4000 * 10_u128.pow(18); // Assuming 18 decimal places - // let current_time = get_block_timestamp(); - // let unlock_time = current_time + FOUR_YEARS_IN_SECONDS; - //create_lock(amount, unlock_time); - - fn create_lock( - ref self: ComponentState, value: u128, unlock_time: u64 - ){ //users will create a lock with the amount of tokens and time to stake it in (seconds)? - + fn unstake_airdrop(ref self: ComponentState, amount: u128) { let caller = get_caller_address(); - let locked = self.locked.read(caller); - assert(locked == 0, 'Withdraw old tokens first'); - assert(value > 0, 'Need non-zero value'); - assert_gt!(unlock_time, get_block_timestamp(), "can only lock in the future"); - assert(unlock_time <= get_block_timestamp() + MAXTIME, 'Voting lock can be 4 years max'); - - self._deposit_for(caller, value, unlock_time, locked, DEPOSIT_TYPE_CREATE); + let total_staked = self.get_total_staked_accounted(caller); // manually staked tokens + let voting_token = IERC20Dispatcher { + contract_address: get_governance_token_address_self() + }; + let voting_token_balance = voting_token.balance_of(caller).try_into().unwrap(); + assert( + voting_token_balance > total_staked, 'no extra tokens to unstake' + ); // potentially unnecessary (underflow checks), but provides for a better error message + let to_unstake = voting_token_balance - total_staked; + + // burn voting token, mint floating token + let voting_token = IERC20Dispatcher { + contract_address: get_governance_token_address_self() + }; + voting_token.burn(caller, to_unstake.into()); + let floating_token = IERC20Dispatcher { + contract_address: self.floating_token_address.read() + }; + floating_token.transfer(caller, to_unstake.into()); + self.emit(UnstakedAirdrop { user: caller, amount: to_unstake }); } - fn increase_amount(ref self: ComponentState, value: u128) { + fn set_curve_point( + ref self: ComponentState, length: u64, conversion_rate: u16 + ) { let caller = get_caller_address(); - let locked = self.locked.read(caller); - assert(value > 0, 'Need non-zero value'); - assert(locked.amount > 0, 'No existing lock found'); - assert(locked.end > get_block_timestamp(), 'Cannot add to expired lock'); - - self._deposit_for(caller, value, 0, locked, DEPOSIT_TYPE_INCREASE_AMOUNT); + let myaddr = get_contract_address(); + assert(caller == myaddr, 'can only call from proposal'); + self.curve.write(length, conversion_rate); } - fn increase_unlock_time(ref self: ComponentState, unlock_time: u64) { + fn set_floating_token_address( + ref self: ComponentState, address: ContractAddress + ) { let caller = get_caller_address(); - let locked = self.locked.read(caller); - assert(locked > 0, 'No existing lock found'); - assert(locked.end > get_block_timestamp(), 'Lock expired'); - assert(unlock_time > locked.end, 'Can only increase lock duration'); - assert(unlock_time <= get_block_timestamp() + MAXTIME, 'Voting lock can be 4 years max'); + let myaddr = get_contract_address(); + assert(caller == myaddr, 'can only call from proposal'); + self.floating_token_address.write(address); + } - self._deposit_for(caller, 0, unlock_time, locked, DEPOSIT_TYPE_INCREASE_TIME); + fn get_floating_token_address(self: @ComponentState) -> ContractAddress { + self.floating_token_address.read() } - fn withdraw(ref self: ComponentState) { - let caller = get_caller_address(); - let locked = self.locked.read(caller); - assert(get_block_timestamp() >= locked.end, 'The lock did not expire'); - let value = locked; - - self.locked.write(caller, LockedBalance { amount: 0, end: 0 }); - let user_epoch = self.user_point_epoch.read(caller); - self.user_point_epoch.write(caller, user_epoch + 1); - self.user_point_history.write( - (caller, user_epoch + 1), - Point { bias: 0, slope: 0, ts: get_block_timestamp(), blk: get_block_number() } - ); - self._checkpoint(caller, locked, LockedBalance { amount: 0, end: 0 }); - - assert(value > 0, 'Withdrawing zero amount'); - let token = IERC20Dispatcher { contract_address: self.token.read() }; - token.transfer(caller, value); - - self.emit(Withdraw { provider: caller, value, ts: get_block_timestamp() }); + fn get_stake( + self: @ComponentState, address: ContractAddress, stake_id: u32 + ) -> Stake { + self.stake.read((address, stake_id)) + } + + fn get_total_voting_power( + self: @ComponentState, address: ContractAddress + ) -> u128 { + let mut id = 0; + let mut acc = 0; + let currtime = get_block_timestamp(); + loop { + let res: Stake = self.stake.read((address, id)); + if (res.amount_voting_token == 0) { + break acc; + } + id += 1; + let not_expired: bool = currtime < (res.length + res.start_date); + if (not_expired) { + acc += res.amount_voting_token; + } + } } } @@ -178,109 +288,37 @@ use super::IVotingEscrow; impl InternalImpl< TContractState, +HasComponent > of InternalTrait { - fn _deposit_for( - ref self: ComponentState, - addr: ContractAddress, - value: u128, - unlock_time: u64, - locked_balance: u128, - type_: u8 - ) { - let _type = type_; - let mut locked = locked_balance; - let supply_before = self._supply_at(get_block_timestamp()); - - self.token.read().transfer_from(addr, get_contract_address(), value); - - let mut amount = locked.amount + value; - assert(amount > 0, 'Insufficient deposit'); - - let unlock_time = if unlock_time == 0 { locked.end } else { unlock_time }; - assert(unlock_time > get_block_timestamp(), 'Can only lock until time in the future'); - assert(unlock_time <= get_block_timestamp() + MAXTIME, 'Voting lock can be 4 years max'); - - self.locked.write(addr, LockedBalance { amount, end: unlock_time }); - - // Record global and per-user point - self._checkpoint(addr, locked, LockedBalance { amount, end: unlock_time }); - - if value != 0 { - self.emit(Deposit { - provider: addr, value, locktime: unlock_time, type_: _type, ts: get_block_timestamp() - }); - } + fn get_free_stake_id( + self: @ComponentState, address: ContractAddress + ) -> u32 { + self._get_free_stake_id(address, 0) } - fn _checkpoint( - ref self: ComponentState, - addr: ContractAddress, - old_locked: LockedBalance, - new_locked: LockedBalance - ) { - let mut u_old = old_locked; - let mut u_new = new_locked; - let mut epoch = self.epoch.read(); - - self.point_history.write( - epoch, - Point { - bias: 0, - slope: 0, - ts: get_block_timestamp(), - blk: get_block_number() - } - ); - - if addr != starknet::contract_address_const::<0>() { - let mut user_epoch = self.user_point_epoch.read(addr); - self.user_point_epoch.write(addr, user_epoch + 1); - self.user_point_history.write( - (addr, user_epoch + 1), - Point { - bias: 0, - slope: 0, - ts: get_block_timestamp(), - blk: get_block_number() - } - ); - } - - self.epoch.write(epoch + 1); - } - - fn _balance_of(self: @ComponentState, addr: ContractAddress, t: u64) -> u128 { - let _t = t; - let locked = self.locked.read(addr); - if _t >= locked.end { - 0 + fn _get_free_stake_id( + self: @ComponentState, address: ContractAddress, id: u32 + ) -> u32 { + let res: Stake = self.stake.read((address, id)); + if (res.amount_staked == 0) { + id } else { - locked.amount * (_t.into() - locked.end.into()) / MAXTIME.into(); + self._get_free_stake_id(address, id + 1) } } - fn _balance_of_at( - self: @ComponentState, addr: ContractAddress, block: u64 + fn get_total_staked_accounted( + self: @ComponentState, address: ContractAddress ) -> u128 { - // TODO: Implement historic balance calculation - 0 - } - - fn _supply_at(self: @ComponentState, t: u64) -> u128 { - let _t = t; - let epoch = self.epoch.read(); - let last_point = self.point_history.read(epoch); - - if _t > last_point.ts { - last_point.bias - } else { - let mut point = last_point; - let mut d_block = 0; - let mut d_t = 0; - if _t > point.ts { - d_block = block - point.blk; - d_t = _t - point.ts; + let mut id = 0; + let mut acc = 0; + loop { + let res: Stake = self.stake.read((address, id)); + if (res.amount_voting_token == 0) { + break acc; + } + id += 1; + if (!res.withdrawn) { + acc += res.amount_voting_token; } - point.bias - point.slope * d_t.into(); } } } From 8b2d8357c4caa2f22bc4617d7f189338290d35f6 Mon Sep 17 00:00:00 2001 From: scobi Date: Fri, 19 Jul 2024 13:41:28 +0000 Subject: [PATCH 29/34] remote up to date --- src/contract.cairo | 12 +----------- src/staking.cairo | 2 +- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/contract.cairo b/src/contract.cairo index 3b88aaf9..08fa95fc 100644 --- a/src/contract.cairo +++ b/src/contract.cairo @@ -40,7 +40,6 @@ mod Governance { use konoha::types::VoteStatus; use konoha::upgrades::upgrades as upgrades_component; use konoha::vesting::vesting as vesting_component; - use konoha::streaming::streaming as streaming_component; use starknet::get_contract_address; use starknet::syscalls::deploy_syscall; @@ -54,8 +53,6 @@ mod Governance { component!(path: upgrades_component, storage: upgrades, event: UpgradesEvent); component!(path: discussion_component, storage: discussions, event: DiscussionEvent); component!(path: staking_component, storage: staking, event: StakingEvent); - component!(path: streaming_component, storage: streaming, event: StreamingEvent); - #[abi(embed_v0)] impl Airdrop = airdrop_component::AirdropImpl; @@ -74,9 +71,6 @@ mod Governance { #[abi(embed_v0)] impl Staking = staking_component::StakingImpl; - #[abi(embed_v0)] - impl Streaming = streaming_component::StreamingImpl; - #[storage] struct Storage { proposal_initializer_run: LegacyMap::, @@ -93,9 +87,6 @@ mod Governance { discussions: discussion_component::Storage, #[substorage(v0)] staking: staking_component::Storage, - #[substorage(v0)] - streaming: streaming_component::Storage, - } // PROPOSALS @@ -125,7 +116,6 @@ mod Governance { UpgradesEvent: upgrades_component::Event, DiscussionEvent: discussion_component::Event, StakingEvent: staking_component::Event, - StreamingEvent: streaming_component::Event, } #[constructor] @@ -175,4 +165,4 @@ mod Governance { self.governance_token_address.read() } } -} +} \ No newline at end of file diff --git a/src/staking.cairo b/src/staking.cairo index cfaea115..51284d65 100644 --- a/src/staking.cairo +++ b/src/staking.cairo @@ -322,4 +322,4 @@ mod staking { } } } -} +} \ No newline at end of file From 9201ad514d44bd2f9e8e8b01854346c7ba806812 Mon Sep 17 00:00:00 2001 From: scobi Date: Fri, 19 Jul 2024 14:37:46 +0000 Subject: [PATCH 30/34] fix reentrancy vulnerability --- src/streaming.cairo | 166 ++++++++++----------- tests/test_streaming.cairo | 293 ++++++++++++++++--------------------- 2 files changed, 207 insertions(+), 252 deletions(-) diff --git a/src/streaming.cairo b/src/streaming.cairo index 92cfb680..ed5612b6 100644 --- a/src/streaming.cairo +++ b/src/streaming.cairo @@ -1,7 +1,7 @@ use starknet::ContractAddress; #[starknet::interface] -trait IStreaming{ +trait IStreaming { fn add_new_stream( ref self: TContractState, streamer: ContractAddress, @@ -17,17 +17,14 @@ trait IStreaming{ recipient: ContractAddress, start_time: u64, end_time: u64, - ); + ); - fn cancel_stream( - ref self: TContractState, - recipient: ContractAddress, - start_time: u64, - end_time: u64 - ); + fn cancel_stream( + ref self: TContractState, recipient: ContractAddress, start_time: u64, end_time: u64 + ); fn get_stream_info( - ref self: TContractState, + ref self: TContractState, streamer: ContractAddress, recipient: ContractAddress, start_time: u64, @@ -44,20 +41,22 @@ mod streaming { use starknet::{get_block_timestamp, get_caller_address, get_contract_address}; #[storage] - struct Storage{ - streams: LegacyMap::<(ContractAddress, ContractAddress, u64, u64), (u128, u128)> // (claimed_amount, total_amount) + struct Storage { + streams: LegacyMap::< + (ContractAddress, ContractAddress, u64, u64), (u128, u128) + > // (already_claimed, total_amount) } #[derive(starknet::Event, Drop, Serde)] #[event] - enum Event{ + enum Event { StreamCreated: StreamCreated, StreamClaimed: StreamClaimed, StreamCanceled: StreamCanceled } #[derive(starknet::Event, Drop, Serde)] - struct StreamCreated{ + struct StreamCreated { streamer: ContractAddress, recipient: ContractAddress, start_time: u64, @@ -66,7 +65,7 @@ mod streaming { } #[derive(starknet::Event, Drop, Serde)] - struct StreamClaimed{ + struct StreamClaimed { streamer: ContractAddress, recipient: ContractAddress, start_time: u64, @@ -75,20 +74,19 @@ mod streaming { } #[derive(starknet::Event, Drop, Serde)] - struct StreamCanceled{ + struct StreamCanceled { streamer: ContractAddress, recipient: ContractAddress, start_time: u64, end_time: u64, - reclaimed_amount: u256 + reclaimed_amount: u256 } - + //TODO: #[embeddable_as(StreamingImpl)] impl Streaming< TContractState, +HasComponent > of super::IStreaming> { - fn add_new_stream( ref self: ComponentState, streamer: ContractAddress, @@ -102,16 +100,19 @@ mod streaming { assert(get_caller_address() == get_contract_address(), 'not self-call'); assert(start_time < end_time, 'starts first'); - let mut claimable_amount = 0; - self.streams.write(key, (claimable_amount, total_amount)); - - self.emit(StreamCreated{ - streamer: get_caller_address(), - recipient: recipient, - start_time: start_time, - end_time: end_time, - total_amount: total_amount - }); + let currently_claimable = 0; + self.streams.write(key, (currently_claimable, total_amount)); + + self + .emit( + StreamCreated { + streamer: get_caller_address(), + recipient: recipient, + start_time: start_time, + end_time: end_time, + total_amount: total_amount + } + ); } fn claim_stream( @@ -120,82 +121,76 @@ mod streaming { recipient: ContractAddress, start_time: u64, end_time: u64, - - ){ + ) { let current_time = get_block_timestamp(); - let key = ( - get_caller_address(), - recipient, - start_time, - end_time, - ); + let key = (streamer, recipient, start_time, end_time,); - let (mut claimed_amount, total_amount): (u128, u128) = self.streams.read(key); + let (already_claimed, total_amount): (u128, u128) = self.streams.read(key); assert(current_time > start_time, 'stream has not started'); let elapsed_time = if current_time > end_time { end_time - start_time - } else{ + } else { current_time - start_time }; let stream_duration = end_time - start_time; + let currently_claimable = (total_amount * elapsed_time.into() / stream_duration.into()); + let amount_to_claim = currently_claimable - already_claimed; - let claimable_amount = (total_amount * elapsed_time.into() / stream_duration.into()); - let amount_to_claim = claimable_amount - claimed_amount; + assert(amount_to_claim > 0, 'nothing to claim'); + // Update the storage with the new claimed amount + self.streams.write(key, (currently_claimable, total_amount)); - assert(amount_to_claim > 0, 'nothing to claim'); - claimed_amount += amount_to_claim; - let self_dsp = IGovernanceDispatcher { contract_address: get_contract_address() }; IGovernanceTokenDispatcher { contract_address: self_dsp.get_governance_token_address() } - .mint(recipient, claimed_amount.into()); - - self.streams.write(key, (claimable_amount, total_amount));//claimable shld be (token_to_claim + amount_to_claim)? - - - self.emit(StreamClaimed{ - streamer: get_caller_address(), - recipient: recipient, - start_time: start_time, - end_time: end_time, - total_amount: total_amount - }) - } - - fn cancel_stream( - ref self: ComponentState, - recipient: ContractAddress, - start_time: u64, - end_time: u64 - ){ + .mint(recipient, amount_to_claim.into()); + + self + .emit( + StreamClaimed { + streamer: streamer, + recipient: recipient, + start_time: start_time, + end_time: end_time, + total_amount: total_amount + } + ); + } + + fn cancel_stream( + ref self: ComponentState, + recipient: ContractAddress, + start_time: u64, + end_time: u64 + ) { let key: (ContractAddress, ContractAddress, u64, u64) = ( - get_caller_address(), - recipient, - start_time, - end_time, + get_caller_address(), recipient, start_time, end_time, ); // Read from the streams LegacyMap - let (claimed_amount, total_amount): (u128, u128) = self.streams.read(key); - let unclaimed_amount: u256 = total_amount.into() - claimed_amount.into(); + let (already_claimed, total_amount): (u128, u128) = self.streams.read(key); + let to_distribute: u256 = total_amount.into() - already_claimed.into(); //cancel stream - self.streams.write(key, (0,0)); + self.streams.write(key, (0, 0)); let self_dsp = IGovernanceDispatcher { contract_address: get_contract_address() }; IGovernanceTokenDispatcher { contract_address: self_dsp.get_governance_token_address() } - .mint(get_caller_address(), unclaimed_amount.into()); - - self.emit(StreamCanceled{ - streamer: get_caller_address(), - recipient: recipient, - start_time: start_time, - end_time: end_time, - reclaimed_amount: unclaimed_amount, - }) + .mint(get_caller_address(), to_distribute.into()); + + self + .emit( + StreamCanceled { + streamer: get_caller_address(), + recipient: recipient, + start_time: start_time, + end_time: end_time, + reclaimed_amount: to_distribute, + } + ) } fn get_stream_info( ref self: ComponentState, @@ -205,15 +200,10 @@ mod streaming { end_time: u64, ) -> (u128, u128) { let key: (ContractAddress, ContractAddress, u64, u64) = ( - get_caller_address(), - recipient, - start_time, - end_time, + get_caller_address(), recipient, start_time, end_time, ); - let (mut claimable_amount, mut total_amount) : (u128, u128) = self.streams.read(key); - (claimable_amount, total_amount) + let (currently_claimable, total_amount): (u128, u128) = self.streams.read(key); + (currently_claimable, total_amount) } - - } -} +} diff --git a/tests/test_streaming.cairo b/tests/test_streaming.cairo index 2cb8dff8..5adf5859 100644 --- a/tests/test_streaming.cairo +++ b/tests/test_streaming.cairo @@ -1,188 +1,153 @@ -use array::ArrayTrait; -use core::option::OptionTrait; -use core::result::ResultTrait; -use core::traits::TryInto; use debug::PrintTrait; - -use konoha::contract::Governance; -use konoha::contract::{IGovernanceDispatcher, IGovernanceDispatcherTrait}; -use konoha::traits::{IGovernanceTokenDispatcher, IGovernanceTokenDispatcherTrait}; -use starknet::{get_block_timestamp, get_caller_address, get_contract_address, ContractAddress}; -use konoha::vesting::{IVestingDispatcher, IVestingDispatcherTrait, IVesting}; -use konoha::streaming::{IStreamingDispatcher, IStreamingDispatcherTrait, IStreaming}; - +use konoha::staking::{IStakingDispatcher, IStakingDispatcherTrait}; use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; + use snforge_std::{ - BlockId, declare, ContractClassTrait, ContractClass, start_prank, start_warp, CheatTarget, - prank, CheatSpan + BlockId, declare, ContractClassTrait, ContractClass, CheatTarget, prank, CheatSpan, start_warp, + stop_warp }; +use starknet::{ContractAddress, get_block_timestamp}; +use super::setup::{admin_addr, first_address, second_address, deploy_governance_and_both_tokens}; -use super::setup::{deploy_governance_and_both_tokens}; +const ONE_MONTH: u64 = 2629743; // 30.44 days +const ONE_YEAR: u64 = 31536000; // 365 days -fn start_stream(gov: ContractAddress){ +fn set_staking_curve(gov: ContractAddress) { + // simulate calling this from a proposal prank(CheatTarget::One(gov), gov, CheatSpan::TargetCalls(4)); - let streaming = IStreamingDispatcher { contract_address: gov }; - streaming.add_new_stream(get_caller_address(), 0x2.try_into().unwrap(), 100, 200, 100000); + + let staking = IStakingDispatcher { contract_address: gov }; + let THREE_MONTHS = ONE_MONTH * 3; + let SIX_MONTHS = ONE_MONTH * 6; + staking.set_curve_point(ONE_MONTH, 100); // 1 KONOHA = 1 veKONOHA if staked for 1 month + staking.set_curve_point(THREE_MONTHS, 120); + staking.set_curve_point(SIX_MONTHS, 160); + staking.set_curve_point(ONE_YEAR, 250); } -//passing! -#[test] -fn test_add_new_stream() { - let (gov, _, _) = deploy_governance_and_both_tokens(); - start_stream(gov.contract_address); - let streaming = IStreamingDispatcher { contract_address: gov.contract_address }; - - let streamer = get_caller_address(); - let recipient = 0x2.try_into().unwrap(); - let start_time: u64 = 100; - let end_time: u64 = 200; - let total_amount: u128 = 100000; - - streaming.add_new_stream(streamer, recipient, start_time, end_time, total_amount); - //let key = (get_caller_address(), recipient, end_time, start_time); - - let (claimed_amount, stored_total_amount) = streaming.get_stream_info( - streamer, - recipient, - start_time, - end_time, - ); +fn set_floating_token_address(gov: ContractAddress, floating_token_address: ContractAddress) { + // simulate calling this from a proposal + prank(CheatTarget::One(gov), gov, CheatSpan::TargetCalls(1)); - assert_eq!(streamer, get_caller_address(), "Incorrect streamer addr"); - assert_eq!(recipient, 0x2.try_into().unwrap(), "Incorrect streamer addr"); - assert_eq!(start_time, 100, "Incorrect start time"); - assert_eq!(end_time, 200, "Incorrect end time"); - assert_eq!(claimed_amount, 0, "Incorrect claimed amount after stream creation"); - assert_eq!(stored_total_amount, 100000, "Incorrect total amount stored"); + let staking = IStakingDispatcher { contract_address: gov }; + staking.set_floating_token_address(floating_token_address); } -//passing! -#[test] -#[should_panic(expected: ('starts first',))] -fn test_valid_stream_time(){ - let (gov, _, _) = deploy_governance_and_both_tokens(); - start_stream(gov.contract_address); - let streaming = IStreamingDispatcher { contract_address: gov.contract_address }; - - let streamer = get_caller_address(); - let recipient = 0x2.try_into().unwrap(); - let start_time: u64 = 200; - let end_time: u64 = 100; - let total_amount: u128 = 100000; - - streaming.add_new_stream(streamer, recipient, start_time, end_time, total_amount); -} +fn stake_all(gov: ContractAddress, floating: IERC20Dispatcher, staker: ContractAddress) { + let staking = IStakingDispatcher { contract_address: gov }; -//passing! -#[test] -#[should_panic(expected: ('nothing to claim',))] -fn test_claimed_amount(){ - let (gov, _, _) = deploy_governance_and_both_tokens(); - start_stream(gov.contract_address); - let streaming = IStreamingDispatcher { contract_address: gov.contract_address }; - - let streamer = get_caller_address(); - let recipient = 0x2.try_into().unwrap(); - let start_time: u64 = 100; - let end_time: u64 = 200; - let total_amount: u128 = 0; - streaming.add_new_stream(streamer, recipient, start_time, end_time, total_amount); - - start_warp(CheatTarget::One(gov.contract_address), 150); - //shouldn't have anything to claim - streaming.claim_stream(streamer, recipient, start_time, end_time); + let balance_of_staker = floating.balance_of(staker).low; + prank(CheatTarget::One(floating.contract_address), staker, CheatSpan::TargetCalls(1)); + floating.approve(gov, balance_of_staker.into()); + prank(CheatTarget::One(gov), staker, CheatSpan::TargetCalls(1)); + staking.stake(ONE_MONTH, balance_of_staker); } -//passing! -#[test] -#[should_panic(expected: ('stream has not started',))] -fn test_stream_started(){ - let (gov, _, _) = deploy_governance_and_both_tokens(); - start_stream(gov.contract_address); - let streaming = IStreamingDispatcher { contract_address: gov.contract_address }; - let streamer = get_caller_address(); - let recipient = 0x2.try_into().unwrap(); - let start_time: u64 = 100; - let end_time: u64 = 200; - let total_amount: u128 = 100000; - streaming.add_new_stream(streamer, recipient, start_time, end_time, total_amount); - start_warp(CheatTarget::One(gov.contract_address), 50);// before of stream - - streaming.claim_stream(streamer, recipient, start_time, end_time); +fn stake_half(gov: ContractAddress, floating: IERC20Dispatcher, staker: ContractAddress) { + let staking = IStakingDispatcher { contract_address: gov }; + + let balance_of_staker = floating.balance_of(staker).low; + prank(CheatTarget::One(floating.contract_address), staker, CheatSpan::TargetCalls(1)); + floating.approve(gov, balance_of_staker.into()); + prank(CheatTarget::One(gov), staker, CheatSpan::TargetCalls(1)); + staking.stake(ONE_MONTH, balance_of_staker / 2); } + #[test] -fn test_claim_stream() { - let (gov, _, _) = deploy_governance_and_both_tokens(); - start_stream(gov.contract_address); - let streaming = IStreamingDispatcher { contract_address: gov.contract_address }; - - let streamer = get_caller_address(); - let recipient = 0x2.try_into().unwrap(); - let start_time: u64 = 100; - let end_time: u64 = 200; - let total_amount: u128 = 100000; - - streaming.add_new_stream(streamer, recipient, start_time, end_time, total_amount); - let (claimable_amount, total_amount) = streaming.get_stream_info( - streamer, - recipient, - start_time, - end_time, +fn test_basic_stake_unstake() { + let (gov, _voting, floating) = deploy_governance_and_both_tokens(); + set_staking_curve(gov.contract_address); + let staking = IStakingDispatcher { contract_address: gov.contract_address }; + let admin: ContractAddress = admin_addr.try_into().unwrap(); + assert( + staking.get_floating_token_address() == floating.contract_address, 'floating token addr !=' ); - start_warp(CheatTarget::One(gov.contract_address), 150); - - streaming.claim_stream(streamer, recipient, start_time, end_time); - - let expected_claimed_amount = (100000 * 50/ 100); //should be 50% since middle of stream - assert_eq!(total_amount, 100000, "Incorrect total amount after claiming the stream"); - assert_eq!(claimable_amount, 0, "Incorrect claimed amount after claiming the stream"); - - let self_dsp = IGovernanceDispatcher { contract_address: gov.contract_address }; - let token_address = self_dsp.get_governance_token_address(); - let erc20 = IERC20Dispatcher { contract_address: token_address }; - - let balance = erc20.balance_of(recipient); - - assert_eq!(balance, expected_claimed_amount, "Balance should match the expected claimed amount"); + let balance_of_staker: u128 = 10000000; + prank(CheatTarget::One(floating.contract_address), admin, CheatSpan::TargetCalls(1)); + floating.approve(gov.contract_address, balance_of_staker.into()); + prank(CheatTarget::One(gov.contract_address), admin, CheatSpan::TargetCalls(3)); + let stake_id = staking.stake(ONE_MONTH, balance_of_staker); + + let current_timestamp = get_block_timestamp(); + start_warp(CheatTarget::One(gov.contract_address), current_timestamp + ONE_MONTH + 1); + staking.unstake(stake_id); + stop_warp(CheatTarget::One(gov.contract_address)); } #[test] -fn test_cancel_stream() { - let (gov, _, _) = deploy_governance_and_both_tokens(); - start_stream(gov.contract_address); - let streaming = IStreamingDispatcher { contract_address: gov.contract_address }; - - let streamer = get_caller_address(); - let recipient = 0x2.try_into().unwrap(); - let start_time: u64 = 100; - let end_time: u64 = 200; - let total_amount: u128 = 100000; - - streaming.add_new_stream(streamer, recipient, start_time, end_time, total_amount); - - start_warp(CheatTarget::One(gov.contract_address), 150); - - //test cancel_stream - streaming.cancel_stream(recipient, start_time, end_time); - - let (claimed_amount, stored_total_amount) = streaming.get_stream_info( - streamer, - recipient, - start_time, - end_time); - - assert_eq!(claimed_amount, 0, "Claimed amount should be 0 after canceling the stream"); - assert_eq!(stored_total_amount, 0, "Total amount should be 0 after canceling the stream"); - - let unclaimed_amount: u256 = total_amount.into() - claimed_amount.into(); //100000 - let self_dsp = IGovernanceDispatcher { contract_address: gov.contract_address }; - let token_address = self_dsp.get_governance_token_address(); - let erc20 = IERC20Dispatcher { contract_address: token_address }; - - // Check the balance of the streamer (caller address) with ERC_20, I couldnt use balance_of - let balance = erc20.balance_of(get_caller_address()); +fn test_multiple_overlapping_stake_unstake() { + let (gov, voting, floating) = deploy_governance_and_both_tokens(); + set_staking_curve(gov.contract_address); + let staking = IStakingDispatcher { contract_address: gov.contract_address }; + let admin: ContractAddress = admin_addr.try_into().unwrap(); + let time_zero = get_block_timestamp(); + let initial_floating_balance = floating.balance_of(admin); + + prank(CheatTarget::One(floating.contract_address), admin, CheatSpan::TargetCalls(1)); + floating.approve(gov.contract_address, 420); + prank(CheatTarget::One(gov.contract_address), admin, CheatSpan::TargetCalls(1)); + let stake_id_month_one = staking.stake(ONE_MONTH, 420); + assert(voting.balance_of(admin) == 420, 'wrong bal stakeid monthone'); + assert(staking.get_total_voting_power(admin) == 420, 'voting power bad'); + + prank(CheatTarget::One(floating.contract_address), admin, CheatSpan::TargetCalls(1)); + floating.approve(gov.contract_address, 937); // not-nice prime number to check rounding + prank(CheatTarget::One(gov.contract_address), admin, CheatSpan::TargetCalls(1)); + let stake_id_year = staking.stake(ONE_YEAR, 937); + assert(voting.balance_of(admin) == 420 + 2342, 'wrong bal yearone+monthone'); + assert(staking.get_total_voting_power(admin) == 420 + 2342, 'voting power baad'); + + start_warp(CheatTarget::One(gov.contract_address), time_zero + ONE_MONTH + 1); + assert(staking.get_total_voting_power(admin) == 2342, 'voting power baaad'); + prank(CheatTarget::One(gov.contract_address), admin, CheatSpan::TargetCalls(1)); + staking.unstake(stake_id_month_one); + assert(voting.balance_of(admin) == 2342, 'wrong bal yearone+monthone'); + assert(staking.get_total_voting_power(admin) == 2342, 'voting power baaaad'); + + prank(CheatTarget::One(floating.contract_address), admin, CheatSpan::TargetCalls(1)); + floating.approve(gov.contract_address, 101); + prank(CheatTarget::One(gov.contract_address), admin, CheatSpan::TargetCalls(1)); + let stake_id_month_one_three_months = staking.stake(ONE_MONTH * 3, 101); + assert(voting.balance_of(admin) == 2342 + 121, 'wrong bal yearone+monthtwo'); + stop_warp(CheatTarget::One(gov.contract_address)); + + start_warp(CheatTarget::One(gov.contract_address), time_zero + ONE_YEAR * 4 + 1); + assert(staking.get_total_voting_power(admin) == 0, 'voting power baaaaad'); + prank(CheatTarget::One(gov.contract_address), admin, CheatSpan::TargetCalls(1)); + staking.unstake(stake_id_year); + assert!(voting.balance_of(admin) == 121, "wrong bal after unstaking yearone but not monthtwo"); + assert( + floating.balance_of(admin) == (initial_floating_balance - 101), 'floating tokens gon' + ); // 101 still in stake_id_month_one_three_months + prank(CheatTarget::One(gov.contract_address), admin, CheatSpan::TargetCalls(1)); + staking.unstake(stake_id_month_one_three_months); + assert(floating.balance_of(admin) == initial_floating_balance, 'floating tokens gone'); + assert(voting.balance_of(admin) == 0, 'nonzero bal after all'); + assert(staking.get_total_voting_power(admin) == 0, 'admin voting power nonzero'); +} - assert_eq!(unclaimed_amount.into(), 100000, "Unclaimed amount should be reclaimed correctly"); - assert_eq!(balance, 0, "balance"); +#[test] +#[should_panic(expected: ('unlock time not yet reached',))] +fn test_unstake_before_unlock(mut amount_to_stake: u16, duration_seed: u8) { + let (gov, _voting, floating) = deploy_governance_and_both_tokens(); + set_staking_curve(gov.contract_address); + let staking = IStakingDispatcher { contract_address: gov.contract_address }; + let admin: ContractAddress = admin_addr.try_into().unwrap(); + + let duration_mod = duration_seed % 2; + let duration = if (duration_mod == 0) { + ONE_MONTH + } else { + ONE_YEAR + }; + if (amount_to_stake == 0) { + amount_to_stake += 1; + } + prank(CheatTarget::One(floating.contract_address), admin, CheatSpan::TargetCalls(1)); + floating.approve(gov.contract_address, amount_to_stake.into()); + prank(CheatTarget::One(gov.contract_address), admin, CheatSpan::TargetCalls(3)); + let stake_id = staking.stake(duration, amount_to_stake.into()); + + staking.unstake(stake_id); } From 71553f8e822eea05d85bf307ef175ca468dc1bd4 Mon Sep 17 00:00:00 2001 From: scobi Date: Sat, 20 Jul 2024 08:42:01 +0000 Subject: [PATCH 31/34] fixed reentrancy vulnerability (#112) --- src/contract.cairo | 10 ++ src/streaming.cairo | 8 +- tests/test_streaming.cairo | 334 +++++++++++++++++++++++-------------- 3 files changed, 221 insertions(+), 131 deletions(-) diff --git a/src/contract.cairo b/src/contract.cairo index 08fa95fc..031aabb7 100644 --- a/src/contract.cairo +++ b/src/contract.cairo @@ -40,6 +40,7 @@ mod Governance { use konoha::types::VoteStatus; use konoha::upgrades::upgrades as upgrades_component; use konoha::vesting::vesting as vesting_component; + use konoha::streaming::streaming as streaming_component; use starknet::get_contract_address; use starknet::syscalls::deploy_syscall; @@ -53,6 +54,8 @@ mod Governance { component!(path: upgrades_component, storage: upgrades, event: UpgradesEvent); component!(path: discussion_component, storage: discussions, event: DiscussionEvent); component!(path: staking_component, storage: staking, event: StakingEvent); + component!(path: streaming_component, storage: streaming, event: StreamingEvent); + #[abi(embed_v0)] impl Airdrop = airdrop_component::AirdropImpl; @@ -71,6 +74,9 @@ mod Governance { #[abi(embed_v0)] impl Staking = staking_component::StakingImpl; + #[abi(embed_v0)] + impl Streaming = streaming_component::StreamingImpl; + #[storage] struct Storage { proposal_initializer_run: LegacyMap::, @@ -87,6 +93,9 @@ mod Governance { discussions: discussion_component::Storage, #[substorage(v0)] staking: staking_component::Storage, + #[substorage(v0)] + streaming: streaming_component::Storage, + } // PROPOSALS @@ -116,6 +125,7 @@ mod Governance { UpgradesEvent: upgrades_component::Event, DiscussionEvent: discussion_component::Event, StakingEvent: staking_component::Event, + StreamingEvent: streaming_component::Event, } #[constructor] diff --git a/src/streaming.cairo b/src/streaming.cairo index ed5612b6..9e1b512b 100644 --- a/src/streaming.cairo +++ b/src/streaming.cairo @@ -82,7 +82,7 @@ mod streaming { reclaimed_amount: u256 } - //TODO: + //TODO #[embeddable_as(StreamingImpl)] impl Streaming< TContractState, +HasComponent @@ -100,7 +100,7 @@ mod streaming { assert(get_caller_address() == get_contract_address(), 'not self-call'); assert(start_time < end_time, 'starts first'); - let currently_claimable = 0; + let mut currently_claimable = 0; self.streams.write(key, (currently_claimable, total_amount)); self @@ -123,8 +123,8 @@ mod streaming { end_time: u64, ) { let current_time = get_block_timestamp(); - - let key = (streamer, recipient, start_time, end_time,); + //get_caller should be streamer I think + let key = (get_caller_address(), recipient, start_time, end_time,); let (already_claimed, total_amount): (u128, u128) = self.streams.read(key); assert(current_time > start_time, 'stream has not started'); diff --git a/tests/test_streaming.cairo b/tests/test_streaming.cairo index 5adf5859..8bd5a542 100644 --- a/tests/test_streaming.cairo +++ b/tests/test_streaming.cairo @@ -1,153 +1,233 @@ +use array::ArrayTrait; +use core::option::OptionTrait; +use core::result::ResultTrait; +use core::traits::TryInto; use debug::PrintTrait; -use konoha::staking::{IStakingDispatcher, IStakingDispatcherTrait}; -use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +use konoha::contract::Governance; +use konoha::contract::{IGovernanceDispatcher, IGovernanceDispatcherTrait}; +use konoha::traits::{IGovernanceTokenDispatcher, IGovernanceTokenDispatcherTrait}; +use starknet::{get_block_timestamp, get_caller_address, get_contract_address, ContractAddress}; +use konoha::vesting::{IVestingDispatcher, IVestingDispatcherTrait, IVesting}; +use konoha::streaming::{IStreamingDispatcher, IStreamingDispatcherTrait, IStreaming}; + +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; use snforge_std::{ - BlockId, declare, ContractClassTrait, ContractClass, CheatTarget, prank, CheatSpan, start_warp, - stop_warp + BlockId, declare, ContractClassTrait, ContractClass, start_prank, start_warp, CheatTarget, + prank, CheatSpan }; -use starknet::{ContractAddress, get_block_timestamp}; -use super::setup::{admin_addr, first_address, second_address, deploy_governance_and_both_tokens}; -const ONE_MONTH: u64 = 2629743; // 30.44 days -const ONE_YEAR: u64 = 31536000; // 365 days +use super::setup::{deploy_governance_and_both_tokens}; -fn set_staking_curve(gov: ContractAddress) { - // simulate calling this from a proposal +fn start_stream(gov: ContractAddress){ prank(CheatTarget::One(gov), gov, CheatSpan::TargetCalls(4)); - - let staking = IStakingDispatcher { contract_address: gov }; - let THREE_MONTHS = ONE_MONTH * 3; - let SIX_MONTHS = ONE_MONTH * 6; - staking.set_curve_point(ONE_MONTH, 100); // 1 KONOHA = 1 veKONOHA if staked for 1 month - staking.set_curve_point(THREE_MONTHS, 120); - staking.set_curve_point(SIX_MONTHS, 160); - staking.set_curve_point(ONE_YEAR, 250); + let streaming = IStreamingDispatcher { contract_address: gov }; + streaming.add_new_stream(get_caller_address(), 0x2.try_into().unwrap(), 100, 200, 100000); } -fn set_floating_token_address(gov: ContractAddress, floating_token_address: ContractAddress) { - // simulate calling this from a proposal - prank(CheatTarget::One(gov), gov, CheatSpan::TargetCalls(1)); +//passing! +#[test] +fn test_add_new_stream() { + let (gov, _, _) = deploy_governance_and_both_tokens(); + start_stream(gov.contract_address); + let streaming = IStreamingDispatcher { contract_address: gov.contract_address }; + + let streamer = get_caller_address(); + let recipient = 0x2.try_into().unwrap(); + let start_time: u64 = 100; + let end_time: u64 = 200; + let total_amount: u128 = 100000; + + streaming.add_new_stream(streamer, recipient, start_time, end_time, total_amount); + //let key = (get_caller_address(), recipient, end_time, start_time); + + let (claimed_amount, stored_total_amount) = streaming.get_stream_info( + streamer, + recipient, + start_time, + end_time, + ); - let staking = IStakingDispatcher { contract_address: gov }; - staking.set_floating_token_address(floating_token_address); + assert_eq!(streamer, get_caller_address(), "Incorrect streamer addr"); + assert_eq!(recipient, 0x2.try_into().unwrap(), "Incorrect streamer addr"); + assert_eq!(start_time, 100, "Incorrect start time"); + assert_eq!(end_time, 200, "Incorrect end time"); + assert_eq!(claimed_amount, 0, "Incorrect claimed amount after stream creation"); + assert_eq!(stored_total_amount, 100000, "Incorrect total amount stored"); } -fn stake_all(gov: ContractAddress, floating: IERC20Dispatcher, staker: ContractAddress) { - let staking = IStakingDispatcher { contract_address: gov }; - - let balance_of_staker = floating.balance_of(staker).low; - prank(CheatTarget::One(floating.contract_address), staker, CheatSpan::TargetCalls(1)); - floating.approve(gov, balance_of_staker.into()); - prank(CheatTarget::One(gov), staker, CheatSpan::TargetCalls(1)); - staking.stake(ONE_MONTH, balance_of_staker); +//passing! +#[test] +#[should_panic(expected: ('starts first',))] +fn test_valid_stream_time(){ + let (gov, _, _) = deploy_governance_and_both_tokens(); + start_stream(gov.contract_address); + let streaming = IStreamingDispatcher { contract_address: gov.contract_address }; + + let streamer = get_caller_address(); + let recipient = 0x2.try_into().unwrap(); + let start_time: u64 = 200; + let end_time: u64 = 100; + let total_amount: u128 = 100000; + + streaming.add_new_stream(streamer, recipient, start_time, end_time, total_amount); } -fn stake_half(gov: ContractAddress, floating: IERC20Dispatcher, staker: ContractAddress) { - let staking = IStakingDispatcher { contract_address: gov }; - - let balance_of_staker = floating.balance_of(staker).low; - prank(CheatTarget::One(floating.contract_address), staker, CheatSpan::TargetCalls(1)); - floating.approve(gov, balance_of_staker.into()); - prank(CheatTarget::One(gov), staker, CheatSpan::TargetCalls(1)); - staking.stake(ONE_MONTH, balance_of_staker / 2); +//passing! +#[test] +#[should_panic(expected: ('nothing to claim',))] +fn test_claimed_amount(){ + let (gov, _, _) = deploy_governance_and_both_tokens(); + start_stream(gov.contract_address); + let streaming = IStreamingDispatcher { contract_address: gov.contract_address }; + + let streamer = get_caller_address(); + let recipient = 0x2.try_into().unwrap(); + let start_time: u64 = 100; + let end_time: u64 = 200; + let total_amount: u128 = 0; + streaming.add_new_stream(streamer, recipient, start_time, end_time, total_amount); + + start_warp(CheatTarget::One(gov.contract_address), 150); + //shouldn't have anything to claim + streaming.claim_stream(streamer, recipient, start_time, end_time); } +//passing! +#[test] +#[should_panic(expected: ('stream has not started',))] +fn test_stream_started(){ + let (gov, _, _) = deploy_governance_and_both_tokens(); + start_stream(gov.contract_address); + let streaming = IStreamingDispatcher { contract_address: gov.contract_address }; + let streamer = get_caller_address(); + let recipient = 0x2.try_into().unwrap(); + let start_time: u64 = 100; + let end_time: u64 = 200; + let total_amount: u128 = 100000; + streaming.add_new_stream(streamer, recipient, start_time, end_time, total_amount); + start_warp(CheatTarget::One(gov.contract_address), 50);// before of stream + + streaming.claim_stream(streamer, recipient, start_time, end_time); +} #[test] -fn test_basic_stake_unstake() { - let (gov, _voting, floating) = deploy_governance_and_both_tokens(); - set_staking_curve(gov.contract_address); - let staking = IStakingDispatcher { contract_address: gov.contract_address }; - let admin: ContractAddress = admin_addr.try_into().unwrap(); - assert( - staking.get_floating_token_address() == floating.contract_address, 'floating token addr !=' +fn test_claim_stream() { + let (gov, _, _) = deploy_governance_and_both_tokens(); + start_stream(gov.contract_address); + let streaming = IStreamingDispatcher { contract_address: gov.contract_address }; + + let streamer = get_caller_address(); + let recipient = 0x2.try_into().unwrap(); + let start_time: u64 = 100; + let end_time: u64 = 200; + let total_amount: u128 = 100000; + + streaming.add_new_stream(streamer, recipient, start_time, end_time, total_amount); + let (claimable_amount, total_amount) = streaming.get_stream_info( + streamer, + recipient, + start_time, + end_time, ); - let balance_of_staker: u128 = 10000000; - prank(CheatTarget::One(floating.contract_address), admin, CheatSpan::TargetCalls(1)); - floating.approve(gov.contract_address, balance_of_staker.into()); - prank(CheatTarget::One(gov.contract_address), admin, CheatSpan::TargetCalls(3)); - let stake_id = staking.stake(ONE_MONTH, balance_of_staker); - - let current_timestamp = get_block_timestamp(); - start_warp(CheatTarget::One(gov.contract_address), current_timestamp + ONE_MONTH + 1); - staking.unstake(stake_id); - stop_warp(CheatTarget::One(gov.contract_address)); + start_warp(CheatTarget::One(gov.contract_address), 150); + + streaming.claim_stream(streamer, recipient, start_time, end_time); + + let expected_claimed_amount = (100000 * 50/ 100); //should be 50% since middle of stream + assert_eq!(total_amount, 100000, "Incorrect total amount after claiming the stream"); + assert_eq!(claimable_amount, 0, "Incorrect claimed amount after claiming the stream"); + + let self_dsp = IGovernanceDispatcher { contract_address: gov.contract_address }; + let token_address = self_dsp.get_governance_token_address(); + let erc20 = IERC20Dispatcher { contract_address: token_address }; + + let balance = erc20.balance_of(recipient); + + assert_eq!(balance, expected_claimed_amount, "Balance should match the expected claimed amount"); } #[test] -fn test_multiple_overlapping_stake_unstake() { - let (gov, voting, floating) = deploy_governance_and_both_tokens(); - set_staking_curve(gov.contract_address); - let staking = IStakingDispatcher { contract_address: gov.contract_address }; - let admin: ContractAddress = admin_addr.try_into().unwrap(); - let time_zero = get_block_timestamp(); - let initial_floating_balance = floating.balance_of(admin); - - prank(CheatTarget::One(floating.contract_address), admin, CheatSpan::TargetCalls(1)); - floating.approve(gov.contract_address, 420); - prank(CheatTarget::One(gov.contract_address), admin, CheatSpan::TargetCalls(1)); - let stake_id_month_one = staking.stake(ONE_MONTH, 420); - assert(voting.balance_of(admin) == 420, 'wrong bal stakeid monthone'); - assert(staking.get_total_voting_power(admin) == 420, 'voting power bad'); - - prank(CheatTarget::One(floating.contract_address), admin, CheatSpan::TargetCalls(1)); - floating.approve(gov.contract_address, 937); // not-nice prime number to check rounding - prank(CheatTarget::One(gov.contract_address), admin, CheatSpan::TargetCalls(1)); - let stake_id_year = staking.stake(ONE_YEAR, 937); - assert(voting.balance_of(admin) == 420 + 2342, 'wrong bal yearone+monthone'); - assert(staking.get_total_voting_power(admin) == 420 + 2342, 'voting power baad'); - - start_warp(CheatTarget::One(gov.contract_address), time_zero + ONE_MONTH + 1); - assert(staking.get_total_voting_power(admin) == 2342, 'voting power baaad'); - prank(CheatTarget::One(gov.contract_address), admin, CheatSpan::TargetCalls(1)); - staking.unstake(stake_id_month_one); - assert(voting.balance_of(admin) == 2342, 'wrong bal yearone+monthone'); - assert(staking.get_total_voting_power(admin) == 2342, 'voting power baaaad'); - - prank(CheatTarget::One(floating.contract_address), admin, CheatSpan::TargetCalls(1)); - floating.approve(gov.contract_address, 101); - prank(CheatTarget::One(gov.contract_address), admin, CheatSpan::TargetCalls(1)); - let stake_id_month_one_three_months = staking.stake(ONE_MONTH * 3, 101); - assert(voting.balance_of(admin) == 2342 + 121, 'wrong bal yearone+monthtwo'); - stop_warp(CheatTarget::One(gov.contract_address)); - - start_warp(CheatTarget::One(gov.contract_address), time_zero + ONE_YEAR * 4 + 1); - assert(staking.get_total_voting_power(admin) == 0, 'voting power baaaaad'); - prank(CheatTarget::One(gov.contract_address), admin, CheatSpan::TargetCalls(1)); - staking.unstake(stake_id_year); - assert!(voting.balance_of(admin) == 121, "wrong bal after unstaking yearone but not monthtwo"); - assert( - floating.balance_of(admin) == (initial_floating_balance - 101), 'floating tokens gon' - ); // 101 still in stake_id_month_one_three_months - prank(CheatTarget::One(gov.contract_address), admin, CheatSpan::TargetCalls(1)); - staking.unstake(stake_id_month_one_three_months); - assert(floating.balance_of(admin) == initial_floating_balance, 'floating tokens gone'); - assert(voting.balance_of(admin) == 0, 'nonzero bal after all'); - assert(staking.get_total_voting_power(admin) == 0, 'admin voting power nonzero'); +fn test_cancel_stream() { + let (gov, _, _) = deploy_governance_and_both_tokens(); + start_stream(gov.contract_address); + let streaming = IStreamingDispatcher { contract_address: gov.contract_address }; + + let streamer = get_caller_address(); + let recipient = 0x2.try_into().unwrap(); + let start_time: u64 = 100; + let end_time: u64 = 200; + let total_amount: u128 = 100000; + + streaming.add_new_stream(streamer, recipient, start_time, end_time, total_amount); + + start_warp(CheatTarget::One(gov.contract_address), 150); + + //test cancel_stream + streaming.cancel_stream(recipient, start_time, end_time); + + let (claimed_amount, stored_total_amount) = streaming.get_stream_info( + streamer, + recipient, + start_time, + end_time); + + assert_eq!(claimed_amount, 0, "Claimed amount should be 0 after canceling the stream"); + assert_eq!(stored_total_amount, 0, "Total amount should be 0 after canceling the stream"); + + let unclaimed_amount: u256 = total_amount.into() - claimed_amount.into(); //100000 + let self_dsp = IGovernanceDispatcher { contract_address: gov.contract_address }; + let token_address = self_dsp.get_governance_token_address(); + let erc20 = IERC20Dispatcher { contract_address: token_address }; + + // Check the balance of the streamer (caller address) with ERC_20, I couldnt use balance_of + let balance = erc20.balance_of(get_caller_address()); + + assert_eq!(unclaimed_amount.into(), 100000, "Unclaimed amount should be reclaimed correctly"); + assert_eq!(balance, 0, "balance"); } #[test] -#[should_panic(expected: ('unlock time not yet reached',))] -fn test_unstake_before_unlock(mut amount_to_stake: u16, duration_seed: u8) { - let (gov, _voting, floating) = deploy_governance_and_both_tokens(); - set_staking_curve(gov.contract_address); - let staking = IStakingDispatcher { contract_address: gov.contract_address }; - let admin: ContractAddress = admin_addr.try_into().unwrap(); - - let duration_mod = duration_seed % 2; - let duration = if (duration_mod == 0) { - ONE_MONTH - } else { - ONE_YEAR - }; - if (amount_to_stake == 0) { - amount_to_stake += 1; - } - prank(CheatTarget::One(floating.contract_address), admin, CheatSpan::TargetCalls(1)); - floating.approve(gov.contract_address, amount_to_stake.into()); - prank(CheatTarget::One(gov.contract_address), admin, CheatSpan::TargetCalls(3)); - let stake_id = staking.stake(duration, amount_to_stake.into()); - - staking.unstake(stake_id); +//#[should_panic(expected: "nothing to claim")] +fn test_add_new_stream_failure() { + let (gov, _, _) = deploy_governance_and_both_tokens(); + start_stream(gov.contract_address); + let streaming = IStreamingDispatcher { contract_address: gov.contract_address }; + + let streamer = 0x1.try_into().unwrap(); // This will be an address other than the contract's address + let recipient = 0x2.try_into().unwrap(); + let start_time: u64 = 100; + let end_time: u64 = 200; + let total_amount: u128 = 100000; + + // Add a stream with `streamer` as `gov` address + streaming.add_new_stream(streamer, recipient, start_time, end_time, total_amount); + + let (claimable_amount, stored_total_amount) = streaming.get_stream_info( + streamer, + recipient, + start_time, + end_time, + ); + + start_warp(CheatTarget::One(gov.contract_address), 150); // Advance time by 150 units + + streaming.claim_stream( + streamer, // Using the wrong `streamer` address + recipient, + start_time, + end_time, + ); + + assert_eq!(stored_total_amount, 100000, "Incorrect total amount after attempting to claim the stream"); + assert_eq!(claimable_amount, 0, "Incorrect claimed amount after attempting to claim the stream"); + + let self_dsp = IGovernanceDispatcher { contract_address: gov.contract_address }; + let token_address = self_dsp.get_governance_token_address(); + let erc20 = IERC20Dispatcher { contract_address: token_address }; + + let balance = erc20.balance_of(recipient); + + let expected_claimed_amount = (100000 * 50 / 100); // should be 50% since middle of stream + assert_eq!(balance, expected_claimed_amount, "Balance should match the expected claimed amount"); } From 7487ec875d448bfef1971edb1f02a8cbdf6ade60 Mon Sep 17 00:00:00 2001 From: scobi Date: Sat, 20 Jul 2024 10:59:57 +0000 Subject: [PATCH 32/34] add streaming to contract (#112) --- src/contract.cairo | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/contract.cairo b/src/contract.cairo index 031aabb7..7ff078ab 100644 --- a/src/contract.cairo +++ b/src/contract.cairo @@ -34,13 +34,13 @@ mod Governance { use konoha::proposals::proposals as proposals_component; use konoha::staking::staking as staking_component; use konoha::staking::{IStakingDispatcher, IStakingDispatcherTrait}; + use konoha::streaming::streaming as streaming_component; use konoha::types::BlockNumber; use konoha::types::ContractType; use konoha::types::PropDetails; use konoha::types::VoteStatus; use konoha::upgrades::upgrades as upgrades_component; use konoha::vesting::vesting as vesting_component; - use konoha::streaming::streaming as streaming_component; use starknet::get_contract_address; use starknet::syscalls::deploy_syscall; @@ -95,7 +95,6 @@ mod Governance { staking: staking_component::Storage, #[substorage(v0)] streaming: streaming_component::Storage, - } // PROPOSALS @@ -175,4 +174,4 @@ mod Governance { self.governance_token_address.read() } } -} \ No newline at end of file +} From cd2a61288bd5092dc383a0ae9741ffa11f30bf4d Mon Sep 17 00:00:00 2001 From: scobi Date: Sat, 20 Jul 2024 11:00:47 +0000 Subject: [PATCH 33/34] scarb fmt --- src/staking.cairo | 2 +- tests/vesting.cairo | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/staking.cairo b/src/staking.cairo index 51284d65..cfaea115 100644 --- a/src/staking.cairo +++ b/src/staking.cairo @@ -322,4 +322,4 @@ mod staking { } } } -} \ No newline at end of file +} diff --git a/tests/vesting.cairo b/tests/vesting.cairo index a7733cc8..b17bfc1b 100644 --- a/tests/vesting.cairo +++ b/tests/vesting.cairo @@ -21,7 +21,7 @@ fn test_setup(gov: ContractAddress) { } #[test] -#[should_panic(expected : ('not self-call',))] +#[should_panic(expected: ('not self-call',))] fn test_unauthorized_add_vesting_schedule() { let (gov, _, _) = deploy_governance_and_both_tokens(); test_setup(gov.contract_address); From 041362407de2de4131d8685183782b623cd585a2 Mon Sep 17 00:00:00 2001 From: scobi Date: Sat, 20 Jul 2024 11:01:38 +0000 Subject: [PATCH 34/34] remove streamer storage (#112) --- src/streaming.cairo | 86 ++++++----------------- tests/test_streaming.cairo | 140 +++++++++++-------------------------- 2 files changed, 62 insertions(+), 164 deletions(-) diff --git a/src/streaming.cairo b/src/streaming.cairo index 9e1b512b..a22a965f 100644 --- a/src/streaming.cairo +++ b/src/streaming.cairo @@ -4,31 +4,22 @@ use starknet::ContractAddress; trait IStreaming { fn add_new_stream( ref self: TContractState, - streamer: ContractAddress, recipient: ContractAddress, start_time: u64, end_time: u64, - total_amount: u128 + total_amount: u128, ); fn claim_stream( - ref self: TContractState, - streamer: ContractAddress, - recipient: ContractAddress, - start_time: u64, - end_time: u64, + ref self: TContractState, recipient: ContractAddress, start_time: u64, end_time: u64, ); fn cancel_stream( - ref self: TContractState, recipient: ContractAddress, start_time: u64, end_time: u64 + ref self: TContractState, recipient: ContractAddress, start_time: u64, end_time: u64, ); fn get_stream_info( - ref self: TContractState, - streamer: ContractAddress, - recipient: ContractAddress, - start_time: u64, - end_time: u64, + ref self: TContractState, recipient: ContractAddress, start_time: u64, end_time: u64, ) -> (u128, u128); } @@ -43,7 +34,7 @@ mod streaming { #[storage] struct Storage { streams: LegacyMap::< - (ContractAddress, ContractAddress, u64, u64), (u128, u128) + (ContractAddress, u64, u64), (u128, u128) > // (already_claimed, total_amount) } @@ -57,75 +48,59 @@ mod streaming { #[derive(starknet::Event, Drop, Serde)] struct StreamCreated { - streamer: ContractAddress, recipient: ContractAddress, start_time: u64, end_time: u64, - total_amount: u128 + total_amount: u128, } #[derive(starknet::Event, Drop, Serde)] struct StreamClaimed { - streamer: ContractAddress, recipient: ContractAddress, start_time: u64, end_time: u64, - total_amount: u128 + total_amount: u128, } #[derive(starknet::Event, Drop, Serde)] struct StreamCanceled { - streamer: ContractAddress, recipient: ContractAddress, start_time: u64, end_time: u64, - reclaimed_amount: u256 + reclaimed_amount: u256, } - //TODO #[embeddable_as(StreamingImpl)] impl Streaming< TContractState, +HasComponent > of super::IStreaming> { fn add_new_stream( ref self: ComponentState, - streamer: ContractAddress, recipient: ContractAddress, start_time: u64, end_time: u64, - total_amount: u128 + total_amount: u128, ) { - let key = (get_caller_address(), recipient, start_time, end_time); + let key = (recipient, start_time, end_time); assert(get_caller_address() == get_contract_address(), 'not self-call'); assert(start_time < end_time, 'starts first'); - let mut currently_claimable = 0; - self.streams.write(key, (currently_claimable, total_amount)); + let mut claimable_amount = 0; + self.streams.write(key, (claimable_amount, total_amount)); - self - .emit( - StreamCreated { - streamer: get_caller_address(), - recipient: recipient, - start_time: start_time, - end_time: end_time, - total_amount: total_amount - } - ); + self.emit(StreamCreated { recipient, start_time, end_time, total_amount, }); } fn claim_stream( ref self: ComponentState, - streamer: ContractAddress, recipient: ContractAddress, start_time: u64, end_time: u64, ) { let current_time = get_block_timestamp(); - //get_caller should be streamer I think - let key = (get_caller_address(), recipient, start_time, end_time,); + let key = (recipient, start_time, end_time); let (already_claimed, total_amount): (u128, u128) = self.streams.read(key); assert(current_time > start_time, 'stream has not started'); @@ -148,33 +123,22 @@ mod streaming { IGovernanceTokenDispatcher { contract_address: self_dsp.get_governance_token_address() } .mint(recipient, amount_to_claim.into()); - self - .emit( - StreamClaimed { - streamer: streamer, - recipient: recipient, - start_time: start_time, - end_time: end_time, - total_amount: total_amount - } - ); + self.emit(StreamClaimed { recipient, start_time, end_time, total_amount, }); } fn cancel_stream( ref self: ComponentState, recipient: ContractAddress, start_time: u64, - end_time: u64 + end_time: u64, ) { - let key: (ContractAddress, ContractAddress, u64, u64) = ( - get_caller_address(), recipient, start_time, end_time, - ); + let key: (ContractAddress, u64, u64) = (recipient, start_time, end_time); // Read from the streams LegacyMap let (already_claimed, total_amount): (u128, u128) = self.streams.read(key); let to_distribute: u256 = total_amount.into() - already_claimed.into(); - //cancel stream + // Cancel stream self.streams.write(key, (0, 0)); let self_dsp = IGovernanceDispatcher { contract_address: get_contract_address() }; @@ -184,24 +148,18 @@ mod streaming { self .emit( StreamCanceled { - streamer: get_caller_address(), - recipient: recipient, - start_time: start_time, - end_time: end_time, - reclaimed_amount: to_distribute, + recipient, start_time, end_time, reclaimed_amount: to_distribute, } - ) + ); } + fn get_stream_info( ref self: ComponentState, - streamer: ContractAddress, recipient: ContractAddress, start_time: u64, end_time: u64, ) -> (u128, u128) { - let key: (ContractAddress, ContractAddress, u64, u64) = ( - get_caller_address(), recipient, start_time, end_time, - ); + let key: (ContractAddress, u64, u64) = (recipient, start_time, end_time); let (currently_claimable, total_amount): (u128, u128) = self.streams.read(key); (currently_claimable, total_amount) } diff --git a/tests/test_streaming.cairo b/tests/test_streaming.cairo index 8bd5a542..7b5b5df8 100644 --- a/tests/test_streaming.cairo +++ b/tests/test_streaming.cairo @@ -6,23 +6,23 @@ use debug::PrintTrait; use konoha::contract::Governance; use konoha::contract::{IGovernanceDispatcher, IGovernanceDispatcherTrait}; +use konoha::streaming::{IStreamingDispatcher, IStreamingDispatcherTrait, IStreaming}; use konoha::traits::{IGovernanceTokenDispatcher, IGovernanceTokenDispatcherTrait}; -use starknet::{get_block_timestamp, get_caller_address, get_contract_address, ContractAddress}; use konoha::vesting::{IVestingDispatcher, IVestingDispatcherTrait, IVesting}; -use konoha::streaming::{IStreamingDispatcher, IStreamingDispatcherTrait, IStreaming}; use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; use snforge_std::{ BlockId, declare, ContractClassTrait, ContractClass, start_prank, start_warp, CheatTarget, prank, CheatSpan }; +use starknet::{get_block_timestamp, get_caller_address, get_contract_address, ContractAddress}; use super::setup::{deploy_governance_and_both_tokens}; -fn start_stream(gov: ContractAddress){ +fn start_stream(gov: ContractAddress) { prank(CheatTarget::One(gov), gov, CheatSpan::TargetCalls(4)); let streaming = IStreamingDispatcher { contract_address: gov }; - streaming.add_new_stream(get_caller_address(), 0x2.try_into().unwrap(), 100, 200, 100000); + streaming.add_new_stream(0x2.try_into().unwrap(), 100, 200, 100000); } //passing! @@ -32,23 +32,17 @@ fn test_add_new_stream() { start_stream(gov.contract_address); let streaming = IStreamingDispatcher { contract_address: gov.contract_address }; - let streamer = get_caller_address(); let recipient = 0x2.try_into().unwrap(); let start_time: u64 = 100; let end_time: u64 = 200; let total_amount: u128 = 100000; - - streaming.add_new_stream(streamer, recipient, start_time, end_time, total_amount); + + streaming.add_new_stream(recipient, start_time, end_time, total_amount); //let key = (get_caller_address(), recipient, end_time, start_time); - let (claimed_amount, stored_total_amount) = streaming.get_stream_info( - streamer, - recipient, - start_time, - end_time, - ); + let (claimed_amount, stored_total_amount) = streaming + .get_stream_info(recipient, start_time, end_time,); - assert_eq!(streamer, get_caller_address(), "Incorrect streamer addr"); assert_eq!(recipient, 0x2.try_into().unwrap(), "Incorrect streamer addr"); assert_eq!(start_time, 100, "Incorrect start time"); assert_eq!(end_time, 200, "Incorrect end time"); @@ -57,58 +51,56 @@ fn test_add_new_stream() { } //passing! -#[test] +#[test] #[should_panic(expected: ('starts first',))] -fn test_valid_stream_time(){ +fn test_valid_stream_time() { let (gov, _, _) = deploy_governance_and_both_tokens(); start_stream(gov.contract_address); let streaming = IStreamingDispatcher { contract_address: gov.contract_address }; - let streamer = get_caller_address(); let recipient = 0x2.try_into().unwrap(); let start_time: u64 = 200; let end_time: u64 = 100; let total_amount: u128 = 100000; - streaming.add_new_stream(streamer, recipient, start_time, end_time, total_amount); + streaming.add_new_stream(recipient, start_time, end_time, total_amount); } //passing! -#[test] +#[test] #[should_panic(expected: ('nothing to claim',))] -fn test_claimed_amount(){ +fn test_claimed_amount() { let (gov, _, _) = deploy_governance_and_both_tokens(); start_stream(gov.contract_address); let streaming = IStreamingDispatcher { contract_address: gov.contract_address }; - let streamer = get_caller_address(); let recipient = 0x2.try_into().unwrap(); let start_time: u64 = 100; let end_time: u64 = 200; let total_amount: u128 = 0; - streaming.add_new_stream(streamer, recipient, start_time, end_time, total_amount); + streaming.add_new_stream(recipient, start_time, end_time, total_amount); start_warp(CheatTarget::One(gov.contract_address), 150); //shouldn't have anything to claim - streaming.claim_stream(streamer, recipient, start_time, end_time); + streaming.claim_stream(recipient, start_time, end_time); } //passing! #[test] #[should_panic(expected: ('stream has not started',))] -fn test_stream_started(){ - let (gov, _, _) = deploy_governance_and_both_tokens(); - start_stream(gov.contract_address); - let streaming = IStreamingDispatcher { contract_address: gov.contract_address }; - let streamer = get_caller_address(); - let recipient = 0x2.try_into().unwrap(); - let start_time: u64 = 100; - let end_time: u64 = 200; - let total_amount: u128 = 100000; - streaming.add_new_stream(streamer, recipient, start_time, end_time, total_amount); - start_warp(CheatTarget::One(gov.contract_address), 50);// before of stream - - streaming.claim_stream(streamer, recipient, start_time, end_time); +fn test_stream_started() { + let (gov, _, _) = deploy_governance_and_both_tokens(); + start_stream(gov.contract_address); + let streaming = IStreamingDispatcher { contract_address: gov.contract_address }; + + let recipient = 0x2.try_into().unwrap(); + let start_time: u64 = 100; + let end_time: u64 = 200; + let total_amount: u128 = 100000; + streaming.add_new_stream(recipient, start_time, end_time, total_amount); + start_warp(CheatTarget::One(gov.contract_address), 50); // before of stream + + streaming.claim_stream(recipient, start_time, end_time); } #[test] @@ -117,34 +109,31 @@ fn test_claim_stream() { start_stream(gov.contract_address); let streaming = IStreamingDispatcher { contract_address: gov.contract_address }; - let streamer = get_caller_address(); let recipient = 0x2.try_into().unwrap(); let start_time: u64 = 100; let end_time: u64 = 200; let total_amount: u128 = 100000; - streaming.add_new_stream(streamer, recipient, start_time, end_time, total_amount); - let (claimable_amount, total_amount) = streaming.get_stream_info( - streamer, - recipient, - start_time, - end_time, - ); + streaming.add_new_stream(recipient, start_time, end_time, total_amount); + let (claimable_amount, total_amount) = streaming + .get_stream_info(recipient, start_time, end_time,); start_warp(CheatTarget::One(gov.contract_address), 150); - streaming.claim_stream(streamer, recipient, start_time, end_time); + streaming.claim_stream(recipient, start_time, end_time); - let expected_claimed_amount = (100000 * 50/ 100); //should be 50% since middle of stream + let expected_claimed_amount = (100000 * 50 / 100); //should be 50% since middle of stream assert_eq!(total_amount, 100000, "Incorrect total amount after claiming the stream"); assert_eq!(claimable_amount, 0, "Incorrect claimed amount after claiming the stream"); let self_dsp = IGovernanceDispatcher { contract_address: gov.contract_address }; let token_address = self_dsp.get_governance_token_address(); let erc20 = IERC20Dispatcher { contract_address: token_address }; - + let balance = erc20.balance_of(recipient); - assert_eq!(balance, expected_claimed_amount, "Balance should match the expected claimed amount"); + assert_eq!( + balance, expected_claimed_amount, "Balance should match the expected claimed amount" + ); } #[test] @@ -153,24 +142,20 @@ fn test_cancel_stream() { start_stream(gov.contract_address); let streaming = IStreamingDispatcher { contract_address: gov.contract_address }; - let streamer = get_caller_address(); let recipient = 0x2.try_into().unwrap(); let start_time: u64 = 100; let end_time: u64 = 200; let total_amount: u128 = 100000; - streaming.add_new_stream(streamer, recipient, start_time, end_time, total_amount); + streaming.add_new_stream(recipient, start_time, end_time, total_amount); start_warp(CheatTarget::One(gov.contract_address), 150); //test cancel_stream streaming.cancel_stream(recipient, start_time, end_time); - let (claimed_amount, stored_total_amount) = streaming.get_stream_info( - streamer, - recipient, - start_time, - end_time); + let (claimed_amount, stored_total_amount) = streaming + .get_stream_info(recipient, start_time, end_time); assert_eq!(claimed_amount, 0, "Claimed amount should be 0 after canceling the stream"); assert_eq!(stored_total_amount, 0, "Total amount should be 0 after canceling the stream"); @@ -186,48 +171,3 @@ fn test_cancel_stream() { assert_eq!(unclaimed_amount.into(), 100000, "Unclaimed amount should be reclaimed correctly"); assert_eq!(balance, 0, "balance"); } - -#[test] -//#[should_panic(expected: "nothing to claim")] -fn test_add_new_stream_failure() { - let (gov, _, _) = deploy_governance_and_both_tokens(); - start_stream(gov.contract_address); - let streaming = IStreamingDispatcher { contract_address: gov.contract_address }; - - let streamer = 0x1.try_into().unwrap(); // This will be an address other than the contract's address - let recipient = 0x2.try_into().unwrap(); - let start_time: u64 = 100; - let end_time: u64 = 200; - let total_amount: u128 = 100000; - - // Add a stream with `streamer` as `gov` address - streaming.add_new_stream(streamer, recipient, start_time, end_time, total_amount); - - let (claimable_amount, stored_total_amount) = streaming.get_stream_info( - streamer, - recipient, - start_time, - end_time, - ); - - start_warp(CheatTarget::One(gov.contract_address), 150); // Advance time by 150 units - - streaming.claim_stream( - streamer, // Using the wrong `streamer` address - recipient, - start_time, - end_time, - ); - - assert_eq!(stored_total_amount, 100000, "Incorrect total amount after attempting to claim the stream"); - assert_eq!(claimable_amount, 0, "Incorrect claimed amount after attempting to claim the stream"); - - let self_dsp = IGovernanceDispatcher { contract_address: gov.contract_address }; - let token_address = self_dsp.get_governance_token_address(); - let erc20 = IERC20Dispatcher { contract_address: token_address }; - - let balance = erc20.balance_of(recipient); - - let expected_claimed_amount = (100000 * 50 / 100); // should be 50% since middle of stream - assert_eq!(balance, expected_claimed_amount, "Balance should match the expected claimed amount"); -}