From 2a503775e23d3a742025198bacba4368dff439e6 Mon Sep 17 00:00:00 2001 From: saimeunt Date: Sun, 30 Mar 2025 21:46:55 +0200 Subject: [PATCH 1/2] wip --- src/base.cairo | 1 + src/base/errors.cairo | 3 + src/base/helpers.cairo | 25 +++ src/base/types.cairo | 3 +- src/interfaces/IPaymentStream.cairo | 28 ++- src/payment_stream.cairo | 243 ++++++++++++++++++++++----- tests/test_distribute_contract.cairo | 3 - tests/test_payment_stream.cairo | 2 +- 8 files changed, 262 insertions(+), 46 deletions(-) create mode 100644 src/base/helpers.cairo diff --git a/src/base.cairo b/src/base.cairo index 422d669..f989bef 100644 --- a/src/base.cairo +++ b/src/base.cairo @@ -1,2 +1,3 @@ pub mod errors; +pub mod helpers; pub mod types; diff --git a/src/base/errors.cairo b/src/base/errors.cairo index a98f510..a0df20a 100644 --- a/src/base/errors.cairo +++ b/src/base/errors.cairo @@ -44,5 +44,8 @@ pub mod Errors { /// Thrown when wrong recipient or delegate pub const WRONG_RECIPIENT_OR_DELEGATE: felt252 = 'WRONG_RECIPIENT_OR_DELEGATE'; + + /// @notice Thrown when trying to withdraw an amount greater than the withdrawable amount. + pub const OVERDRAW: felt252 = 'Error: Overdraw'; } diff --git a/src/base/helpers.cairo b/src/base/helpers.cairo new file mode 100644 index 0000000..c83f1de --- /dev/null +++ b/src/base/helpers.cairo @@ -0,0 +1,25 @@ +pub mod Helpers { + use core::num::traits::Pow; + + /// @dev Descales the provided `amount` from 18 decimals fixed-point number to token's decimals + /// number. + pub fn descale_amount(amount: u256, decimals: u8) -> u256 { + if decimals == 18 { + return amount; + } + + let scale_factor = 10_u256.pow(18 - decimals.into()); + return amount / scale_factor; + } + + /// @dev Scales the provided `amount` from token's decimals number to 18 decimals fixed-point + /// number. + pub fn scale_amount(amount: u256, decimals: u8) -> u256 { + if decimals == 18 { + return amount; + } + + let scale_factor = 10_u256.pow(18 - decimals.into()); + return amount * scale_factor; + } +} diff --git a/src/base/types.cairo b/src/base/types.cairo index 38e4d13..c3bcf75 100644 --- a/src/base/types.cairo +++ b/src/base/types.cairo @@ -14,7 +14,8 @@ pub struct Stream { pub token_decimals: u8, pub status: StreamStatus, pub rate_per_second: UFixedPoint123x128, - pub last_update_time: u64, + pub snapshot_debt_scaled: u256, + pub snapshot_time: u64, } #[derive(Drop, starknet::Event)] diff --git a/src/interfaces/IPaymentStream.cairo b/src/interfaces/IPaymentStream.cairo index b4a1214..7f273c4 100644 --- a/src/interfaces/IPaymentStream.cairo +++ b/src/interfaces/IPaymentStream.cairo @@ -1,6 +1,6 @@ use fp::UFixedPoint123x128; use starknet::ContractAddress; -use crate::base::types::{ProtocolMetrics, Stream, StreamMetrics}; +use crate::base::types::{ProtocolMetrics, Stream, StreamMetrics, StreamStatus}; /// @title IPaymentStream /// @notice Creates and manages payment streams with linear streaming functions. @@ -250,4 +250,30 @@ pub trait IPaymentStream { /// @param amount The amount to refund from the stream /// @return Boolean indicating if the refund and pause operation was successful fn refund_and_pause(ref self: TContractState, stream_id: u256, amount: u256) -> bool; + + /// @notice Returns the stream's status. + /// @dev Reverts if `stream_id` references a null stream. + /// Integrators should exercise caution when depending on the return value of this function as + /// streams can be paused and resumed at any moment. + /// @param stream_id The stream ID for the query. + fn status_of(self: @TContractState, stream_id: u256) -> StreamStatus; + + /// @notice Returns the amount of debt accrued since the snapshot time until now, denoted as a + /// fixed-point number where 1e18 is 1 token. + /// @dev Reverts if `stream_id` references a null stream. + /// @param stream_id The stream ID for the query. + fn ongoing_debt_scaled_of(self: @TContractState, stream_id: u256) -> u256; + + /// @notice Calculates the amount that the recipient can withdraw from the stream, denoted in + /// token decimals. + /// @dev Reverts if `stream_id` references a null stream. + /// @param stream_id The stream ID for the query. + /// @return withdrawableAmount The amount that the recipient can withdraw. + fn covered_debt_of(self: @TContractState, stream_id: u256) -> u128; + + /// @notice Returns the amount of debt not covered by the stream balance, denoted in token's + /// decimals. + /// @dev Reverts if `stream_id` references a null stream. + /// @param stream_id The stream ID for the query. + fn uncovered_debt_of(self: @TContractState, stream_id: u256) -> u256; } diff --git a/src/payment_stream.cairo b/src/payment_stream.cairo index cd52bfc..985dda9 100644 --- a/src/payment_stream.cairo +++ b/src/payment_stream.cairo @@ -18,9 +18,10 @@ mod PaymentStream { use starknet::{ContractAddress, get_block_timestamp, get_caller_address, get_contract_address}; use crate::base::errors::Errors::{ DECIMALS_TOO_HIGH, END_BEFORE_START, INSUFFICIENT_ALLOWANCE, INVALID_RECIPIENT, - INVALID_TOKEN, TOO_SHORT_DURATION, UNEXISTING_STREAM, WRONG_RECIPIENT_OR_DELEGATE, + INVALID_TOKEN, OVERDRAW, TOO_SHORT_DURATION, UNEXISTING_STREAM, WRONG_RECIPIENT_OR_DELEGATE, WRONG_SENDER, ZERO_AMOUNT, }; + use crate::base::helpers::Helpers; use crate::base::types::{ProtocolMetrics, Stream, StreamMetrics, StreamStatus}; component!(path: AccessControlComponent, storage: accesscontrol, event: AccessControlEvent); @@ -276,8 +277,7 @@ mod PaymentStream { assert(duration >= 1, TOO_SHORT_DURATION); let rate_per_second = self.calculate_stream_rate(total_amount, duration); - let erc20_dispatcher = IERC20MetadataDispatcher { contract_address: token }; - let token_decimals = erc20_dispatcher.decimals(); + let token_decimals = IERC20MetadataDispatcher { contract_address: token }.decimals(); assert(token_decimals <= 18, DECIMALS_TOO_HIGH); // Create new stream @@ -293,7 +293,8 @@ mod PaymentStream { cancelable, status: StreamStatus::Active, rate_per_second, - last_update_time: start_time, + snapshot_debt_scaled: 0, + snapshot_time: start_time, }; self.accesscontrol._grant_role(STREAM_ADMIN_ROLE, stream.sender); @@ -329,11 +330,73 @@ mod PaymentStream { fn withdraw( ref self: ContractState, stream_id: u256, amount: u256, to: ContractAddress, ) -> (u128, u128) { - self.assert_is_recipient_or_delegate(stream_id); + // Check: the withdraw amount is not zero. assert(amount > 0, ZERO_AMOUNT); + + // Check: the withdrawal address is not zero. assert(to.is_non_zero(), INVALID_RECIPIENT); + // Check: `msg.sender` is neither the stream's recipient nor an approved third party, + // the withdrawal address must be the recipient. + if to != self.erc721.owner_of(stream_id) { + self.assert_is_recipient_or_delegate(stream_id); + } + let stream = self.streams.read(stream_id); + + let token_decimals = stream.token_decimals; + + // Calculate the total debt. + let total_debt_scaled = self.ongoing_debt_scaled_of(stream_id) + + stream.snapshot_debt_scaled; + let total_debt = Helpers::descale_amount(total_debt_scaled, token_decimals); + println!("total_debt_scaled: {} total_debt: {}", total_debt_scaled, total_debt); + + // Calculate the withdrawable amount. + let balance = stream.total_amount; + let withdrawable_amount = if balance < total_debt { + // If the stream balance is less than the total debt, the withdrawable amount is the + // balance. + balance + } else { + // Otherwise, the withdrawable amount is the total debt. + total_debt + }; + + println!("amount: {} withdrawable_amount: {}", amount, withdrawable_amount); + // Check: the withdraw amount is not greater than the withdrawable amount. + assert(amount <= withdrawable_amount, OVERDRAW); + + // Calculate the amount scaled. + let amount_scaled = Helpers::scale_amount(amount, token_decimals); + + let mut snapshot_debt_scaled = stream.snapshot_debt_scaled; + let mut snapshot_time = stream.snapshot_time; + // If the amount is less than the snapshot debt, reduce it from the snapshot debt and + // leave the snapshot time unchanged. + if amount_scaled <= stream.snapshot_debt_scaled { + snapshot_debt_scaled -= amount_scaled; + } // Else reduce the amount from the ongoing debt by setting snapshot time to + // `block.timestamp` and set the snapshot debt to the remaining total debt. + else { + snapshot_debt_scaled = total_debt_scaled - amount_scaled; + // Effect: update the stream time. + snapshot_time = get_block_timestamp(); + } + + self + .streams + .write( + stream_id, + Stream { + snapshot_debt_scaled, + snapshot_time, + // Effect: update the stream balance. + total_amount: stream.total_amount - amount, + ..stream, + }, + ); + let fee = self.calculate_protocol_fee(amount); let net_amount = (amount - fee); let token_address = stream.token; @@ -536,14 +599,52 @@ mod PaymentStream { } fn void(ref self: ContractState, stream_id: u256) { - let mut stream = self.streams.read(stream_id); - self.assert_stream_exists(stream_id); + + let stream = self.streams.read(stream_id); assert(stream.status != StreamStatus::Canceled, 'Stream is not active'); - stream.status = StreamStatus::Voided; - self.streams.write(stream_id, stream); + // Check: `msg.sender` is either the stream's sender, recipient or an approved third + // party. + let caller = get_caller_address(); + if caller != stream.sender { + self.assert_is_recipient_or_delegate(stream_id); + } + + let debt_to_write_off = self.uncovered_debt_of(stream_id); + + let mut snapshot_debt_scaled = stream.snapshot_debt_scaled; + // If the stream is solvent, update the total debt normally. + if debt_to_write_off == 0 { + let ongoing_debt_scaled = self.ongoing_debt_scaled_of(stream_id); + if ongoing_debt_scaled > 0 { + // Effect: Update the snapshot debt by adding the ongoing debt. + snapshot_debt_scaled += ongoing_debt_scaled; + } + } // If the stream is insolvent, write off the uncovered debt. + else { + // Effect: update the total debt by setting snapshot debt to the stream balance. + snapshot_debt_scaled = + Helpers::scale_amount(stream.total_amount, stream.token_decimals); + } + + self + .streams + .write( + stream_id, + Stream { + snapshot_debt_scaled, + // Effect: update the snapshot time. + snapshot_time: get_block_timestamp(), + // Effect: set the rate per second to zero. + rate_per_second: 0.into(), + // Effect: set the stream as voided. + status: StreamStatus::Voided, + ..stream, + }, + ); + // Log the void. self.emit(StreamVoided { stream_id }); } @@ -617,21 +718,6 @@ mod PaymentStream { fn get_stream(self: @ContractState, stream_id: u256) -> Stream { - // Return dummy stream - // Stream { - // sender: starknet::contract_address_const::<0>(), - // recipient: starknet::contract_address_const::<0>(), - // token: starknet::contract_address_const::<0>(), - // total_amount: 0_u256, - // start_time: 0_u64, - // end_time: 0_u64, - // withdrawn_amount: 0_u256, - // cancelable: false, - // status: StreamStatus::Active, - // rate_per_second: 0, - // last_update_time: 0, - // } - self.streams.read(stream_id) } @@ -661,8 +747,10 @@ mod PaymentStream { } fn get_total_debt(self: @ContractState, stream_id: u256) -> u256 { - // Return dummy amount - 0_u256 + let stream = self.streams.read(stream_id); + let total_debt_scaled = self.ongoing_debt_scaled_of(stream_id) + + stream.snapshot_debt_scaled; + Helpers::descale_amount(total_debt_scaled, decimals: stream.token_decimals) } fn get_uncovered_debt(self: @ContractState, stream_id: u256) -> u256 { @@ -735,21 +823,26 @@ mod PaymentStream { let stream: Stream = self.streams.read(stream_id); assert!(stream.status == StreamStatus::Active, "Stream is not active"); - let new_stream = Stream { - rate_per_second: new_rate_per_second, - sender: stream.sender, - token: stream.token, - token_decimals: stream.token_decimals, - total_amount: stream.total_amount, - start_time: stream.start_time, - end_time: stream.end_time, - withdrawn_amount: stream.withdrawn_amount, - cancelable: stream.cancelable, - status: stream.status, - last_update_time: starknet::get_block_timestamp(), + let ongoing_debt_scaled = self.ongoing_debt_scaled_of(stream_id); + + let snapshot_debt_scaled = if ongoing_debt_scaled > 0 { + stream.snapshot_debt_scaled + ongoing_debt_scaled + } else { + stream.snapshot_debt_scaled }; - self.streams.write(stream_id, new_stream); + self + .streams + .write( + stream_id, + Stream { + rate_per_second: new_rate_per_second, + snapshot_debt_scaled, + snapshot_time: get_block_timestamp(), + ..stream, + }, + ); + self .emit( Event::StreamRateUpdated( @@ -762,12 +855,15 @@ mod PaymentStream { ), ); } + fn get_protocol_fee(self: @ContractState, token: ContractAddress) -> u256 { self.protocol_fees.read(token) } + fn get_protocol_revenue(self: @ContractState, token: ContractAddress) -> u256 { self.protocol_revenue.read(token) } + fn collect_protocol_revenue( ref self: ContractState, token: ContractAddress, to: ContractAddress, ) { @@ -784,8 +880,9 @@ mod PaymentStream { sent_to: to, amount: protocol_revenue, }, - ) + ); } + fn set_protocol_fee( ref self: ContractState, token: ContractAddress, new_protocol_fee: u256, ) { @@ -853,5 +950,71 @@ mod PaymentStream { return stream.rate_per_second; } + + fn status_of(self: @ContractState, stream_id: u256) -> StreamStatus { + let stream = self.streams.read(stream_id); + stream.status + } + + /// @dev Calculates the ongoing debt, as a 18-decimals fixed point number, accrued since + /// last snapshot. Return 0 if the stream is paused or `block.timestamp` is less than or + /// equal to snapshot time. + fn ongoing_debt_scaled_of(self: @ContractState, stream_id: u256) -> u256 { + let block_timestamp = get_block_timestamp(); + let stream = self.streams.read(stream_id); + let snapshot_time = stream.snapshot_time; + + let rate_per_second: u256 = stream.rate_per_second.into(); + + // Check:if the rate per second is zero or the `block.timestamp` is less than the + // `snapshotTime`. + if rate_per_second == 0 || (block_timestamp <= snapshot_time) { + return 0; + } + + // Calculate time elapsed since the last snapshot. + let elapsed_time = block_timestamp - snapshot_time; + + // Calculate the ongoing debt scaled accrued by multiplying the elapsed time by the rate + // per second. + elapsed_time.into() * rate_per_second + } + + /// @dev Calculates the amount of covered debt by the stream balance. + fn covered_debt_of(self: @ContractState, stream_id: u256) -> u128 { + let stream = self.streams.read(stream_id); + let balance = stream.total_amount; + + // If the balance is zero, return zero. + if balance == 0 { + return 0; + } + + let total_debt = self.get_total_debt(stream_id); + + // If the stream balance is less than or equal to the total debt, return the stream + // balance. + if balance < total_debt { + return balance.try_into().unwrap(); + } + + // At this point, the total debt fits within `uint128`, as it is less than or equal to + // the balance. + total_debt.try_into().unwrap() + } + + /// @dev Calculates the uncovered debt. + fn uncovered_debt_of(self: @ContractState, stream_id: u256) -> u256 { + let stream = self.streams.read(stream_id); + let balance = stream.total_amount; + + let total_debt = self.get_total_debt(stream_id); + + if balance < total_debt { + total_debt - balance + } else { + 0 + } + } } } diff --git a/tests/test_distribute_contract.cairo b/tests/test_distribute_contract.cairo index cbc4336..72505be 100644 --- a/tests/test_distribute_contract.cairo +++ b/tests/test_distribute_contract.cairo @@ -1,8 +1,5 @@ use core::num::traits::Bounded; use core::traits::Into; -use fundable::base::types::{ - Distribution, DistributionHistory, TokenStats, UserStats, WeightedDistribution, -}; use fundable::interfaces::IDistributor::{IDistributorDispatcher, IDistributorDispatcherTrait}; //use fundable::interfaces::IERC20::{IERC20Dispatcher, IERC20DispatcherTrait}; use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; diff --git a/tests/test_payment_stream.cairo b/tests/test_payment_stream.cairo index c5e8d1c..2733b5e 100644 --- a/tests/test_payment_stream.cairo +++ b/tests/test_payment_stream.cairo @@ -1,6 +1,6 @@ use core::traits::Into; use fp::UFixedPoint123x128; -use fundable::base::types::{Stream, StreamStatus}; +use fundable::base::types::StreamStatus; use fundable::interfaces::IPaymentStream::{IPaymentStreamDispatcher, IPaymentStreamDispatcherTrait}; use openzeppelin::access::accesscontrol::interface::{ IAccessControlDispatcher, IAccessControlDispatcherTrait, From 260de3bd97ee9319d1cdecfa9d3ef58ab670c7b4 Mon Sep 17 00:00:00 2001 From: saimeunt Date: Tue, 1 Apr 2025 23:33:07 +0200 Subject: [PATCH 2/2] add tests --- src/base/types.cairo | 2 +- src/payment_stream.cairo | 2 - tests/test_payment_stream.cairo | 174 +++++++++++++++++++++++++++++++- 3 files changed, 173 insertions(+), 5 deletions(-) diff --git a/src/base/types.cairo b/src/base/types.cairo index 39b37fd..3646b73 100644 --- a/src/base/types.cairo +++ b/src/base/types.cairo @@ -46,7 +46,7 @@ pub struct WeightedDistribution { } /// @notice Enum representing the possible states of a stream -#[derive(Drop, Serde, starknet::Store, PartialEq)] +#[derive(Drop, Serde, starknet::Store, PartialEq, Debug)] #[allow(starknet::store_no_default_variant)] pub enum StreamStatus { Active, // Stream is actively streaming tokens diff --git a/src/payment_stream.cairo b/src/payment_stream.cairo index a00a231..a9b8a7f 100644 --- a/src/payment_stream.cairo +++ b/src/payment_stream.cairo @@ -549,7 +549,6 @@ pub mod PaymentStream { let total_debt_scaled = self.ongoing_debt_scaled_of(stream_id) + stream.snapshot_debt_scaled; let total_debt = Helpers::descale_amount(total_debt_scaled, token_decimals); - println!("total_debt_scaled: {} total_debt: {}", total_debt_scaled, total_debt); // Calculate the withdrawable amount. let balance = stream.balance; @@ -562,7 +561,6 @@ pub mod PaymentStream { total_debt }; - println!("amount: {} withdrawable_amount: {}", amount, withdrawable_amount); // Check: the withdraw amount is not greater than the withdrawable amount. // assert(amount <= withdrawable_amount, OVERDRAW); diff --git a/tests/test_payment_stream.cairo b/tests/test_payment_stream.cairo index 9461247..9e830a8 100644 --- a/tests/test_payment_stream.cairo +++ b/tests/test_payment_stream.cairo @@ -1,19 +1,23 @@ use core::traits::Into; use fp::UFixedPoint123x128; +use fundable::base::helpers::Helpers; use fundable::base::types::StreamStatus; use fundable::interfaces::IPaymentStream::{IPaymentStreamDispatcher, IPaymentStreamDispatcherTrait}; use fundable::payment_stream::PaymentStream; use openzeppelin::access::accesscontrol::interface::{ IAccessControlDispatcher, IAccessControlDispatcherTrait, }; -use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +use openzeppelin::token::erc20::interface::{ + IERC20Dispatcher, IERC20DispatcherTrait, IERC20MixinDispatcher, IERC20MixinDispatcherTrait, +}; use openzeppelin::token::erc721::interface::{ IERC721Dispatcher, IERC721DispatcherTrait, IERC721MetadataDispatcher, IERC721MetadataDispatcherTrait, }; use snforge_std::{ ContractClassTrait, DeclareResultTrait, EventSpyAssertionsTrait, declare, spy_events, - start_cheat_caller_address, stop_cheat_caller_address, test_address, + start_cheat_block_timestamp, start_cheat_caller_address, stop_cheat_caller_address, + test_address, }; use starknet::{ContractAddress, contract_address_const}; @@ -2143,3 +2147,169 @@ fn test_restart_and_deposit_insufficient_allowance() { payment_stream.restart_and_deposit(stream_id, new_rate, additional_amount); stop_cheat_caller_address(payment_stream.contract_address); } + +#[test] +fn test_status_of_active() { + let (token_address, _, payment_stream, _) = setup(); + let recipient = contract_address_const::<0x2>(); + let total_amount = 1000_u256; + let start_time = 100_u64; + let end_time = 200_u64; + let cancelable = true; + let transferable = true; + + let stream_id = payment_stream + .create_stream( + recipient, total_amount, start_time, end_time, cancelable, token_address, transferable, + ); + assert_eq!(payment_stream.status_of(stream_id), StreamStatus::Active); +} + +#[test] +fn test_status_of_paused() { + let (token_address, _, payment_stream, _) = setup(); + let recipient = contract_address_const::<0x2>(); + let total_amount = 1000_u256; + let start_time = 100_u64; + let end_time = 200_u64; + let cancelable = true; + let transferable = true; + + let stream_id = payment_stream + .create_stream( + recipient, total_amount, start_time, end_time, cancelable, token_address, transferable, + ); + payment_stream.pause(stream_id); + assert_eq!(payment_stream.status_of(stream_id), StreamStatus::Paused); +} + +#[test] +fn test_status_of_voided() { + let (token_address, _, payment_stream, _) = setup(); + let recipient = contract_address_const::<0x2>(); + let total_amount = 1000_u256; + let start_time = 100_u64; + let end_time = 200_u64; + let cancelable = true; + let transferable = true; + + let stream_id = payment_stream + .create_stream( + recipient, total_amount, start_time, end_time, cancelable, token_address, transferable, + ); + payment_stream.void(stream_id); + assert_eq!(payment_stream.status_of(stream_id), StreamStatus::Voided); +} + +#[test] +fn test_status_of_canceled() { + let (token_address, sender, payment_stream, _) = setup(); + let recipient = contract_address_const::<0x2>(); + let total_amount = 1000_u256; + let start_time = 100_u64; + let end_time = 200_u64; + let cancelable = true; + let transferable = true; + + let token = IERC20Dispatcher { contract_address: token_address }; + start_cheat_caller_address(token_address, sender); + token.approve(payment_stream.contract_address, total_amount); + stop_cheat_caller_address(token_address); + + start_cheat_caller_address(payment_stream.contract_address, sender); + let stream_id = payment_stream + .create_stream_with_deposit( + recipient, total_amount, start_time, end_time, cancelable, token_address, transferable, + ); + payment_stream.cancel(stream_id); + assert_eq!(payment_stream.status_of(stream_id), StreamStatus::Canceled); +} + +#[test] +fn test_ongoing_debt_scaled_of() { + let (token_address, _, payment_stream, _) = setup(); + let recipient = contract_address_const::<0x2>(); + let total_amount = 1000_u256; + let start_time = 100_u64; + let end_time = 200_u64; + let cancelable = true; + let transferable = true; + + let stream_id = payment_stream + .create_stream( + recipient, total_amount, start_time, end_time, cancelable, token_address, transferable, + ); + start_cheat_block_timestamp(payment_stream.contract_address, start_time); + assert_eq!(payment_stream.ongoing_debt_scaled_of(stream_id), 0); + start_cheat_block_timestamp( + payment_stream.contract_address, start_time + (end_time - start_time) / 2, + ); + assert_eq!(payment_stream.ongoing_debt_scaled_of(stream_id), total_amount / 2); + start_cheat_block_timestamp(payment_stream.contract_address, end_time); + assert_eq!(payment_stream.ongoing_debt_scaled_of(stream_id), total_amount); +} + +#[test] +fn test_covered_debt_of() { + let (token_address, sender, payment_stream, _) = setup(); + let recipient = contract_address_const::<0x2>(); + let total_amount = 10000000000000_u256; + let start_time = 100_u64; + let end_time = 200_u64; + let cancelable = true; + let transferable = true; + + let stream_id = payment_stream + .create_stream( + recipient, total_amount, start_time, end_time, cancelable, token_address, transferable, + ); + assert_eq!(payment_stream.covered_debt_of(stream_id), 0); + + let token = IERC20MixinDispatcher { contract_address: token_address }; + start_cheat_caller_address(token_address, sender); + token.approve(payment_stream.contract_address, total_amount); + stop_cheat_caller_address(token_address); + + start_cheat_caller_address(payment_stream.contract_address, sender); + let stream_id = payment_stream + .create_stream_with_deposit( + recipient, total_amount, start_time, end_time, cancelable, token_address, transferable, + ); + start_cheat_block_timestamp(payment_stream.contract_address, start_time); + assert_eq!(payment_stream.covered_debt_of(stream_id), 0); + start_cheat_block_timestamp( + payment_stream.contract_address, start_time + (end_time - start_time) / 2, + ); + assert_eq!( + payment_stream.covered_debt_of(stream_id).into(), + Helpers::descale_amount(total_amount / 2, token.decimals()), + ); + start_cheat_block_timestamp(payment_stream.contract_address, end_time); + assert_eq!( + payment_stream.covered_debt_of(stream_id).into(), + Helpers::descale_amount(total_amount, token.decimals()), + ); +} + +#[test] +fn test_uncovered_debt_of() { + let (token_address, sender, payment_stream, _) = setup(); + let recipient = contract_address_const::<0x2>(); + let total_amount = 10000000000000_u256; + let start_time = 100_u64; + let end_time = 200_u64; + let cancelable = true; + let transferable = true; + + let token = IERC20MixinDispatcher { contract_address: token_address }; + start_cheat_caller_address(token_address, sender); + token.approve(payment_stream.contract_address, total_amount); + stop_cheat_caller_address(token_address); + + start_cheat_caller_address(payment_stream.contract_address, sender); + let stream_id = payment_stream + .create_stream_with_deposit( + recipient, total_amount, start_time, end_time, cancelable, token_address, transferable, + ); + assert_eq!(payment_stream.uncovered_debt_of(stream_id), 0); +}