streamflow/
lib.rs

1// Copyright (c) 2021 Ivan Jelincic <parazyd@dyne.org>
2//
3// This file is part of streamflow-program
4// https://github.com/StreamFlow-Finance/streamflow-program
5//
6// This program is free software: you can redistribute it and/or modify
7// it under the terms of the GNU Affero General Public License version 3
8// as published by the Free Software Foundation.
9//
10// This program is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU Affero General Public License for more details.
14//
15// You should have received a copy of the GNU Affero General Public License
16// along with this program. If not, see <https://www.gnu.org/licenses/>.
17
18use std::convert::TryInto;
19use std::str::FromStr;
20
21use solana_program::{
22    account_info::{next_account_info, AccountInfo},
23    entrypoint,
24    entrypoint::ProgramResult,
25    msg,
26    native_token::lamports_to_sol,
27    program::invoke,
28    program_error::ProgramError,
29    pubkey::Pubkey,
30    system_instruction,
31    sysvar::{clock::Clock, fees::Fees, rent::Rent, Sysvar},
32};
33
34/// StreamFlow is the struct containing all our necessary metadata.
35#[repr(C)]
36pub struct StreamFlow {
37    /// Timestamp when the funds start unlocking
38    pub start_time: u64,
39    /// Timestamp when all funds should be unlocked
40    pub end_time: u64,
41    /// Amount of funds locked
42    pub amount: u64,
43    /// Amount of funds withdrawn
44    pub withdrawn: u64,
45    /// Pubkey of the program initializer
46    pub sender: [u8; 32],
47    /// Pubkey of the funds' recipient
48    pub recipient: [u8; 32],
49}
50
51/// Serialize any to u8 slice.
52/// # Safety
53///
54/// :)
55pub unsafe fn any_as_u8_slice<T: Sized>(p: &T) -> &[u8] {
56    ::std::slice::from_raw_parts((p as *const T) as *const u8, ::std::mem::size_of::<T>())
57}
58
59/// Deserialize instruction_data into StreamFlow struct.
60/// This is used to read instructions given to us by the program's initializer.
61pub fn unpack_init_instruction(ix: &[u8], alice: &Pubkey, bob: &Pubkey) -> StreamFlow {
62    StreamFlow {
63        start_time: u64::from(u32::from_le_bytes(ix[1..5].try_into().unwrap())),
64        end_time: u64::from(u32::from_le_bytes(ix[5..9].try_into().unwrap())),
65        amount: u64::from_le_bytes(ix[9..17].try_into().unwrap()),
66        withdrawn: 0,
67        sender: alice.to_bytes(),
68        recipient: bob.to_bytes(),
69    }
70}
71
72/// Deserialize account data into StreamFlow struct.
73/// This is used for reading the metadata from the account holding the locked funds.
74pub fn unpack_account_data(ix: &[u8]) -> StreamFlow {
75    StreamFlow {
76        start_time: u64::from_le_bytes(ix[0..8].try_into().unwrap()),
77        end_time: u64::from_le_bytes(ix[8..16].try_into().unwrap()),
78        amount: u64::from_le_bytes(ix[16..24].try_into().unwrap()),
79        withdrawn: u64::from_le_bytes(ix[24..32].try_into().unwrap()),
80        sender: ix[32..64].try_into().unwrap(),
81        recipient: ix[64..96].try_into().unwrap(),
82    }
83}
84
85fn calculate_streamed(now: u64, start: u64, end: u64, amount: u64) -> u64 {
86    // This is valid float division, but we lose precision when going u64.
87    // The loss however should not matter, as in the end we will simply
88    // send everything that is remaining.
89    (((now - start) as f64) / ((end - start) as f64) * amount as f64) as u64
90}
91
92fn initialize_stream(pid: &Pubkey, accounts: &[AccountInfo], ix: &[u8]) -> ProgramResult {
93    msg!("Requested stream initialization");
94    let account_info_iter = &mut accounts.iter();
95    let alice = next_account_info(account_info_iter)?;
96    let bob = next_account_info(account_info_iter)?;
97    let pda = next_account_info(account_info_iter)?;
98    let system_program = next_account_info(account_info_iter)?;
99
100    if ix.len() != 17 {
101        return Err(ProgramError::InvalidInstructionData);
102    }
103
104    if !pda.data_is_empty() {
105        return Err(ProgramError::AccountAlreadyInitialized);
106    }
107
108    if !alice.is_writable
109        || !bob.is_writable
110        || !pda.is_writable
111        || !alice.is_signer
112        || !pda.is_signer
113    {
114        return Err(ProgramError::MissingRequiredSignature);
115    }
116
117    let mut sf = unpack_init_instruction(ix, alice.key, bob.key);
118    let struct_size = std::mem::size_of::<StreamFlow>();
119
120    // We also transfer enough to be rent-exempt (about 0.00156 SOL) to the
121    // new account. After all funds are withdrawn and unlocked, this might
122    // be returned to the initializer or put in another pool for future reuse.
123    let cluster_rent = Rent::get()?;
124    if alice.lamports() < sf.amount + cluster_rent.minimum_balance(struct_size) {
125        msg!("Not enough funds in sender's account to initialize stream");
126        return Err(ProgramError::InsufficientFunds);
127    }
128
129    let now = Clock::get()?.unix_timestamp as u64;
130    if sf.start_time < now || sf.start_time >= sf.end_time {
131        msg!("Timestamps are invalid!");
132        msg!("Solana cluster time: {}", now);
133        msg!("Stream start time:   {}", sf.start_time);
134        msg!("Stream end time:     {}", sf.end_time);
135        msg!("Stream duration:     {}", sf.end_time - sf.start_time);
136        return Err(ProgramError::InvalidArgument);
137    }
138
139    // Create the account holding locked funds and data
140    invoke(
141        &system_instruction::create_account(
142            &alice.key,
143            &pda.key,
144            sf.amount + cluster_rent.minimum_balance(struct_size),
145            struct_size as u64,
146            &pid,
147        ),
148        &[alice.clone(), pda.clone(), system_program.clone()],
149    )?;
150
151    // Send enough for one transaction to Bob, so Bob can do an initial
152    // withdraw without having previous funds on their account.
153    let fees = Fees::get()?;
154    **pda.try_borrow_mut_lamports()? -= fees.fee_calculator.lamports_per_signature * 2;
155    **bob.try_borrow_mut_lamports()? += fees.fee_calculator.lamports_per_signature * 2;
156    sf.withdrawn += fees.fee_calculator.lamports_per_signature * 2;
157
158    // Write our metadata to pda's data.
159    let mut data = pda.try_borrow_mut_data()?;
160    let bytes: &[u8] = unsafe { any_as_u8_slice(&sf) };
161    data[0..bytes.len()].clone_from_slice(bytes);
162
163    msg!(
164        "Successfully initialized {} SOL ({} lamports) stream for: {}",
165        lamports_to_sol(sf.amount),
166        sf.amount,
167        bob.key
168    );
169    msg!("Called by account: {}", alice.key);
170    msg!("Funds locked in account: {}", pda.key);
171    msg!("Stream duration: {} seconds", sf.end_time - sf.start_time);
172
173    Ok(())
174}
175
176fn withdraw_unlocked(pid: &Pubkey, accounts: &[AccountInfo], ix: &[u8]) -> ProgramResult {
177    msg!("Requested withdraw of unlocked funds");
178    let account_info_iter = &mut accounts.iter();
179    let bob = next_account_info(account_info_iter)?;
180    let pda = next_account_info(account_info_iter)?;
181    let lld = next_account_info(account_info_iter)?;
182
183    if ix.len() != 9 {
184        return Err(ProgramError::InvalidInstructionData);
185    }
186
187    // Hardcoded rent collector
188    let rent_reaper = Pubkey::from_str("DrFtxPb9F6SxpHHHFiEtSNXE3SZCUNLXMaHS6r8pkoz2").unwrap();
189    if lld.key != &rent_reaper {
190        msg!("Got unexpected rent collection account");
191        return Err(ProgramError::InvalidAccountData);
192    }
193
194    if !bob.is_signer || !bob.is_writable || !pda.is_writable || !lld.is_writable {
195        return Err(ProgramError::MissingRequiredSignature);
196    }
197
198    if pda.data_is_empty() || pda.owner != pid {
199        return Err(ProgramError::UninitializedAccount);
200    }
201
202    let mut data = pda.try_borrow_mut_data()?;
203    let mut sf = unpack_account_data(&data);
204
205    if bob.key.to_bytes() != sf.recipient {
206        msg!("This stream isn't indented for {}", bob.key);
207        return Err(ProgramError::MissingRequiredSignature);
208    }
209
210    // Current cluster time used to calculate unlocked amount.
211    let now = Clock::get()?.unix_timestamp as u64;
212
213    let amount_unlocked = calculate_streamed(now, sf.start_time, sf.end_time, sf.amount);
214    let mut available = amount_unlocked - sf.withdrawn;
215
216    // In case we're past the set time, everything is available.
217    if now >= sf.end_time {
218        available = sf.amount - sf.withdrawn;
219    }
220
221    let mut requested = u64::from_le_bytes(ix[1..9].try_into().unwrap());
222    if requested == 0 {
223        requested = available;
224    }
225
226    if requested > available {
227        msg!("Amount requested for withdraw is larger than what is available.");
228        msg!(
229            "Requested: {} SOL ({} lamports)",
230            lamports_to_sol(requested),
231            requested
232        );
233        msg!(
234            "Available: {} SOL ({} lamports)",
235            lamports_to_sol(available),
236            available
237        );
238        return Err(ProgramError::InvalidArgument);
239    }
240
241    **pda.try_borrow_mut_lamports()? -= requested;
242    **bob.try_borrow_mut_lamports()? += requested;
243
244    // Update account data
245    sf.withdrawn += available as u64;
246    let bytes: &[u8] = unsafe { any_as_u8_slice(&sf) };
247    data[0..bytes.len()].clone_from_slice(bytes);
248
249    msg!(
250        "Successfully withdrawn: {} SOL ({} lamports)",
251        lamports_to_sol(available),
252        available
253    );
254    msg!(
255        "Remaining: {} SOL ({} lamports)",
256        lamports_to_sol(sf.amount - sf.withdrawn),
257        sf.amount - sf.withdrawn
258    );
259
260    /*
261    if sf.withdrawn == sf.amount {
262        // Collect rent after stream is finished.
263        let rent = pda.lamports();
264        **pda.try_borrow_mut_lamports()? -= rent;
265        **lld.try_borrow_mut_lamports()? += rent;
266    }
267    */
268
269    Ok(())
270}
271
272fn cancel_stream(pid: &Pubkey, accounts: &[AccountInfo], _ix: &[u8]) -> ProgramResult {
273    msg!("Requested stream cancellation");
274    let account_info_iter = &mut accounts.iter();
275    let alice = next_account_info(account_info_iter)?;
276    let bob = next_account_info(account_info_iter)?;
277    let pda = next_account_info(account_info_iter)?;
278
279    if !alice.is_signer || !alice.is_writable || !bob.is_writable || !pda.is_writable {
280        return Err(ProgramError::MissingRequiredSignature);
281    }
282
283    if pda.data_is_empty() || pda.owner != pid {
284        return Err(ProgramError::UninitializedAccount);
285    }
286
287    let data = pda.try_borrow_data()?;
288    let sf = unpack_account_data(&data);
289
290    if alice.key.to_bytes() != sf.sender {
291        msg!("Unauthorized to withdraw for {}", alice.key);
292        return Err(ProgramError::MissingRequiredSignature);
293    }
294
295    if bob.key.to_bytes() != sf.recipient {
296        msg!("This stream isn't intended for {}", bob.key);
297        return Err(ProgramError::MissingRequiredSignature);
298    }
299
300    // Current cluster time used to calculate unlocked amount.
301    let now = Clock::get()?.unix_timestamp as u64;
302
303    // Transfer what was unlocked but not withdrawn to Bob.
304    let amount_unlocked = calculate_streamed(now, sf.start_time, sf.end_time, sf.amount);
305    let available = amount_unlocked - sf.withdrawn;
306    **pda.try_borrow_mut_lamports()? -= available;
307    **bob.try_borrow_mut_lamports()? += available;
308
309    // Alice decides to cancel, and withdraws from the derived account,
310    // resulting in its purge.
311    let remains = pda.lamports();
312    **pda.try_borrow_mut_lamports()? -= remains;
313    **alice.try_borrow_mut_lamports()? += remains;
314
315    msg!("Successfully cancelled stream on {} ", pda.key);
316    msg!(
317        "Transferred unlocked {} SOL ({} lamports to {}",
318        lamports_to_sol(available),
319        available,
320        bob.key
321    );
322    msg!(
323        "Returned {} SOL ({} lamports) to {}",
324        lamports_to_sol(remains),
325        remains,
326        alice.key
327    );
328
329    Ok(())
330}
331
332entrypoint!(process_instruction);
333/// The program entrypoint
334pub fn process_instruction(
335    program_id: &Pubkey,
336    accounts: &[AccountInfo],
337    instruction_data: &[u8],
338) -> ProgramResult {
339    msg!(
340        "StreamFlowFinance v{}.{}.{}",
341        env!("CARGO_PKG_VERSION_MAJOR"),
342        env!("CARGO_PKG_VERSION_MINOR"),
343        env!("CARGO_PKG_VERSION_PATCH")
344    );
345
346    match instruction_data[0] {
347        0 => initialize_stream(program_id, accounts, instruction_data),
348        1 => withdraw_unlocked(program_id, accounts, instruction_data),
349        2 => cancel_stream(program_id, accounts, instruction_data),
350        _ => Err(ProgramError::InvalidArgument),
351    }
352}