diff --git a/api/src/consts.rs b/api/src/consts.rs index 46f51b2e..5de19e6b 100644 --- a/api/src/consts.rs +++ b/api/src/consts.rs @@ -33,6 +33,9 @@ pub const ONE_ORE: u64 = 10u64.pow(TOKEN_DECIMALS as u32); /// The duration of one minute, in seconds. pub const ONE_MINUTE: i64 = 60; +/// The duration of one day, in seconds. +pub const ONE_DAY: i64 = 86400; + /// The number of minutes in a program epoch. pub const EPOCH_MINUTES: i64 = 15; @@ -67,6 +70,9 @@ pub const PROOF: &[u8] = b"proof"; /// The seed of the treasury account PDA. pub const TREASURY: &[u8] = b"treasury"; +/// The seed of the vesting account PDA. +pub const VESTING: &[u8] = b"vesting"; + /// Noise for deriving the mint pda pub const MINT_NOISE: [u8; 16] = [ 89, 157, 88, 232, 243, 249, 197, 132, 199, 49, 19, 234, 91, 94, 150, 41, diff --git a/api/src/sdk.rs b/api/src/sdk.rs index a00d51dd..200ac286 100644 --- a/api/src/sdk.rs +++ b/api/src/sdk.rs @@ -4,7 +4,7 @@ use steel::*; use crate::{ consts::*, instruction::*, - state::{bus_pda, config_pda, proof_pda, treasury_pda}, + state::{bus_pda, config_pda, proof_pda, treasury_pda, vesting_pda}, }; /// Builds an auth instruction. @@ -19,6 +19,7 @@ pub fn auth(proof: Pubkey) -> Instruction { /// Builds a claim instruction. pub fn claim(signer: Pubkey, beneficiary: Pubkey, amount: u64) -> Instruction { let proof = proof_pda(signer).0; + let vesting = vesting_pda(proof).0; Instruction { program_id: crate::ID, accounts: vec![ @@ -27,6 +28,7 @@ pub fn claim(signer: Pubkey, beneficiary: Pubkey, amount: u64) -> Instruction { AccountMeta::new(proof, false), AccountMeta::new_readonly(TREASURY_ADDRESS, false), AccountMeta::new(TREASURY_TOKENS_ADDRESS, false), + AccountMeta::new(vesting, false), AccountMeta::new_readonly(spl_token::ID, false), ], data: Claim { diff --git a/api/src/state/mod.rs b/api/src/state/mod.rs index 20baa131..85ffb09d 100644 --- a/api/src/state/mod.rs +++ b/api/src/state/mod.rs @@ -2,11 +2,13 @@ mod bus; mod config; mod proof; mod treasury; +mod vesting; pub use bus::*; pub use config::*; pub use proof::*; pub use treasury::*; +pub use vesting::*; use steel::*; @@ -19,6 +21,7 @@ pub enum OreAccount { Config = 101, Proof = 102, Treasury = 103, + Vesting = 104, } /// Fetch the PDA of a bus account. @@ -40,3 +43,8 @@ pub fn proof_pda(authority: Pubkey) -> (Pubkey, u8) { pub fn treasury_pda() -> (Pubkey, u8) { Pubkey::find_program_address(&[TREASURY], &crate::id()) } + +/// Derive the PDA of a vesting account. +pub fn vesting_pda(proof: Pubkey) -> (Pubkey, u8) { + Pubkey::find_program_address(&[VESTING, proof.as_ref()], &crate::id()) +} diff --git a/api/src/state/vesting.rs b/api/src/state/vesting.rs new file mode 100644 index 00000000..77083089 --- /dev/null +++ b/api/src/state/vesting.rs @@ -0,0 +1,23 @@ +use steel::*; + +use super::OreAccount; + +/// Vesting accounts track a miner's current vesting schedule. +/// Miners are allowed to claim 1% of their earnings every 24 hours. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] +pub struct Vesting { + /// The proof assocaited with this vesting schedule. + pub proof: Pubkey, + + /// The amount of tokens that have been claimed in the current window. + pub window_claim_amount: u64, + + /// The high water mark of the proof balance. + pub window_proof_balance: u64, + + /// The start of the vesting window. + pub window_start_at: i64, +} + +account!(OreAccount, Vesting); diff --git a/program/src/claim.rs b/program/src/claim.rs index 29965f8d..3c69cc75 100644 --- a/program/src/claim.rs +++ b/program/src/claim.rs @@ -9,7 +9,7 @@ pub fn process_claim(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult // Load accounts. let clock = Clock::get()?; - let [signer_info, beneficiary_info, proof_info, treasury_info, treasury_tokens_info, token_program] = + let [signer_info, beneficiary_info, proof_info, treasury_info, treasury_tokens_info, vesting_info, token_program, system_program] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); @@ -27,12 +27,67 @@ pub fn process_claim(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult )?; treasury_info.is_treasury()?; treasury_tokens_info.is_writable()?.is_treasury_tokens()?; + system_program.is_program(&system_program::ID)?; token_program.is_program(&spl_token::ID)?; + // Create vesting account if it doesn't exist. + let vesting = if vesting_info.data_is_empty() { + // Verify seeds + vesting_info + .is_empty()? + .is_writable()? + .has_seeds(&[VESTING, proof_info.key.as_ref()], &ore_api::ID)?; + + // Initialize vesting. + create_program_account::( + vesting_info, + system_program, + signer_info, + &ore_api::ID, + &[VESTING, proof_info.key.as_ref()], + )?; + let vesting = vesting_info + .as_account_mut::(&ore_api::ID)? + .assert_mut(|v| v.proof == *proof_info.key)?; + vesting.proof = *proof_info.key; + vesting.window_claim_amount = 0; + vesting.window_proof_balance = proof.balance; + vesting.window_start_at = clock.unix_timestamp; + vesting + } else { + // Load vesting account. + vesting_info + .as_account_mut::(&ore_api::ID)? + .assert_mut(|v| v.proof == *proof_info.key)? + }; + + // Update vesting window. + if clock.unix_timestamp > vesting.window_start_at + ONE_DAY { + vesting.window_claim_amount = 0; + vesting.window_start_at = clock.unix_timestamp; + }; + + // Update the high water mark. + vesting.window_proof_balance = vesting.window_proof_balance.max(proof.balance); + + // Calculate claim amount. + let max_claim_amount = vesting.window_proof_balance.checked_div(100).unwrap(); + let remaining_claim_amount = max_claim_amount + .checked_sub(vesting.window_claim_amount) + .unwrap(); + + // Exempt boost proof from vesting. + let boost_config_address = ore_boost_api::state::config_pda().0; + let claim_amount = if proof.authority == boost_config_address { + amount.min(proof.balance) + } else { + amount.min(remaining_claim_amount).min(proof.balance) + }; + // Update miner balance. proof.balance = proof .balance - .checked_sub(amount) + .checked_sub(claim_amount) .ok_or(OreError::ClaimTooLarge)?; // Update last claim timestamp. @@ -44,7 +99,7 @@ pub fn process_claim(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult treasury_tokens_info, beneficiary_info, token_program, - amount, + claim_amount, &[TREASURY], )?;