Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement erc20 streaming #237

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
7 changes: 7 additions & 0 deletions Scarb.lock
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,16 @@ dependencies = [
"openzeppelin",
]

[[package]]
name = "simple_storage"
version = "0.1.0"

[[package]]
name = "simple_vault"
version = "0.1.0"
dependencies = [
"erc20",
]

[[package]]
name = "snforge_std"
Expand Down
1 change: 1 addition & 0 deletions Scarb.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
137 changes: 137 additions & 0 deletions listings/applications/erc20/src/erc20_streaming.cairo
julio4 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
#[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::LegacyMap;

#[storage]
struct Storage {
streams: LegacyMap<u64, Stream>,
next_stream_id: u64,

}

#[derive(Copy, Drop, Debug, PartialEq)]
struct Stream {
from: ContractAddress,
start_time: u64,
end_time: u64,
total_amount: felt252,
released_amount: felt252,
to: ContractAddress,
erc20_token: ContractAddress,
}

#[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';
pub const END_TIME_INVALID: felt252 = 'End time must be greater than start time';
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';
}

#[constructor]
fn constructor(ref self: ContractState, erc20_token: ContractAddress) {
self.erc20_token.write(erc20_token);
self.next_stream_id.write(1);
}

#[abi(embed_v0)]
impl IStreamImpl of super::IStream<ContractState> {
fn create_stream(
ref self: ContractState,
to: ContractAddress,
total_amount: felt252,
start_time: u64,
Mystic-Nayy marked this conversation as resolved.
Show resolved Hide resolved
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 = 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));

let stream = Stream {
from: caller;
to,
start_time,
end_time,
total_amount,
released_amount: felt252::zero(),
};
self.streams.write(stream_id, stream);

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();
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_id,
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", (stream.to, releasable_amount));

self.emit(TokensReleased { to, amount: releasable_amount });
}

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;
}
}

55 changes: 55 additions & 0 deletions listings/applications/erc20/tests/test_erc20_streaming.cairo
Original file line number Diff line number Diff line change
@@ -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);
}
}
2 changes: 1 addition & 1 deletion src/applications/advanced_factory.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
```
8 changes: 4 additions & 4 deletions src/applications/erc20.md
Mystic-Nayy marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ Contracts that follow the [ERC20 Standard](https://eips.ethereum.org/EIPS/eip-20

To create an ERC20 contract, it must implement the following interface:

```rust
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.

Here's an implementation of the ERC20 interface in Cairo:

```rust
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.
11 changes: 11 additions & 0 deletions src/applications/token_streaming.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Token Streaming

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**: Only the vested amount can be released at any point, ensuring the tokens are gradually unlocked.

```cairo
{{#include ..listings/applications/erc20/src/erc20_streaming.cairo}}

```
1 change: 1 addition & 0 deletions starknet-foundry
Submodule starknet-foundry added at 0e2141