From f86486aaec70f85c26528435913f8d467eba2a2e Mon Sep 17 00:00:00 2001 From: Gift-Naomi Date: Mon, 22 Jul 2024 13:50:27 +0100 Subject: [PATCH 1/8] feat: implement erc20 streaming --- Scarb.lock | 4 + .../erc20/src/erc20_streaming.cairo | 121 ++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 listings/applications/erc20/src/erc20_streaming.cairo diff --git a/Scarb.lock b/Scarb.lock index 96d58087..038b225c 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -131,6 +131,10 @@ dependencies = [ "openzeppelin", ] +[[package]] +name = "simple_storage" +version = "0.1.0" + [[package]] name = "simple_vault" version = "0.1.0" diff --git a/listings/applications/erc20/src/erc20_streaming.cairo b/listings/applications/erc20/src/erc20_streaming.cairo new file mode 100644 index 00000000..4105e538 --- /dev/null +++ b/listings/applications/erc20/src/erc20_streaming.cairo @@ -0,0 +1,121 @@ +#[starknet::contract] + +pub mod erc20_streaming { + + // Import necessary modules and traits + use core::num::traits::Zero; + use starknet::get_caller_address; + use starknet::ContractAddress; + use starknet::SyscallResultTrait; + use starknet::LegacyMap; + use starknet::testing::set_contract_address; + use starknet::testing::set_account_contract_address; + + #[storage] + struct Storage { + streams: LegacyMap<(ContractAddress, ContractAddress), Stream>, + erc20_token: ContractAddress, + } + + #[derive(Copy, Drop, Debug, PartialEq)] + struct Stream { + start_time: u64, + end_time: u64, + total_amount: felt252, + released_amount: felt252, + } + + #[event] + #[derive(Copy, Drop, Debug, PartialEq, starknet::Event)] + pub enum Event { + StreamCreated: StreamCreated, + TokensReleased: TokensReleased, + } + + #[derive(Copy, Drop, Debug, PartialEq, starknet::Event)] + pub struct StreamCreated { + pub from: ContractAddress, + pub to: ContractAddress, + pub total_amount: felt252, + pub start_time: u64, + pub end_time: u64, + } + + #[derive(Copy, Drop, Debug, PartialEq, starknet::Event)] + pub struct TokensReleased { + pub to: ContractAddress, + pub amount: felt252, + } + + mod Errors { + pub const STREAM_AMOUNT_ZERO: felt252 = 'Stream amount cannot be zero'; + pub const STREAM_ALREADY_EXISTS: felt252 = 'Stream already exists'; + } + + #[constructor] + fn constructor(ref self: ContractState, erc20_token: ContractAddress) { + self.erc20_token.write(erc20_token); + } + + #[abi(embed_v0)] + impl IStreamImpl of super::IStream { + fn create_stream( + ref self: ContractState, + to: ContractAddress, + total_amount: felt252, + start_time: u64, + end_time: u64 + ) { + assert(total_amount != felt252::zero(), Errors::STREAM_AMOUNT_ZERO); + let caller = get_caller_address(); + let stream_key = (caller, to); + assert(self.streams.read(stream_key).start_time == 0, Errors::STREAM_ALREADY_EXISTS); + + // Call the ERC20 contract to transfer tokens + let erc20 = self.erc20_token.read(); + erc20.call("transfer_from", (caller, self.contract_address(), total_amount)); + + let stream = Stream { + start_time, + end_time, + total_amount, + released_amount: felt252::zero(), + }; + self.streams.write(stream_key, stream); + + self.emit(StreamCreated { from: caller, to, total_amount, start_time, end_time }); + } + + fn release_tokens(ref self: ContractState, to: ContractAddress) { + let caller = get_caller_address(); + let stream_key = (caller, to); + let stream = self.streams.read(stream_key); + let releasable_amount = self.releasable_amount(stream); + self.streams.write( + stream_key, + Stream { + released_amount: stream.released_amount + releasable_amount, + ..stream + } + ); + + // Call the ERC20 contract to transfer tokens + let erc20 = self.erc20_token.read(); + erc20.call("transfer", (to, releasable_amount)); + + self.emit(TokensReleased { to, amount: releasable_amount }); + } + + fn releasable_amount(&self, stream: Stream) -> felt252 { + let current_time = starknet::get_block_timestamp(); + if current_time >= stream.end_time { + return stream.total_amount - stream.released_amount; + } else { + let time_elapsed = current_time - stream.start_time; + let vesting_duration = stream.end_time - stream.start_time; + let vested_amount = stream.total_amount * time_elapsed / vesting_duration; + return vested_amount - stream.released_amount; + } + } + } +} From 09b1a44cb4a143e43ffddf5f1ad5982e658fe686 Mon Sep 17 00:00:00 2001 From: Mystic <149405096+Gift-Naomi@users.noreply.github.com> Date: Fri, 26 Jul 2024 15:49:21 +0100 Subject: [PATCH 2/8] Update erc20_streaming.cairo --- listings/applications/erc20/src/erc20_streaming.cairo | 3 --- 1 file changed, 3 deletions(-) diff --git a/listings/applications/erc20/src/erc20_streaming.cairo b/listings/applications/erc20/src/erc20_streaming.cairo index 4105e538..a4cd6911 100644 --- a/listings/applications/erc20/src/erc20_streaming.cairo +++ b/listings/applications/erc20/src/erc20_streaming.cairo @@ -6,10 +6,7 @@ pub mod erc20_streaming { use core::num::traits::Zero; use starknet::get_caller_address; use starknet::ContractAddress; - use starknet::SyscallResultTrait; use starknet::LegacyMap; - use starknet::testing::set_contract_address; - use starknet::testing::set_account_contract_address; #[storage] struct Storage { From acec0cde25fe45aeb7640a2b2b91fce950ce6ff6 Mon Sep 17 00:00:00 2001 From: Gift-Naomi Date: Thu, 3 Oct 2024 12:39:47 +0100 Subject: [PATCH 3/8] feat: add ERC20 token streaming functionality and update chapter page --- .../erc20/src/erc20_streaming.cairo | 32 +++++++++----- src/applications/erc20.md | 43 ++++++++++++++++--- 2 files changed, 58 insertions(+), 17 deletions(-) diff --git a/listings/applications/erc20/src/erc20_streaming.cairo b/listings/applications/erc20/src/erc20_streaming.cairo index 4105e538..e1e0736a 100644 --- a/listings/applications/erc20/src/erc20_streaming.cairo +++ b/listings/applications/erc20/src/erc20_streaming.cairo @@ -23,6 +23,7 @@ pub mod erc20_streaming { end_time: u64, total_amount: felt252, released_amount: felt252, + to: ContractAddress, } #[event] @@ -50,6 +51,8 @@ pub mod erc20_streaming { mod Errors { pub const STREAM_AMOUNT_ZERO: felt252 = 'Stream amount cannot be zero'; pub const STREAM_ALREADY_EXISTS: felt252 = 'Stream already exists'; + pub const END_TIME_INVALID: felt252 = 'End time must be greater than start time'; + pub const STREAM_UNAUTHORIZED: felt252 = 'Caller is not the recipient of the stream'; } #[constructor] @@ -63,11 +66,13 @@ pub mod erc20_streaming { ref self: ContractState, to: ContractAddress, total_amount: felt252, - start_time: u64, end_time: u64 ) { assert(total_amount != felt252::zero(), Errors::STREAM_AMOUNT_ZERO); let caller = get_caller_address(); + let start_time = get_block_timestamp(); // Use block timestamp for start time + assert(end_time > start_time, Errors::END_TIME_INVALID); // Assert end_time > start_time + let stream_key = (caller, to); assert(self.streams.read(stream_key).start_time == 0, Errors::STREAM_ALREADY_EXISTS); @@ -86,13 +91,20 @@ pub mod erc20_streaming { self.emit(StreamCreated { from: caller, to, total_amount, start_time, end_time }); } - fn release_tokens(ref self: ContractState, to: ContractAddress) { + fn release_tokens(ref self: ContractState, stream_id: u64) { let caller = get_caller_address(); let stream_key = (caller, to); - let stream = self.streams.read(stream_key); + let stream = self.streams.read(stream_id); + assert(caller == stream.to, Errors::STREAM_UNAUTHORIZED); + let releasable_amount = self.releasable_amount(stream); + assert( + releasable_amount <= (stream.total_amount - stream.released_amount), + "Releasable amount exceeds remaining tokens" + ); + self.streams.write( - stream_key, + stream_id, Stream { released_amount: stream.released_amount + releasable_amount, ..stream @@ -107,15 +119,13 @@ pub mod erc20_streaming { } fn releasable_amount(&self, stream: Stream) -> felt252 { - let current_time = starknet::get_block_timestamp(); - if current_time >= stream.end_time { - return stream.total_amount - stream.released_amount; - } else { + let current_time = starknet::get_block_timestamp(); let time_elapsed = current_time - stream.start_time; let vesting_duration = stream.end_time - stream.start_time; - let vested_amount = stream.total_amount * time_elapsed / vesting_duration; - return vested_amount - stream.released_amount; + + let vested_amount = stream.total_amount * min(time_elapsed, vesting_duration) / vesting_duration;; + vested_amount - stream.released_amount; } } } -} + diff --git a/src/applications/erc20.md b/src/applications/erc20.md index af0085ac..a4a4a402 100644 --- a/src/applications/erc20.md +++ b/src/applications/erc20.md @@ -1,20 +1,51 @@ # ERC20 Token -Contracts that follow the [ERC20 Standard](https://eips.ethereum.org/EIPS/eip-20) are called ERC20 tokens. They are used to represent fungible assets. +Contracts that follow the [ERC20 Standard](https://eips.ethereum.org/EIPS/eip-20) are called ERC20 tokens. They are used to represent fungible assets, and are fundamental in decentralized applications for representing tradable assets, such as currencies or utility tokens. To create an ERC20 contract, it must implement the following interface: -```rust +``` {{#include ../../listings/applications/erc20/src/token.cairo:interface}} ``` -In Starknet, function names should be written in _snake_case_. This is not the case in Solidity, where function names are written in _camelCase_. -The Starknet ERC20 interface is therefore slightly different from the Solidity ERC20 interface. +In Starknet, function names should be written in _snake_case_. This is not the case in Solidity, where function names are written in _camelCase_. As a result, the Starknet ERC20 interface is slightly different from the Solidity ERC20 interface, though it maintains the same core functionalities for minting, transferring, and approving tokens. + +### ERC20 Implementation in Cairo Here's an implementation of the ERC20 interface in Cairo: -```rust +``` {{#include ../../listings/applications/erc20/src/token.cairo:erc20}} ``` -There's several other implementations, such as the [Open Zeppelin](https://docs.openzeppelin.com/contracts-cairo/0.7.0/erc20) or the [Cairo By Example](https://cairo-by-example.com/examples/erc20/) ones. +The above implementation showcases the basic structure required for ERC20 tokens on Starknet. Starknet's native Cairo language enables handling token functionalities in a highly scalable and efficient manner, benefiting from Cairo's zero-knowledge architecture. + +## Token Streaming Extension + +In addition to basic ERC20 functionality, the contract can also be extended with additional features such as token streaming. Token streaming allows gradual distribution of tokens over time, making it suitable for vesting scenarios. + +This extension includes: + +1. **Setting Up Token Streams**: Defining a recipient and total amount, along with start and end times for the token distribution. +2. **Vesting Period Management**: Automatically calculates the vested amount of tokens based on time. +3. **Releasing Tokens**: Allows users to withdraw tokens as they become vested. + +Here is a basic function used to calculate the amount of tokens available for release: + +``` +fn releasable_amount(&self, stream: Stream) -> felt252 { + let current_time = starknet::get_block_timestamp(); + let time_elapsed = current_time - stream.start_time; + let vesting_duration = stream.end_time - stream.start_time; + let vested_amount = stream.total_amount * min(time_elapsed, vesting_duration) / vesting_duration; + vested_amount - stream.released_amount +} +``` + +This function dynamically calculates the amount of tokens that have vested and are available for release based on the elapsed time since the start of the stream. + +## Further Resources + +For other implementations and variations of ERC20, there are several notable libraries and examples: +- The [OpenZeppelin Cairo ERC20](https://docs.openzeppelin.com/contracts-cairo/0.7.0/erc20) library provides battle-tested contracts for ERC20 functionality in Starknet. +- The [Cairo By Example](https://cairo-by-example.com/examples/erc20/) repository offers detailed explanations and examples of ERC20 implementation in Cairo. From 3de89211882e0369a8d630ed01af5d4998216e42 Mon Sep 17 00:00:00 2001 From: Gift-Naomi Date: Fri, 4 Oct 2024 11:59:31 +0100 Subject: [PATCH 4/8] docs: add docs for erc20_streaming --- src/applications/advanced_factory.md | 2 +- src/applications/erc20.md | 43 ++++------------------------ src/applications/token_streaming.md | 22 ++++++++++++++ 3 files changed, 29 insertions(+), 38 deletions(-) create mode 100644 src/applications/token_streaming.md diff --git a/src/applications/advanced_factory.md b/src/applications/advanced_factory.md index 5d2a27fd..bc197d9b 100644 --- a/src/applications/advanced_factory.md +++ b/src/applications/advanced_factory.md @@ -9,5 +9,5 @@ Key Features - the factory only updates it's `Campaign` class hash and emits an event to notify any listeners, but the `Campaign` creators are in the end responsible for actually upgrading their contracts. ```rust -{{#include ../../listings/applications/advanced_factory/src/contract.cairo:contract}} +{{#include ../../listings/applications/crowdfunding/src/campaign.cairo:contract}} ``` diff --git a/src/applications/erc20.md b/src/applications/erc20.md index a4a4a402..b46ccae0 100644 --- a/src/applications/erc20.md +++ b/src/applications/erc20.md @@ -1,51 +1,20 @@ # ERC20 Token -Contracts that follow the [ERC20 Standard](https://eips.ethereum.org/EIPS/eip-20) are called ERC20 tokens. They are used to represent fungible assets, and are fundamental in decentralized applications for representing tradable assets, such as currencies or utility tokens. +Contracts that follow the [ERC20 Standard](https://eips.ethereum.org/EIPS/eip-20) are called ERC20 tokens. They are used to represent fungible assets. To create an ERC20 contract, it must implement the following interface: -``` +rust {{#include ../../listings/applications/erc20/src/token.cairo:interface}} -``` -In Starknet, function names should be written in _snake_case_. This is not the case in Solidity, where function names are written in _camelCase_. As a result, the Starknet ERC20 interface is slightly different from the Solidity ERC20 interface, though it maintains the same core functionalities for minting, transferring, and approving tokens. -### ERC20 Implementation in Cairo +In Starknet, function names should be written in _snake_case_. This is not the case in Solidity, where function names are written in _camelCase_. +The Starknet ERC20 interface is therefore slightly different from the Solidity ERC20 interface. Here's an implementation of the ERC20 interface in Cairo: -``` +rust {{#include ../../listings/applications/erc20/src/token.cairo:erc20}} -``` -The above implementation showcases the basic structure required for ERC20 tokens on Starknet. Starknet's native Cairo language enables handling token functionalities in a highly scalable and efficient manner, benefiting from Cairo's zero-knowledge architecture. -## Token Streaming Extension - -In addition to basic ERC20 functionality, the contract can also be extended with additional features such as token streaming. Token streaming allows gradual distribution of tokens over time, making it suitable for vesting scenarios. - -This extension includes: - -1. **Setting Up Token Streams**: Defining a recipient and total amount, along with start and end times for the token distribution. -2. **Vesting Period Management**: Automatically calculates the vested amount of tokens based on time. -3. **Releasing Tokens**: Allows users to withdraw tokens as they become vested. - -Here is a basic function used to calculate the amount of tokens available for release: - -``` -fn releasable_amount(&self, stream: Stream) -> felt252 { - let current_time = starknet::get_block_timestamp(); - let time_elapsed = current_time - stream.start_time; - let vesting_duration = stream.end_time - stream.start_time; - let vested_amount = stream.total_amount * min(time_elapsed, vesting_duration) / vesting_duration; - vested_amount - stream.released_amount -} -``` - -This function dynamically calculates the amount of tokens that have vested and are available for release based on the elapsed time since the start of the stream. - -## Further Resources - -For other implementations and variations of ERC20, there are several notable libraries and examples: -- The [OpenZeppelin Cairo ERC20](https://docs.openzeppelin.com/contracts-cairo/0.7.0/erc20) library provides battle-tested contracts for ERC20 functionality in Starknet. -- The [Cairo By Example](https://cairo-by-example.com/examples/erc20/) repository offers detailed explanations and examples of ERC20 implementation in Cairo. +There's several other implementations, such as the [Open Zeppelin](https://docs.openzeppelin.com/contracts-cairo/0.7.0/erc20) or the [Cairo By Example](https://cairo-by-example.com/examples/erc20/) ones. diff --git a/src/applications/token_streaming.md b/src/applications/token_streaming.md new file mode 100644 index 00000000..6f64d9cc --- /dev/null +++ b/src/applications/token_streaming.md @@ -0,0 +1,22 @@ +# Token Streaming + +_Last updated: 2024_07 Edition_ + +This provides an in-depth look at how ERC20 token streaming is implemented. + +## 1. Overview + +Token streaming is a mechanism where tokens are distributed gradually over a specified vesting period. This ensures that the recipient of the tokens cannot access the entire token balance upfront but will receive portions of it periodically. + +## 2. Core Features + +The smart contract includes the following features: +- **Setting up Token Streams**: Create a stream that specifies the recipient, total tokens, start time, and vesting duration. +- **Vesting Period Specification**: The contract calculates the vested amount over time based on the start and end time of the vesting period. +- **Token Release Management**: Only the vested amount can be released at any point, ensuring the tokens are gradually unlocked. + + +```rust +{{#include ..listings/applications/erc20/src/erc20_streaming.cairo}} + +``` From 3dd765fd8b3eb28cd80f25a6c081dd663ea65d97 Mon Sep 17 00:00:00 2001 From: Gift-Naomi Date: Fri, 4 Oct 2024 12:23:05 +0100 Subject: [PATCH 5/8] feat: update ERC20 Token Streaming Contract --- Scarb.lock | 3 +++ listings/applications/erc20/src/erc20_streaming.cairo | 8 ++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Scarb.lock b/Scarb.lock index 038b225c..c84915d1 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -138,6 +138,9 @@ version = "0.1.0" [[package]] name = "simple_vault" version = "0.1.0" +dependencies = [ + "erc20", +] [[package]] name = "snforge_std" diff --git a/listings/applications/erc20/src/erc20_streaming.cairo b/listings/applications/erc20/src/erc20_streaming.cairo index 48a9b0a9..704eff85 100644 --- a/listings/applications/erc20/src/erc20_streaming.cairo +++ b/listings/applications/erc20/src/erc20_streaming.cairo @@ -11,7 +11,7 @@ pub mod erc20_streaming { #[storage] struct Storage { streams: LegacyMap<(ContractAddress, ContractAddress), Stream>, - erc20_token: ContractAddress, + } #[derive(Copy, Drop, Debug, PartialEq)] @@ -21,6 +21,7 @@ pub mod erc20_streaming { total_amount: felt252, released_amount: felt252, to: ContractAddress, + erc20_token: ContractAddress, } #[event] @@ -49,6 +50,7 @@ pub mod erc20_streaming { pub const STREAM_AMOUNT_ZERO: felt252 = 'Stream amount cannot be zero'; pub const STREAM_ALREADY_EXISTS: felt252 = 'Stream already exists'; pub const END_TIME_INVALID: felt252 = 'End time must be greater than start time'; + pub const START_TIME_INVALID: felt252 = 'End time must be greater than start time'; pub const STREAM_UNAUTHORIZED: felt252 = 'Caller is not the recipient of the stream'; } @@ -63,12 +65,14 @@ pub mod erc20_streaming { ref self: ContractState, to: ContractAddress, total_amount: felt252, + start_time: u64, end_time: u64 ) { assert(total_amount != felt252::zero(), Errors::STREAM_AMOUNT_ZERO); let caller = get_caller_address(); let start_time = get_block_timestamp(); // Use block timestamp for start time assert(end_time > start_time, Errors::END_TIME_INVALID); // Assert end_time > start_time + assert(start_time >= get_block_timestamp(), Errors::START_TIME_INVALID); let stream_key = (caller, to); assert(self.streams.read(stream_key).start_time == 0, Errors::STREAM_ALREADY_EXISTS); @@ -90,7 +94,7 @@ pub mod erc20_streaming { fn release_tokens(ref self: ContractState, stream_id: u64) { let caller = get_caller_address(); - let stream_key = (caller, to); + let stream_id = (caller, to); let stream = self.streams.read(stream_id); assert(caller == stream.to, Errors::STREAM_UNAUTHORIZED); From 932c170d52547f8100d0890a4bd528bad944a5f7 Mon Sep 17 00:00:00 2001 From: Gift-Naomi Date: Fri, 4 Oct 2024 13:57:32 +0100 Subject: [PATCH 6/8] feat: nonce-based stream ID system --- .../applications/erc20/src/erc20_streaming.cairo | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/listings/applications/erc20/src/erc20_streaming.cairo b/listings/applications/erc20/src/erc20_streaming.cairo index 704eff85..bfa80e8b 100644 --- a/listings/applications/erc20/src/erc20_streaming.cairo +++ b/listings/applications/erc20/src/erc20_streaming.cairo @@ -11,11 +11,13 @@ pub mod erc20_streaming { #[storage] struct Storage { streams: LegacyMap<(ContractAddress, ContractAddress), Stream>, + next_stream_id: u64, } #[derive(Copy, Drop, Debug, PartialEq)] struct Stream { + from: ContractAddress, start_time: u64, end_time: u64, total_amount: felt252, @@ -50,13 +52,14 @@ pub mod erc20_streaming { pub const STREAM_AMOUNT_ZERO: felt252 = 'Stream amount cannot be zero'; pub const STREAM_ALREADY_EXISTS: felt252 = 'Stream already exists'; pub const END_TIME_INVALID: felt252 = 'End time must be greater than start time'; - pub const START_TIME_INVALID: felt252 = 'End time must be greater than start time'; + pub const START_TIME_INVALID: felt252 = ''; pub const STREAM_UNAUTHORIZED: felt252 = 'Caller is not the recipient of the stream'; } #[constructor] fn constructor(ref self: ContractState, erc20_token: ContractAddress) { self.erc20_token.write(erc20_token); + self.next_stream_id.write(1); } #[abi(embed_v0)] @@ -82,15 +85,19 @@ pub mod erc20_streaming { erc20.call("transfer_from", (caller, self.contract_address(), total_amount)); let stream = Stream { + from: caller; + to, start_time, end_time, total_amount, released_amount: felt252::zero(), }; - self.streams.write(stream_key, stream); + self.streams.write(stream_id, stream); - self.emit(StreamCreated { from: caller, to, total_amount, start_time, end_time }); + self.emit(StreamCreated { stream_id, from: caller, to, total_amount, start_time, end_time }); } + self.next_stream_id.write(stream_id + 1); // Increment the stream ID counter + } fn release_tokens(ref self: ContractState, stream_id: u64) { let caller = get_caller_address(); @@ -114,7 +121,7 @@ pub mod erc20_streaming { // Call the ERC20 contract to transfer tokens let erc20 = self.erc20_token.read(); - erc20.call("transfer", (to, releasable_amount)); + erc20.call("transfer", (stream.to, releasable_amount)); self.emit(TokensReleased { to, amount: releasable_amount }); } @@ -128,5 +135,4 @@ pub mod erc20_streaming { vested_amount - stream.released_amount; } } - } From 2d5f86c63d4410d27aa6bdf676d414560f164c26 Mon Sep 17 00:00:00 2001 From: Gift-Naomi Date: Fri, 4 Oct 2024 14:08:37 +0100 Subject: [PATCH 7/8] fix: correct error message --- listings/applications/erc20/src/erc20_streaming.cairo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/listings/applications/erc20/src/erc20_streaming.cairo b/listings/applications/erc20/src/erc20_streaming.cairo index bfa80e8b..3920347f 100644 --- a/listings/applications/erc20/src/erc20_streaming.cairo +++ b/listings/applications/erc20/src/erc20_streaming.cairo @@ -52,7 +52,7 @@ pub mod erc20_streaming { pub const STREAM_AMOUNT_ZERO: felt252 = 'Stream amount cannot be zero'; pub const STREAM_ALREADY_EXISTS: felt252 = 'Stream already exists'; pub const END_TIME_INVALID: felt252 = 'End time must be greater than start time'; - pub const START_TIME_INVALID: felt252 = ''; + pub const START_TIME_INVALID: felt252 = 'Start time must be greater than or equal to the current block timestamp';''; pub const STREAM_UNAUTHORIZED: felt252 = 'Caller is not the recipient of the stream'; } From b266d5abe1ad1dc7f59ed03d18e5b63c1247c6cc Mon Sep 17 00:00:00 2001 From: Gift-Naomi Date: Thu, 24 Oct 2024 14:16:11 +0100 Subject: [PATCH 8/8] fix: Correct ERC20 streaming contract and add basic test case --- Scarb.toml | 1 + .../erc20/src/erc20_streaming.cairo | 9 ++- .../erc20/tests/test_erc20_streaming.cairo | 55 +++++++++++++++++++ src/applications/token_streaming.md | 17 +----- starknet-foundry | 1 + 5 files changed, 64 insertions(+), 19 deletions(-) create mode 100644 listings/applications/erc20/tests/test_erc20_streaming.cairo create mode 160000 starknet-foundry diff --git a/Scarb.toml b/Scarb.toml index 8df8fcb2..02bd2661 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -12,6 +12,7 @@ test = "$(git rev-parse --show-toplevel)/scripts/test_resolver.sh" [workspace.tool.snforge] [workspace.dependencies] +snforge = { git = "https://github.com/foundry-rs/starknet-foundry" } starknet = ">=2.6.4" openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag="v0.14.0" } components = { path = "listings/applications/components" } diff --git a/listings/applications/erc20/src/erc20_streaming.cairo b/listings/applications/erc20/src/erc20_streaming.cairo index 3920347f..32f6c55c 100644 --- a/listings/applications/erc20/src/erc20_streaming.cairo +++ b/listings/applications/erc20/src/erc20_streaming.cairo @@ -10,7 +10,7 @@ pub mod erc20_streaming { #[storage] struct Storage { - streams: LegacyMap<(ContractAddress, ContractAddress), Stream>, + streams: LegacyMap, next_stream_id: u64, } @@ -77,9 +77,9 @@ pub mod erc20_streaming { assert(end_time > start_time, Errors::END_TIME_INVALID); // Assert end_time > start_time assert(start_time >= get_block_timestamp(), Errors::START_TIME_INVALID); - let stream_key = (caller, to); - assert(self.streams.read(stream_key).start_time == 0, Errors::STREAM_ALREADY_EXISTS); - + let stream_key = self.next_stream_id; + self.next_stream_id.write(stream_key + 1); + // Call the ERC20 contract to transfer tokens let erc20 = self.erc20_token.read(); erc20.call("transfer_from", (caller, self.contract_address(), total_amount)); @@ -101,7 +101,6 @@ pub mod erc20_streaming { fn release_tokens(ref self: ContractState, stream_id: u64) { let caller = get_caller_address(); - let stream_id = (caller, to); let stream = self.streams.read(stream_id); assert(caller == stream.to, Errors::STREAM_UNAUTHORIZED); diff --git a/listings/applications/erc20/tests/test_erc20_streaming.cairo b/listings/applications/erc20/tests/test_erc20_streaming.cairo new file mode 100644 index 00000000..41f11cd2 --- /dev/null +++ b/listings/applications/erc20/tests/test_erc20_streaming.cairo @@ -0,0 +1,55 @@ +#[cfg(test)] +mod tests { + use super::*; + use starknet::testing::{start_block, get_block_timestamp}; + use starknet::ContractAddress; + use core::num::traits::Zero; + use erc20_streaming::{Stream, Storage, Errors}; + + #[test] + fn test_create_stream() { + let erc20_token = ContractAddress::new(1); // Mock ERC20 token address + let mut contract = erc20_streaming::ContractState::new(erc20_token); + + let to = ContractAddress::new(2); + let total_amount = felt252::from(1000); + let start_time = get_block_timestamp(); + let end_time = start_time + 3600; // 1 hour later + contract.create_stream(to, total_amount, start_time, end_time); + + // Assert + let stream = contract.streams.read(1); // Stream ID should start from 1 + assert_eq!(stream.from, get_caller_address()); + assert_eq!(stream.to, to); + assert_eq!(stream.total_amount, total_amount); + assert_eq!(stream.released_amount, felt252::zero()); + assert_eq!(stream.start_time, start_time); + assert_eq!(stream.end_time, end_time); + + // Check if the next stream ID is incremented + assert_eq!(contract.next_stream_id.read(), 2); + } + + #[test] + fn test_release_tokens() { + let erc20_token = ContractAddress::new(1); + let mut contract = erc20_streaming::ContractState::new(erc20_token); + + // Create a stream + let to = ContractAddress::new(2); + let total_amount = felt252::from(1000); + let start_time = get_block_timestamp(); + let end_time = start_time + 3600; + contract.create_stream(to, total_amount, start_time, end_time); + + start_block(start_time + 1800); // 30 minutes later + + + contract.release_tokens(1); // Release tokens for stream ID 1 + + // Assert + let stream = contract.streams.read(1); + let expected_released_amount = felt252::from(500); // Half of the total amount should be released + assert_eq!(stream.released_amount, expected_released_amount); + } +} diff --git a/src/applications/token_streaming.md b/src/applications/token_streaming.md index 6f64d9cc..a1c82e9b 100644 --- a/src/applications/token_streaming.md +++ b/src/applications/token_streaming.md @@ -1,22 +1,11 @@ # Token Streaming -_Last updated: 2024_07 Edition_ - -This provides an in-depth look at how ERC20 token streaming is implemented. - -## 1. Overview - -Token streaming is a mechanism where tokens are distributed gradually over a specified vesting period. This ensures that the recipient of the tokens cannot access the entire token balance upfront but will receive portions of it periodically. - -## 2. Core Features - -The smart contract includes the following features: +Token streaming is a mechanism where tokens are distributed gradually over a specified vesting period. This ensures that the recipient of the tokens cannot access the entire token balance upfront but will receive portions of it gradually: - **Setting up Token Streams**: Create a stream that specifies the recipient, total tokens, start time, and vesting duration. - **Vesting Period Specification**: The contract calculates the vested amount over time based on the start and end time of the vesting period. -- **Token Release Management**: Only the vested amount can be released at any point, ensuring the tokens are gradually unlocked. +- **Token Release**: Only the vested amount can be released at any point, ensuring the tokens are gradually unlocked. - -```rust +```cairo {{#include ..listings/applications/erc20/src/erc20_streaming.cairo}} ``` diff --git a/starknet-foundry b/starknet-foundry new file mode 160000 index 00000000..0e214183 --- /dev/null +++ b/starknet-foundry @@ -0,0 +1 @@ +Subproject commit 0e214183672b37e6958ae7d32c209f18aeaa6fda