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
138 changes: 138 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,138 @@
#[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<(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,
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 = (caller, to);
assert(self.streams.read(stream_key).start_time == 0, Errors::STREAM_ALREADY_EXISTS);
julio4 marked this conversation as resolved.
Show resolved Hide resolved

// 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_id = (caller, to);
Mystic-Nayy marked this conversation as resolved.
Show resolved Hide resolved
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;
}
}

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.
22 changes: 22 additions & 0 deletions src/applications/token_streaming.md
Original file line number Diff line number Diff line change
@@ -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}}

```
Mystic-Nayy marked this conversation as resolved.
Show resolved Hide resolved