diff --git a/Anchor.toml b/Anchor.toml index 42a4a8e..0c3f49a 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -20,6 +20,9 @@ wallet = "packages/tests/fixtures/provider.json" [scripts] test = "packages/tests/run.sh" +[test] +startup_wait = 3000 + [test.validator] slots_per_epoch = "32" #url = "https://api.mainnet-beta.solana.com" diff --git a/Cargo.lock b/Cargo.lock index c395242..fa87a2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2272,10 +2272,20 @@ version = "0.1.0" dependencies = [ "anchor-lang", "anchor-spl", + "marinade-common", "marinade-cpi", "sunrise-core", ] +[[package]] +name = "marinade-common" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "marinade-cpi", + "num-traits", +] + [[package]] name = "marinade-cpi" version = "0.4.0" @@ -2291,6 +2301,7 @@ version = "0.1.0" dependencies = [ "anchor-lang", "anchor-spl", + "marinade-common", "marinade-cpi", "sunrise-core", ] diff --git a/lib/marinade-common/Cargo.toml b/lib/marinade-common/Cargo.toml new file mode 100644 index 0000000..51d262a --- /dev/null +++ b/lib/marinade-common/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "marinade-common" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anchor-lang = '0.29.0' +marinade-cpi = { git = "https://github.com/sunrise-stake/anchor-gen", branch = "update/anchor-v0.29" } +num-traits = "0.2.17" \ No newline at end of file diff --git a/lib/marinade-common/src/lib.rs b/lib/marinade-common/src/lib.rs new file mode 100644 index 0000000..3633a1d --- /dev/null +++ b/lib/marinade-common/src/lib.rs @@ -0,0 +1,95 @@ +pub mod vault_authority_seed; + +use marinade_cpi::state::State as MarinadeState; +use num_traits::{NumCast, PrimInt}; +use std::fmt::Debug; +use std::ops::{Div, Mul}; + +/// calculate amount*numerator/denominator +/// as value = shares * share_price where share_price=total_value/total_shares +/// or shares = amount_value / share_price where share_price=total_value/total_shares +/// => shares = amount_value * 1/share_price where 1/share_price=total_shares/total_value +pub fn proportional(amount: T, numerator: T, denominator: T) -> T +where + T: PrimInt + Mul + Div + TryInto, + >::Error: Debug, +{ + proportional_with_rounding(amount, numerator, denominator, RoundingMode::Down) +} + +pub enum RoundingMode { + Up, + Down, +} + +pub fn proportional_with_rounding( + amount: T, + numerator: T, + denominator: T, + rounding_mode: RoundingMode, +) -> T +where + T: PrimInt + Mul + Div + TryInto, + >::Error: Debug, +{ + if denominator == T::zero() { + return amount; + } + + let amount_i128: i128 = amount.try_into().unwrap(); + let numerator_i128: i128 = numerator.try_into().unwrap(); + let denominator_i128: i128 = denominator.try_into().unwrap(); + + match rounding_mode { + RoundingMode::Up => { + // Round up by adding (denominator - 1) before dividing + let result = (amount_i128 * numerator_i128 + (denominator_i128 - 1)) / denominator_i128; + ::from(result).unwrap() + } + RoundingMode::Down => { + // Default behavior (round down) + let result = amount_i128 * numerator_i128 / denominator_i128; + ::from(result).unwrap() + } + } +} + +// All lifted from https://github.com/marinade-finance/liquid-staking-program/blob/447f9607a8c755cac7ad63223febf047142c6c8f/programs/marinade-finance/src/state.rs#L227 +pub fn calc_msol_from_lamports(marinade_state: &MarinadeState, stake_lamports: u64) -> u64 { + proportional( + stake_lamports, + marinade_state.msol_supply, + total_virtual_staked_lamports(marinade_state), + ) +} +pub fn calc_lamports_from_msol_amount(marinade_state: &MarinadeState, msol_amount: u64) -> u64 { + proportional( + msol_amount, + total_virtual_staked_lamports(marinade_state), + marinade_state.msol_supply, + ) +} + +fn total_cooling_down(marinade_state: &MarinadeState) -> u64 { + marinade_state + .stake_system + .delayed_unstake_cooling_down + .checked_add(marinade_state.emergency_cooling_down) + .expect("Total cooling down overflow") +} + +fn total_lamports_under_control(marinade_state: &MarinadeState) -> u64 { + marinade_state + .validator_system + .total_active_balance + .checked_add(total_cooling_down(marinade_state)) + .expect("Stake balance overflow") + .checked_add(marinade_state.available_reserve_balance) // reserve_pda.lamports() - self.rent_exempt_for_token_acc + .expect("Total SOLs under control overflow") +} + +fn total_virtual_staked_lamports(marinade_state: &MarinadeState) -> u64 { + // if we get slashed it may be negative but we must use 0 instead + total_lamports_under_control(marinade_state) + .saturating_sub(marinade_state.circulating_ticket_balance) //tickets created -> cooling down lamports or lamports already in reserve and not claimed yet +} diff --git a/lib/marinade-common/src/vault_authority_seed.rs b/lib/marinade-common/src/vault_authority_seed.rs new file mode 100644 index 0000000..b8b0aa6 --- /dev/null +++ b/lib/marinade-common/src/vault_authority_seed.rs @@ -0,0 +1,33 @@ +use anchor_lang::prelude::{Account, Key}; +use anchor_lang::{AccountDeserialize, AccountSerialize}; + +// NOTE: this must match the constant used by the programs themselves +const VAULT_AUTHORITY: &[u8] = b"vault-authority"; + +pub struct VaultAuthoritySeed<'a> { + state_address: Vec, + vault_authority: &'a [u8], + bump: Vec, +} + +pub trait HasVaultAuthority: AccountSerialize + AccountDeserialize + Clone { + fn vault_authority_bump(&self) -> u8; +} + +impl<'a> VaultAuthoritySeed<'a> { + pub fn new<'info>(state: &'a Account<'info, impl HasVaultAuthority>) -> Self { + let state_address = state.key().to_bytes().to_vec(); + let vault_authority = VAULT_AUTHORITY; + let bump = vec![state.vault_authority_bump()]; + + VaultAuthoritySeed { + state_address, + vault_authority, + bump, + } + } + + pub fn as_slices(&self) -> [&[u8]; 3] { + [&self.state_address, self.vault_authority, &self.bump] + } +} diff --git a/package.json b/package.json index 85cd709..3c9da39 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "prettier": "^3.0.3", "ts-node": "^10.9.1", "turbo": "^1.10.16", - "typedoc": "^0.25.1", + "typedoc": "^0.25.8", "typescript": "^5.3.2" } } diff --git a/packages/sdks/common/src/types/marinade_beam.ts b/packages/sdks/common/src/types/marinade_beam.ts index 3b41d32..992e909 100644 --- a/packages/sdks/common/src/types/marinade_beam.ts +++ b/packages/sdks/common/src/types/marinade_beam.ts @@ -132,7 +132,7 @@ export type MarinadeBeam = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -251,7 +251,7 @@ export type MarinadeBeam = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -385,7 +385,7 @@ export type MarinadeBeam = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -469,7 +469,7 @@ export type MarinadeBeam = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -573,7 +573,7 @@ export type MarinadeBeam = { ] }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -647,7 +647,7 @@ export type MarinadeBeam = { "args": [] }, { - "name": "initEpochReport", + "name": "extractYield", "accounts": [ { "name": "state", @@ -656,12 +656,12 @@ export type MarinadeBeam = { }, { "name": "sunriseState", - "isMut": false, + "isMut": true, "isSigner": false }, { "name": "marinadeState", - "isMut": false, + "isMut": true, "isSigner": false }, { @@ -669,16 +669,6 @@ export type MarinadeBeam = { "isMut": true, "isSigner": true }, - { - "name": "updateAuthority", - "isMut": false, - "isSigner": true - }, - { - "name": "epochReportAccount", - "isMut": true, - "isSigner": false - }, { "name": "msolMint", "isMut": true, @@ -695,68 +685,47 @@ export type MarinadeBeam = { "isSigner": false }, { - "name": "clock", - "isMut": false, - "isSigner": false - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "extractedYield", - "type": "u64" - } - ] - }, - { - "name": "updateEpochReport", - "accounts": [ - { - "name": "state", - "isMut": false, + "name": "yieldAccount", + "isMut": true, "isSigner": false }, { - "name": "sunriseState", - "isMut": false, + "name": "liqPoolSolLegPda", + "isMut": true, "isSigner": false }, { - "name": "marinadeState", - "isMut": false, + "name": "liqPoolMsolLeg", + "isMut": true, "isSigner": false }, { - "name": "payer", + "name": "treasuryMsolAccount", "isMut": true, - "isSigner": true + "isSigner": false }, { - "name": "epochReportAccount", - "isMut": true, + "name": "sysvarInstructions", + "isMut": false, "isSigner": false }, { - "name": "msolMint", - "isMut": true, + "name": "sunriseProgram", + "isMut": false, "isSigner": false }, { - "name": "msolVault", - "isMut": true, + "name": "marinadeProgram", + "isMut": false, "isSigner": false }, { - "name": "vaultAuthority", + "name": "systemProgram", "isMut": false, "isSigner": false }, { - "name": "clock", + "name": "tokenProgram", "isMut": false, "isSigner": false } @@ -764,7 +733,7 @@ export type MarinadeBeam = { "args": [] }, { - "name": "extractYield", + "name": "updateEpochReport", "accounts": [ { "name": "state", @@ -773,27 +742,22 @@ export type MarinadeBeam = { }, { "name": "sunriseState", - "isMut": false, + "isMut": true, "isSigner": false }, { "name": "marinadeState", - "isMut": true, + "isMut": false, "isSigner": false }, - { - "name": "payer", - "isMut": true, - "isSigner": true - }, { "name": "msolMint", - "isMut": true, + "isMut": false, "isSigner": false }, { "name": "msolVault", - "isMut": true, + "isMut": false, "isSigner": false }, { @@ -802,17 +766,21 @@ export type MarinadeBeam = { "isSigner": false }, { - "name": "yieldAccount", - "isMut": true, - "isSigner": false + "name": "gsolMint", + "isMut": false, + "isSigner": false, + "docs": [ + "Required to update the core state epoch report", + "Verified in CPI to Sunrise program." + ] }, { - "name": "epochReportAccount", - "isMut": true, + "name": "sysvarInstructions", + "isMut": false, "isSigner": false }, { - "name": "clock", + "name": "sunriseProgram", "isMut": false, "isSigner": false } @@ -880,42 +848,6 @@ export type MarinadeBeam = { } ] } - }, - { - "name": "epochReport", - "type": { - "kind": "struct", - "fields": [ - { - "name": "state", - "type": "publicKey" - }, - { - "name": "epoch", - "type": "u64" - }, - { - "name": "tickets", - "type": "u64" - }, - { - "name": "totalOrderedLamports", - "type": "u64" - }, - { - "name": "extractableYield", - "type": "u64" - }, - { - "name": "extractedYield", - "type": "u64" - }, - { - "name": "bump", - "type": "u8" - } - ] - } } ], "types": [ @@ -952,26 +884,21 @@ export type MarinadeBeam = { }, { "code": 6001, - "name": "CalculationFailure", - "msg": "An error occurred during calculation" - }, - { - "code": 6002, "name": "InvalidEpochReportAccount", "msg": "The epoch report account has not been updated to the current epoch yet" }, { - "code": 6003, + "code": 6002, "name": "RemainingUnclaimableTicketAmount", "msg": "The total ordered ticket amount exceeds the amount in all found tickets" }, { - "code": 6004, + "code": 6003, "name": "DelayedUnstakeTicketsNotYetClaimable", "msg": "Delayed unstake tickets for the current epoch can not yet be claimed" }, { - "code": 6005, + "code": 6004, "name": "TooManyTicketsClaimed", "msg": "The amount of delayed unstake tickets requested to be recovered exceeds the amount in the report" } @@ -1112,7 +1039,7 @@ export const IDL: MarinadeBeam = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -1231,7 +1158,7 @@ export const IDL: MarinadeBeam = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -1365,7 +1292,7 @@ export const IDL: MarinadeBeam = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -1449,7 +1376,7 @@ export const IDL: MarinadeBeam = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -1553,7 +1480,7 @@ export const IDL: MarinadeBeam = { ] }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -1627,7 +1554,7 @@ export const IDL: MarinadeBeam = { "args": [] }, { - "name": "initEpochReport", + "name": "extractYield", "accounts": [ { "name": "state", @@ -1636,12 +1563,12 @@ export const IDL: MarinadeBeam = { }, { "name": "sunriseState", - "isMut": false, + "isMut": true, "isSigner": false }, { "name": "marinadeState", - "isMut": false, + "isMut": true, "isSigner": false }, { @@ -1649,16 +1576,6 @@ export const IDL: MarinadeBeam = { "isMut": true, "isSigner": true }, - { - "name": "updateAuthority", - "isMut": false, - "isSigner": true - }, - { - "name": "epochReportAccount", - "isMut": true, - "isSigner": false - }, { "name": "msolMint", "isMut": true, @@ -1675,68 +1592,47 @@ export const IDL: MarinadeBeam = { "isSigner": false }, { - "name": "clock", - "isMut": false, - "isSigner": false - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "extractedYield", - "type": "u64" - } - ] - }, - { - "name": "updateEpochReport", - "accounts": [ - { - "name": "state", - "isMut": false, + "name": "yieldAccount", + "isMut": true, "isSigner": false }, { - "name": "sunriseState", - "isMut": false, + "name": "liqPoolSolLegPda", + "isMut": true, "isSigner": false }, { - "name": "marinadeState", - "isMut": false, + "name": "liqPoolMsolLeg", + "isMut": true, "isSigner": false }, { - "name": "payer", + "name": "treasuryMsolAccount", "isMut": true, - "isSigner": true + "isSigner": false }, { - "name": "epochReportAccount", - "isMut": true, + "name": "sysvarInstructions", + "isMut": false, "isSigner": false }, { - "name": "msolMint", - "isMut": true, + "name": "sunriseProgram", + "isMut": false, "isSigner": false }, { - "name": "msolVault", - "isMut": true, + "name": "marinadeProgram", + "isMut": false, "isSigner": false }, { - "name": "vaultAuthority", + "name": "systemProgram", "isMut": false, "isSigner": false }, { - "name": "clock", + "name": "tokenProgram", "isMut": false, "isSigner": false } @@ -1744,7 +1640,7 @@ export const IDL: MarinadeBeam = { "args": [] }, { - "name": "extractYield", + "name": "updateEpochReport", "accounts": [ { "name": "state", @@ -1753,27 +1649,22 @@ export const IDL: MarinadeBeam = { }, { "name": "sunriseState", - "isMut": false, + "isMut": true, "isSigner": false }, { "name": "marinadeState", - "isMut": true, + "isMut": false, "isSigner": false }, - { - "name": "payer", - "isMut": true, - "isSigner": true - }, { "name": "msolMint", - "isMut": true, + "isMut": false, "isSigner": false }, { "name": "msolVault", - "isMut": true, + "isMut": false, "isSigner": false }, { @@ -1782,17 +1673,21 @@ export const IDL: MarinadeBeam = { "isSigner": false }, { - "name": "yieldAccount", - "isMut": true, - "isSigner": false + "name": "gsolMint", + "isMut": false, + "isSigner": false, + "docs": [ + "Required to update the core state epoch report", + "Verified in CPI to Sunrise program." + ] }, { - "name": "epochReportAccount", - "isMut": true, + "name": "sysvarInstructions", + "isMut": false, "isSigner": false }, { - "name": "clock", + "name": "sunriseProgram", "isMut": false, "isSigner": false } @@ -1860,42 +1755,6 @@ export const IDL: MarinadeBeam = { } ] } - }, - { - "name": "epochReport", - "type": { - "kind": "struct", - "fields": [ - { - "name": "state", - "type": "publicKey" - }, - { - "name": "epoch", - "type": "u64" - }, - { - "name": "tickets", - "type": "u64" - }, - { - "name": "totalOrderedLamports", - "type": "u64" - }, - { - "name": "extractableYield", - "type": "u64" - }, - { - "name": "extractedYield", - "type": "u64" - }, - { - "name": "bump", - "type": "u8" - } - ] - } } ], "types": [ @@ -1932,26 +1791,21 @@ export const IDL: MarinadeBeam = { }, { "code": 6001, - "name": "CalculationFailure", - "msg": "An error occurred during calculation" - }, - { - "code": 6002, "name": "InvalidEpochReportAccount", "msg": "The epoch report account has not been updated to the current epoch yet" }, { - "code": 6003, + "code": 6002, "name": "RemainingUnclaimableTicketAmount", "msg": "The total ordered ticket amount exceeds the amount in all found tickets" }, { - "code": 6004, + "code": 6003, "name": "DelayedUnstakeTicketsNotYetClaimable", "msg": "Delayed unstake tickets for the current epoch can not yet be claimed" }, { - "code": 6005, + "code": 6004, "name": "TooManyTicketsClaimed", "msg": "The amount of delayed unstake tickets requested to be recovered exceeds the amount in the report" } diff --git a/packages/sdks/common/src/types/marinade_lp_beam.ts b/packages/sdks/common/src/types/marinade_lp_beam.ts index e1459f6..a64e08d 100644 --- a/packages/sdks/common/src/types/marinade_lp_beam.ts +++ b/packages/sdks/common/src/types/marinade_lp_beam.ts @@ -86,7 +86,7 @@ export type MarinadeLpBeam = { "accounts": [ { "name": "state", - "isMut": true, + "isMut": false, "isSigner": false }, { @@ -138,7 +138,7 @@ export type MarinadeLpBeam = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -195,7 +195,7 @@ export type MarinadeLpBeam = { "accounts": [ { "name": "state", - "isMut": true, + "isMut": false, "isSigner": false }, { @@ -277,7 +277,7 @@ export type MarinadeLpBeam = { ] }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -346,7 +346,7 @@ export type MarinadeLpBeam = { ] }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -372,6 +372,172 @@ export type MarinadeLpBeam = { "name": "redeemTicket", "accounts": [], "args": [] + }, + { + "name": "extractYield", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "marinadeState", + "isMut": true, + "isSigner": false + }, + { + "name": "sunriseState", + "isMut": true, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "yieldAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "liqPoolMint", + "isMut": true, + "isSigner": false + }, + { + "name": "liqPoolTokenVault", + "isMut": true, + "isSigner": false + }, + { + "name": "vaultAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "transferMsolTo", + "isMut": true, + "isSigner": false, + "docs": [ + "When withdrawing from the Marinade LP, the withdrawal is part SOL, part mSOL.", + "The SOL portion is transferred to the user (withdrawer) and the mSOL portion", + "is transferred to the msol_token_account owned by the marinade stake pool." + ] + }, + { + "name": "liqPoolSolLegPda", + "isMut": true, + "isSigner": false + }, + { + "name": "liqPoolMsolLeg", + "isMut": true, + "isSigner": false + }, + { + "name": "liqPoolMsolLegAuthority", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "sysvarInstructions", + "isMut": false, + "isSigner": false + }, + { + "name": "sunriseProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "marinadeProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "updateEpochReport", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "marinadeState", + "isMut": false, + "isSigner": false + }, + { + "name": "sunriseState", + "isMut": true, + "isSigner": false + }, + { + "name": "gsolMint", + "isMut": false, + "isSigner": false, + "docs": [ + "Required to update the core state epoch report", + "Verified in CPI to Sunrise program." + ] + }, + { + "name": "liqPoolMint", + "isMut": true, + "isSigner": false + }, + { + "name": "liqPoolTokenVault", + "isMut": true, + "isSigner": false + }, + { + "name": "vaultAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "liqPoolSolLegPda", + "isMut": true, + "isSigner": false + }, + { + "name": "liqPoolMsolLeg", + "isMut": true, + "isSigner": false + }, + { + "name": "liqPoolMsolLegAuthority", + "isMut": true, + "isSigner": false + }, + { + "name": "sysvarInstructions", + "isMut": false, + "isSigner": false + }, + { + "name": "sunriseProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] } ], "accounts": [ @@ -410,9 +576,10 @@ export type MarinadeLpBeam = { "type": "u8" }, { - "name": "treasury", + "name": "msolRecipientBeam", "docs": [ - "This state's SOL vault." + "The beam address of the recipient of msol when withdrawing liquidity.", + "Typically the marinade-sp beam" ], "type": "publicKey" }, @@ -450,7 +617,7 @@ export type MarinadeLpBeam = { "type": "u8" }, { - "name": "treasury", + "name": "msolRecipientBeam", "type": "publicKey" }, { @@ -471,6 +638,11 @@ export type MarinadeLpBeam = { "code": 6001, "name": "Unimplemented", "msg": "This feature is unimplemented for this beam" + }, + { + "code": 6002, + "name": "InsufficientYieldBalance", + "msg": "The yield balance is insufficient to extract yield" } ] }; @@ -563,7 +735,7 @@ export const IDL: MarinadeLpBeam = { "accounts": [ { "name": "state", - "isMut": true, + "isMut": false, "isSigner": false }, { @@ -615,7 +787,7 @@ export const IDL: MarinadeLpBeam = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -672,7 +844,7 @@ export const IDL: MarinadeLpBeam = { "accounts": [ { "name": "state", - "isMut": true, + "isMut": false, "isSigner": false }, { @@ -754,7 +926,7 @@ export const IDL: MarinadeLpBeam = { ] }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -823,7 +995,7 @@ export const IDL: MarinadeLpBeam = { ] }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -849,6 +1021,172 @@ export const IDL: MarinadeLpBeam = { "name": "redeemTicket", "accounts": [], "args": [] + }, + { + "name": "extractYield", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "marinadeState", + "isMut": true, + "isSigner": false + }, + { + "name": "sunriseState", + "isMut": true, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "yieldAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "liqPoolMint", + "isMut": true, + "isSigner": false + }, + { + "name": "liqPoolTokenVault", + "isMut": true, + "isSigner": false + }, + { + "name": "vaultAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "transferMsolTo", + "isMut": true, + "isSigner": false, + "docs": [ + "When withdrawing from the Marinade LP, the withdrawal is part SOL, part mSOL.", + "The SOL portion is transferred to the user (withdrawer) and the mSOL portion", + "is transferred to the msol_token_account owned by the marinade stake pool." + ] + }, + { + "name": "liqPoolSolLegPda", + "isMut": true, + "isSigner": false + }, + { + "name": "liqPoolMsolLeg", + "isMut": true, + "isSigner": false + }, + { + "name": "liqPoolMsolLegAuthority", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "sysvarInstructions", + "isMut": false, + "isSigner": false + }, + { + "name": "sunriseProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "marinadeProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "updateEpochReport", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "marinadeState", + "isMut": false, + "isSigner": false + }, + { + "name": "sunriseState", + "isMut": true, + "isSigner": false + }, + { + "name": "gsolMint", + "isMut": false, + "isSigner": false, + "docs": [ + "Required to update the core state epoch report", + "Verified in CPI to Sunrise program." + ] + }, + { + "name": "liqPoolMint", + "isMut": true, + "isSigner": false + }, + { + "name": "liqPoolTokenVault", + "isMut": true, + "isSigner": false + }, + { + "name": "vaultAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "liqPoolSolLegPda", + "isMut": true, + "isSigner": false + }, + { + "name": "liqPoolMsolLeg", + "isMut": true, + "isSigner": false + }, + { + "name": "liqPoolMsolLegAuthority", + "isMut": true, + "isSigner": false + }, + { + "name": "sysvarInstructions", + "isMut": false, + "isSigner": false + }, + { + "name": "sunriseProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] } ], "accounts": [ @@ -887,9 +1225,10 @@ export const IDL: MarinadeLpBeam = { "type": "u8" }, { - "name": "treasury", + "name": "msolRecipientBeam", "docs": [ - "This state's SOL vault." + "The beam address of the recipient of msol when withdrawing liquidity.", + "Typically the marinade-sp beam" ], "type": "publicKey" }, @@ -927,7 +1266,7 @@ export const IDL: MarinadeLpBeam = { "type": "u8" }, { - "name": "treasury", + "name": "msolRecipientBeam", "type": "publicKey" }, { @@ -948,6 +1287,11 @@ export const IDL: MarinadeLpBeam = { "code": 6001, "name": "Unimplemented", "msg": "This feature is unimplemented for this beam" + }, + { + "code": 6002, + "name": "InsufficientYieldBalance", + "msg": "The yield balance is insufficient to extract yield" } ] }; diff --git a/packages/sdks/common/src/types/spl_beam.ts b/packages/sdks/common/src/types/spl_beam.ts index 8237497..7358ccc 100644 --- a/packages/sdks/common/src/types/spl_beam.ts +++ b/packages/sdks/common/src/types/spl_beam.ts @@ -150,7 +150,7 @@ export type SplBeam = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -289,7 +289,7 @@ export type SplBeam = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -398,7 +398,7 @@ export type SplBeam = { ] }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -472,7 +472,7 @@ export type SplBeam = { ] }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -591,7 +591,7 @@ export type SplBeam = { ] }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -634,7 +634,7 @@ export type SplBeam = { "args": [] }, { - "name": "extractYield", + "name": "updateEpochReport", "accounts": [ { "name": "state", @@ -643,9 +643,64 @@ export type SplBeam = { }, { "name": "sunriseState", + "isMut": true, + "isSigner": false + }, + { + "name": "stakePool", + "isMut": false, + "isSigner": false + }, + { + "name": "poolMint", + "isMut": false, + "isSigner": false + }, + { + "name": "vaultAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "poolTokenVault", + "isMut": false, + "isSigner": false + }, + { + "name": "gsolMint", + "isMut": false, + "isSigner": false, + "docs": [ + "Required to update the core state epoch report", + "Verified in CPI to Sunrise program." + ] + }, + { + "name": "sunriseProgram", "isMut": false, "isSigner": false }, + { + "name": "sysvarInstructions", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "extractYield", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "sunriseState", + "isMut": true, + "isSigner": false + }, { "name": "stakePool", "isMut": true, @@ -704,15 +759,6 @@ export type SplBeam = { "isMut": true, "isSigner": false }, - { - "name": "epochReport", - "isMut": true, - "isSigner": false, - "docs": [ - "The epoch report account. This is updated with the latest extracted yield value.", - "It must be up to date with the current epoch. If not, run updateEpochReport before it." - ] - }, { "name": "sysvarClock", "isMut": false, @@ -1003,7 +1049,7 @@ export const IDL: SplBeam = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -1142,7 +1188,7 @@ export const IDL: SplBeam = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -1251,7 +1297,7 @@ export const IDL: SplBeam = { ] }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -1325,7 +1371,7 @@ export const IDL: SplBeam = { ] }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -1444,7 +1490,7 @@ export const IDL: SplBeam = { ] }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -1487,7 +1533,7 @@ export const IDL: SplBeam = { "args": [] }, { - "name": "extractYield", + "name": "updateEpochReport", "accounts": [ { "name": "state", @@ -1496,9 +1542,64 @@ export const IDL: SplBeam = { }, { "name": "sunriseState", + "isMut": true, + "isSigner": false + }, + { + "name": "stakePool", + "isMut": false, + "isSigner": false + }, + { + "name": "poolMint", + "isMut": false, + "isSigner": false + }, + { + "name": "vaultAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "poolTokenVault", + "isMut": false, + "isSigner": false + }, + { + "name": "gsolMint", + "isMut": false, + "isSigner": false, + "docs": [ + "Required to update the core state epoch report", + "Verified in CPI to Sunrise program." + ] + }, + { + "name": "sunriseProgram", "isMut": false, "isSigner": false }, + { + "name": "sysvarInstructions", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "extractYield", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "sunriseState", + "isMut": true, + "isSigner": false + }, { "name": "stakePool", "isMut": true, @@ -1557,15 +1658,6 @@ export const IDL: SplBeam = { "isMut": true, "isSigner": false }, - { - "name": "epochReport", - "isMut": true, - "isSigner": false, - "docs": [ - "The epoch report account. This is updated with the latest extracted yield value.", - "It must be up to date with the current epoch. If not, run updateEpochReport before it." - ] - }, { "name": "sysvarClock", "isMut": false, diff --git a/packages/sdks/common/src/types/sunrise_core.ts b/packages/sdks/common/src/types/sunrise_core.ts index af05d75..a3655ab 100644 --- a/packages/sdks/common/src/types/sunrise_core.ts +++ b/packages/sdks/common/src/types/sunrise_core.ts @@ -18,11 +18,6 @@ export type SunriseCore = { "isMut": true, "isSigner": true }, - { - "name": "epochReport", - "isMut": true, - "isSigner": false - }, { "name": "gsolMint", "isMut": false, @@ -208,7 +203,7 @@ export type SunriseCore = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -259,7 +254,7 @@ export type SunriseCore = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -276,6 +271,47 @@ export type SunriseCore = { } ] }, + { + "name": "transferGsol", + "docs": [ + "CPI request from a beam program to transfer gSol.", + "", + "Same invariants as for [minting][sunrise_core::mint_gsol()].", + "Errors if the recipient beam is not registered in the state." + ], + "accounts": [ + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "beam", + "isMut": false, + "isSigner": true + }, + { + "name": "gsolMint", + "isMut": true, + "isSigner": false + }, + { + "name": "sysvarInstructions", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "recipientBeam", + "type": "publicKey" + }, + { + "name": "amount", + "type": "u64" + } + ] + }, { "name": "removeBeam", "docs": [ @@ -349,13 +385,17 @@ export type SunriseCore = { "accounts": [ { "name": "state", - "isMut": false, + "isMut": true, "isSigner": false }, { - "name": "epochReport", - "isMut": true, - "isSigner": false + "name": "beam", + "isMut": false, + "isSigner": true, + "docs": [ + "The beam updating its epoch report.", + "This is verified in the handler to be a beam attached to this state." + ] }, { "name": "gsolMint", @@ -363,12 +403,17 @@ export type SunriseCore = { "isSigner": false }, { - "name": "clock", + "name": "sysvarInstructions", "isMut": false, "isSigner": false } ], - "args": [] + "args": [ + { + "name": "extractableYield", + "type": "u64" + } + ] }, { "name": "extractYield", @@ -378,8 +423,11 @@ export type SunriseCore = { "accounts": [ { "name": "state", - "isMut": false, - "isSigner": false + "isMut": true, + "isSigner": false, + "docs": [ + "The core sunrise state - will have its epoch report updated." + ] }, { "name": "beam", @@ -390,20 +438,6 @@ export type SunriseCore = { "This is verified in the handler to be a beam attached to this state." ] }, - { - "name": "epochReport", - "isMut": true, - "isSigner": false, - "docs": [ - "The epoch report account. This is updated with the latest extracted yield value.", - "It must be up to date with the current epoch. If not, run updateEpochReport before it." - ] - }, - { - "name": "sysvarClock", - "isMut": false, - "isSigner": false - }, { "name": "sysvarInstructions", "isMut": false, @@ -456,13 +490,6 @@ export type SunriseCore = { ], "type": "u8" }, - { - "name": "epochReportBump", - "docs": [ - "Bump of the eppch report PDA." - ], - "type": "u8" - }, { "name": "yieldAccount", "docs": [ @@ -492,34 +519,12 @@ export type SunriseCore = { "defined": "BeamDetails" } } - } - ] - } - }, - { - "name": "epochReport", - "type": { - "kind": "struct", - "fields": [ - { - "name": "epoch", - "type": "u64" - }, - { - "name": "extractableYield", - "type": "u64" }, { - "name": "extractedYield", - "type": "u64" - }, - { - "name": "currentGsolSupply", - "type": "u64" - }, - { - "name": "bump", - "type": "u8" + "name": "epochReport", + "type": { + "defined": "EpochReport" + } } ] } @@ -529,7 +534,7 @@ export type SunriseCore = { { "name": "BeamDetails", "docs": [ - "Holds information about a registed beam." + "Holds information about a registered beam." ], "type": { "kind": "struct", @@ -653,6 +658,52 @@ export type SunriseCore = { } ] } + }, + { + "name": "EpochReport", + "type": { + "kind": "struct", + "fields": [ + { + "name": "currentGsolSupply", + "type": "u64" + }, + { + "name": "beamEpochDetails", + "docs": [ + "Holds [BeamEpochDetails] for all supported beams." + ], + "type": { + "vec": { + "defined": "BeamEpochDetails" + } + } + } + ] + } + }, + { + "name": "BeamEpochDetails", + "type": { + "kind": "struct", + "fields": [ + { + "name": "epoch", + "docs": [ + "The most recent epoch that this beam has reported its extractable yield for" + ], + "type": "u64" + }, + { + "name": "extractableYield", + "type": "u64" + }, + { + "name": "extractedYield", + "type": "u64" + } + ] + } } ], "errors": [ @@ -703,31 +754,21 @@ export type SunriseCore = { }, { "code": 6009, - "name": "IncorrectBeamEpochReportCount", - "msg": "Incorrect amount of beam epoch reports" - }, - { - "code": 6010, - "name": "IncorrectBeamEpochReportEpoch", - "msg": "Incorrect epoch for beam epoch reports" - }, - { - "code": 6011, "name": "IncorrectBeamEpochReport", "msg": "Incorrect beam epoch report" }, { - "code": 6012, + "code": 6010, "name": "EpochReportAlreadyUpdated", "msg": "Epoch report already updated" }, { - "code": 6013, + "code": 6011, "name": "EpochReportNotUpToDate", "msg": "Epoch report not up to date" }, { - "code": 6014, + "code": 6012, "name": "Overflow", "msg": "Overflow" } @@ -754,11 +795,6 @@ export const IDL: SunriseCore = { "isMut": true, "isSigner": true }, - { - "name": "epochReport", - "isMut": true, - "isSigner": false - }, { "name": "gsolMint", "isMut": false, @@ -944,7 +980,7 @@ export const IDL: SunriseCore = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -995,7 +1031,7 @@ export const IDL: SunriseCore = { "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false }, @@ -1012,6 +1048,47 @@ export const IDL: SunriseCore = { } ] }, + { + "name": "transferGsol", + "docs": [ + "CPI request from a beam program to transfer gSol.", + "", + "Same invariants as for [minting][sunrise_core::mint_gsol()].", + "Errors if the recipient beam is not registered in the state." + ], + "accounts": [ + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "beam", + "isMut": false, + "isSigner": true + }, + { + "name": "gsolMint", + "isMut": true, + "isSigner": false + }, + { + "name": "sysvarInstructions", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "recipientBeam", + "type": "publicKey" + }, + { + "name": "amount", + "type": "u64" + } + ] + }, { "name": "removeBeam", "docs": [ @@ -1085,13 +1162,17 @@ export const IDL: SunriseCore = { "accounts": [ { "name": "state", - "isMut": false, + "isMut": true, "isSigner": false }, { - "name": "epochReport", - "isMut": true, - "isSigner": false + "name": "beam", + "isMut": false, + "isSigner": true, + "docs": [ + "The beam updating its epoch report.", + "This is verified in the handler to be a beam attached to this state." + ] }, { "name": "gsolMint", @@ -1099,12 +1180,17 @@ export const IDL: SunriseCore = { "isSigner": false }, { - "name": "clock", + "name": "sysvarInstructions", "isMut": false, "isSigner": false } ], - "args": [] + "args": [ + { + "name": "extractableYield", + "type": "u64" + } + ] }, { "name": "extractYield", @@ -1114,8 +1200,11 @@ export const IDL: SunriseCore = { "accounts": [ { "name": "state", - "isMut": false, - "isSigner": false + "isMut": true, + "isSigner": false, + "docs": [ + "The core sunrise state - will have its epoch report updated." + ] }, { "name": "beam", @@ -1126,20 +1215,6 @@ export const IDL: SunriseCore = { "This is verified in the handler to be a beam attached to this state." ] }, - { - "name": "epochReport", - "isMut": true, - "isSigner": false, - "docs": [ - "The epoch report account. This is updated with the latest extracted yield value.", - "It must be up to date with the current epoch. If not, run updateEpochReport before it." - ] - }, - { - "name": "sysvarClock", - "isMut": false, - "isSigner": false - }, { "name": "sysvarInstructions", "isMut": false, @@ -1192,13 +1267,6 @@ export const IDL: SunriseCore = { ], "type": "u8" }, - { - "name": "epochReportBump", - "docs": [ - "Bump of the eppch report PDA." - ], - "type": "u8" - }, { "name": "yieldAccount", "docs": [ @@ -1228,34 +1296,12 @@ export const IDL: SunriseCore = { "defined": "BeamDetails" } } - } - ] - } - }, - { - "name": "epochReport", - "type": { - "kind": "struct", - "fields": [ - { - "name": "epoch", - "type": "u64" - }, - { - "name": "extractableYield", - "type": "u64" }, { - "name": "extractedYield", - "type": "u64" - }, - { - "name": "currentGsolSupply", - "type": "u64" - }, - { - "name": "bump", - "type": "u8" + "name": "epochReport", + "type": { + "defined": "EpochReport" + } } ] } @@ -1265,7 +1311,7 @@ export const IDL: SunriseCore = { { "name": "BeamDetails", "docs": [ - "Holds information about a registed beam." + "Holds information about a registered beam." ], "type": { "kind": "struct", @@ -1389,6 +1435,52 @@ export const IDL: SunriseCore = { } ] } + }, + { + "name": "EpochReport", + "type": { + "kind": "struct", + "fields": [ + { + "name": "currentGsolSupply", + "type": "u64" + }, + { + "name": "beamEpochDetails", + "docs": [ + "Holds [BeamEpochDetails] for all supported beams." + ], + "type": { + "vec": { + "defined": "BeamEpochDetails" + } + } + } + ] + } + }, + { + "name": "BeamEpochDetails", + "type": { + "kind": "struct", + "fields": [ + { + "name": "epoch", + "docs": [ + "The most recent epoch that this beam has reported its extractable yield for" + ], + "type": "u64" + }, + { + "name": "extractableYield", + "type": "u64" + }, + { + "name": "extractedYield", + "type": "u64" + } + ] + } } ], "errors": [ @@ -1439,31 +1531,21 @@ export const IDL: SunriseCore = { }, { "code": 6009, - "name": "IncorrectBeamEpochReportCount", - "msg": "Incorrect amount of beam epoch reports" - }, - { - "code": 6010, - "name": "IncorrectBeamEpochReportEpoch", - "msg": "Incorrect epoch for beam epoch reports" - }, - { - "code": 6011, "name": "IncorrectBeamEpochReport", "msg": "Incorrect beam epoch report" }, { - "code": 6012, + "code": 6010, "name": "EpochReportAlreadyUpdated", "msg": "Epoch report already updated" }, { - "code": 6013, + "code": 6011, "name": "EpochReportNotUpToDate", "msg": "Epoch report not up to date" }, { - "code": 6014, + "code": 6012, "name": "Overflow", "msg": "Overflow" } diff --git a/packages/sdks/core/src/constants.ts b/packages/sdks/core/src/constants.ts index 5f0273c..de93809 100644 --- a/packages/sdks/core/src/constants.ts +++ b/packages/sdks/core/src/constants.ts @@ -6,5 +6,3 @@ export const SUNRISE_PROGRAM_ID = new PublicKey( ); /** The constant seed of the GSOL mint authority PDA. */ export const GSOL_AUTHORITY_SEED = "gsol_mint_authority"; - -export const EPOCH_REPORT_SEED = "epoch_report"; diff --git a/packages/sdks/core/src/index.ts b/packages/sdks/core/src/index.ts index ca2f632..ff201c8 100644 --- a/packages/sdks/core/src/index.ts +++ b/packages/sdks/core/src/index.ts @@ -13,11 +13,7 @@ import { } from "@solana/spl-token"; import { sendAndConfirmChecked, SunriseCore } from "@sunrisestake/beams-common"; import { StateAccount } from "./state.js"; -import { - EPOCH_REPORT_SEED, - GSOL_AUTHORITY_SEED, - SUNRISE_PROGRAM_ID, -} from "./constants.js"; +import { GSOL_AUTHORITY_SEED, SUNRISE_PROGRAM_ID } from "./constants.js"; /** An instance of the Sunrise program that checks the validity of other * beams and regulates the minting and burning of GSOL. @@ -77,16 +73,11 @@ export class SunriseClient { programId, provider, ); - const epochReport = SunriseClient.deriveEpochReport( - state.publicKey, - programId, - )[0]; const register = await program.methods .registerState({ updateAuthority, yieldAccount, initialCapacity }) .accounts({ payer: provider.publicKey, state: state.publicKey, - epochReport, gsolMint, gsolMintAuthority: SunriseClient.deriveGsolMintAuthority( state.publicKey, @@ -186,7 +177,7 @@ export class SunriseClient { gsolMint: PublicKey; gsolMintAuthority: PublicKey; mintGsolTo: PublicKey; - instructionsSysvar: PublicKey; + sysvarInstructions: PublicKey; tokenProgram: PublicKey; } { return { @@ -196,7 +187,7 @@ export class SunriseClient { gsolMintAuthority: this.gsolMintAuthority[0], mintGsolTo: gsolTokenAccount ?? this.gsolAssociatedTokenAccount(tokenAccountOwner), - instructionsSysvar: SYSVAR_INSTRUCTIONS_PUBKEY, + sysvarInstructions: SYSVAR_INSTRUCTIONS_PUBKEY, tokenProgram: TOKEN_PROGRAM_ID, }; } @@ -212,7 +203,7 @@ export class SunriseClient { gsolMint: PublicKey; burnGsolFromOwner: PublicKey; burnGsolFrom: PublicKey; - instructionsSysvar: PublicKey; + sysvarInstructions: PublicKey; tokenProgram: PublicKey; } { return { @@ -222,7 +213,7 @@ export class SunriseClient { burnGsolFromOwner: tokenAccountOwner, burnGsolFrom: gsolTokenAccount ?? this.gsolAssociatedTokenAccount(tokenAccountOwner), - instructionsSysvar: SYSVAR_INSTRUCTIONS_PUBKEY, + sysvarInstructions: SYSVAR_INSTRUCTIONS_PUBKEY, tokenProgram: TOKEN_PROGRAM_ID, }; } @@ -237,16 +228,6 @@ export class SunriseClient { ); } - private static deriveEpochReport( - stateAddress: PublicKey, - programId = SUNRISE_PROGRAM_ID, - ): [PublicKey, number] { - return PublicKey.findProgramAddressSync( - [stateAddress.toBuffer(), Buffer.from(EPOCH_REPORT_SEED)], - programId, - ); - } - /** Get the address of the gsol mint authority. */ public get gsolMintAuthority(): [PublicKey, number] { return SunriseClient.deriveGsolMintAuthority( @@ -255,15 +236,6 @@ export class SunriseClient { ); } - /** Gets the address of the epoch report account */ - public get epochReport(): [PublicKey, number] { - console.log("this.stateAddress", this.stateAddress); - return SunriseClient.deriveEpochReport( - this.stateAddress, - this.program.programId, - ); - } - /** Derive the gsol ATA for a particular owner or the current provider key by default. */ public gsolAssociatedTokenAccount(owner?: PublicKey): PublicKey { return getAssociatedTokenAddressSync( diff --git a/packages/sdks/core/src/state.ts b/packages/sdks/core/src/state.ts index 4cc00c6..bc1a086 100644 --- a/packages/sdks/core/src/state.ts +++ b/packages/sdks/core/src/state.ts @@ -2,6 +2,15 @@ import { type PublicKey } from "@solana/web3.js"; import { type IdlAccounts, type BN } from "@coral-xyz/anchor"; import { SunriseCore } from "@sunrisestake/beams-common"; +export type EpochReport = { + currentGsolSupply: BN; + beamEpochDetails: Array<{ + epoch: BN; + extractableYield: BN; + extractedYield: BN; + }>; +}; + /** The deserialized state for the on-chain beam account.*/ export class StateAccount { public readonly address: PublicKey; @@ -11,6 +20,7 @@ export class StateAccount { public readonly gsolAuthBump: number; public readonly yieldAccount: PublicKey; public readonly beams: BeamDetails[]; + public readonly epochReport: EpochReport; private constructor( _address: PublicKey, @@ -23,6 +33,7 @@ export class StateAccount { this.gsolAuthBump = account.gsolMintAuthorityBump; this.yieldAccount = account.yieldAccount; this.beams = account.allocations; + this.epochReport = account.epochReport; } /** Create a new instance from an anchor-deserialized account. */ @@ -37,6 +48,8 @@ export class StateAccount { public pretty(): { [Property in keyof Omit]: Property extends "beams" ? Array + : Property extends "epochReport" + ? EpochReportPretty : string; } { return { @@ -47,6 +60,7 @@ export class StateAccount { gsolAuthBump: this.gsolAuthBump.toString(), yieldAccount: this.yieldAccount.toBase58(), beams: this.beams.map((beam) => printBeamDetails(beam)), + epochReport: printEpochReport(this.epochReport), }; } } @@ -68,3 +82,21 @@ const printBeamDetails = (raw: BeamDetails): BeamDetailsPretty => { drainingMode: raw.drainingMode.toString(), }; }; + +type EpochReportPretty = { + currentGsolSupply: string; + beamEpochDetails: Array<{ + epoch: string; + extractableYield: string; + extractedYield: string; + }>; +}; + +const printEpochReport = (raw: EpochReport): EpochReportPretty => ({ + currentGsolSupply: raw.currentGsolSupply.toString(), + beamEpochDetails: raw.beamEpochDetails.map((epoch) => ({ + epoch: epoch.epoch.toString(), + extractableYield: epoch.extractableYield.toString(), + extractedYield: epoch.extractedYield.toString(), + })), +}); diff --git a/packages/sdks/marinade-lp/src/index.ts b/packages/sdks/marinade-lp/src/index.ts index 68e955e..fc68580 100644 --- a/packages/sdks/marinade-lp/src/index.ts +++ b/packages/sdks/marinade-lp/src/index.ts @@ -1,21 +1,23 @@ import { type AnchorProvider, Program } from "@coral-xyz/anchor"; import { + Keypair, PublicKey, + SystemProgram, + SYSVAR_INSTRUCTIONS_PUBKEY, Transaction, type TransactionInstruction, - SystemProgram, - Keypair, } from "@solana/web3.js"; import { ASSOCIATED_TOKEN_PROGRAM_ID, - TOKEN_PROGRAM_ID, createAssociatedTokenAccountIdempotentInstruction, getAssociatedTokenAddressSync, + TOKEN_PROGRAM_ID, } from "@solana/spl-token"; import { - MarinadeLpBeam, BeamInterface, deriveAuthorityAddress, + MarinadeLpBeam, + requestIncreasedCUsIx, sendAndConfirmChecked, } from "@sunrisestake/beams-common"; import { StateAccount } from "./state.js"; @@ -23,7 +25,7 @@ import { MARINADE_BEAM_PROGRAM_ID, MARINADE_FINANCE_PROGRAM_ID, } from "./constants.js"; -import { MarinadeLpClientParams, Utils } from "./utils.js"; +import { Balance, MarinadeLpClientParams, Utils } from "./utils.js"; import BN from "bn.js"; import { SunriseClient } from "@sunrisestake/beams-core"; @@ -59,7 +61,7 @@ export class MarinadeLpClient extends BeamInterface< provider: AnchorProvider, updateAuthority: PublicKey, sunriseState: PublicKey, - treasury: PublicKey, + msolRecipientBeam: PublicKey, msolTokenAccount: PublicKey, programId = MARINADE_BEAM_PROGRAM_ID, ): Promise { @@ -85,7 +87,7 @@ export class MarinadeLpClient extends BeamInterface< marinadeState: marinadeLpClientParams.marinade.marinadeStateAddress, sunriseState, vaultAuthorityBump, - treasury, + msolRecipientBeam, msolTokenAccount, }) .accounts({ @@ -178,7 +180,7 @@ export class MarinadeLpClient extends BeamInterface< recipient?: PublicKey, ): Promise { const depositor = this.provider.publicKey; - const { gsolMint, gsolMintAuthority, instructionsSysvar } = + const { gsolMint, gsolMintAuthority, sysvarInstructions } = this.sunrise.mintGsolAccounts(this.stateAddress, depositor); const transaction = new Transaction(); @@ -202,7 +204,7 @@ export class MarinadeLpClient extends BeamInterface< vaultAuthority: this.vaultAuthority[0], gsolMint, gsolMintAuthority, - instructionsSysvar, + sysvarInstructions, liqPoolSolLegPda: await this.marinadeLp.marinade.solLeg(), liqPoolMsolLeg: this.marinadeLp.marinade.mSolLeg, liqPoolMsolLegAuthority: @@ -230,7 +232,7 @@ export class MarinadeLpClient extends BeamInterface< gsolTokenAccount?: PublicKey, ): Promise { const withdrawer = this.provider.publicKey; - const { gsolMint, instructionsSysvar, burnGsolFrom } = + const { gsolMint, sysvarInstructions, burnGsolFrom } = this.sunrise.burnGsolAccounts( this.stateAddress, withdrawer, @@ -256,13 +258,13 @@ export class MarinadeLpClient extends BeamInterface< systemProgram: SystemProgram.programId, tokenProgram: TOKEN_PROGRAM_ID, gsolMint, - instructionsSysvar, + sysvarInstructions, sunriseProgram: this.sunrise.program.programId, marinadeProgram: MARINADE_FINANCE_PROGRAM_ID, }) .instruction(); - return new Transaction().add(instruction); + return new Transaction().add(requestIncreasedCUsIx(400_000), instruction); } /** @@ -287,7 +289,7 @@ export class MarinadeLpClient extends BeamInterface< gsolTokenAccount?: PublicKey, ): Promise { const burner = this.provider.publicKey; - const { gsolMint, instructionsSysvar, burnGsolFrom } = + const { gsolMint, sysvarInstructions, burnGsolFrom } = this.sunrise.burnGsolAccounts( this.stateAddress, burner, @@ -304,7 +306,7 @@ export class MarinadeLpClient extends BeamInterface< systemProgram: SystemProgram.programId, tokenProgram: TOKEN_PROGRAM_ID, gsolMint, - instructionsSysvar, + sysvarInstructions, sunriseProgram: this.sunrise.program.programId, }) .instruction(); @@ -312,21 +314,64 @@ export class MarinadeLpClient extends BeamInterface< return new Transaction().add(instruction); } - // public async extractYield(): Promise { - // const instruction = await this.program.methods - // .extractYield() - // .accounts({ - // state: this.stateAddress, - // sunriseState: this.state.sunriseState, - // systemProgram: SystemProgram.programId, - // tokenProgram: TOKEN_PROGRAM_ID, - // instructionsSysvar, - // sunriseProgram: this.sunrise.program.programId, - // }) - // .instruction(); - // - // return new Transaction().add(instruction); - // } + // Update this beam's extractable yield in the core state's epoch report + public async updateEpochReport(): Promise { + const accounts = { + state: this.stateAddress, + marinadeState: this.state.proxyState, + sunriseState: this.state.sunriseState, + liqPoolMint: this.marinadeLp.marinade.lpMint.address, + liqPoolTokenVault: this.marinadeLp.beamVault, + vaultAuthority: this.vaultAuthority[0], + transferMsolTo: this.state.msolTokenAccount, + liqPoolSolLegPda: await this.marinadeLp.marinade.solLeg(), + liqPoolMsolLeg: this.marinadeLp.marinade.mSolLeg, + liqPoolMsolLegAuthority: + await this.marinadeLp.marinade.mSolLegAuthority(), + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + gsolMint: this.sunrise.state.gsolMint, + sysvarInstructions: SYSVAR_INSTRUCTIONS_PUBKEY, + sunriseProgram: this.sunrise.program.programId, + }; + const instruction = await this.program.methods + .updateEpochReport() + .accounts(accounts) + .instruction(); + + return new Transaction().add(instruction); + } + + /** + * Return a transaction to extract any yield from this beam into the yield account + */ + public async extractYield(): Promise { + const accounts = { + state: this.stateAddress, + marinadeState: this.state.proxyState, + sunriseState: this.state.sunriseState, + yieldAccount: this.sunrise.state.yieldAccount, + liqPoolMint: this.marinadeLp.marinade.lpMint.address, + liqPoolTokenVault: this.marinadeLp.beamVault, + vaultAuthority: this.vaultAuthority[0], + transferMsolTo: this.state.msolTokenAccount, + liqPoolSolLegPda: await this.marinadeLp.marinade.solLeg(), + liqPoolMsolLeg: this.marinadeLp.marinade.mSolLeg, + liqPoolMsolLegAuthority: + await this.marinadeLp.marinade.mSolLegAuthority(), + sysvarInstructions: SYSVAR_INSTRUCTIONS_PUBKEY, + sunriseProgram: this.sunrise.program.programId, + marinadeProgram: MARINADE_FINANCE_PROGRAM_ID, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + }; + const instruction = await this.program.methods + .extractYield() + .accounts(accounts) + .instruction(); + + return new Transaction().add(instruction); + } /** * Return a transaction to redeem a ticket received from ordering a withdrawal from a marinade-lp. @@ -382,8 +427,15 @@ export class MarinadeLpClient extends BeamInterface< return new BN("" + lpMintInfo.supply); }; + public msolLegBalance = async (): Promise => { + const lpMsolLeg = this.marinadeLp.marinade.mSolLeg; + const lpMsolLegBalance = + await this.provider.connection.getTokenAccountBalance(lpMsolLeg); + return new BN(lpMsolLegBalance.value.amount); + }; + /** - * A convenience method for calculating the price of the stake-pool's token. + * A convenience method for calculating the price of the stake-pool's token in Lamports * NOTE: This might not give the current price is refresh() isn't called first. */ public poolTokenPrice = async () => { @@ -391,16 +443,31 @@ export class MarinadeLpClient extends BeamInterface< const lpSupply = lpMintInfo.supply; const solBalance = await this.poolSolBalance(); - const lpMsolLeg = this.marinadeLp.marinade.mSolLeg; - const lpMsolLegBalance = - await this.provider.connection.getTokenAccountBalance(lpMsolLeg); + const lpMsolLegBalance = await this.msolLegBalance(); const msolPrice = this.marinadeLp.marinade.mSolPrice; - const msolValue = Number(lpMsolLegBalance.value.amount) * msolPrice; - - const lpPrice = (solBalance + msolValue) / Number(lpSupply); + const msolValue = lpMsolLegBalance.toNumber() * msolPrice; - return lpPrice; + return (solBalance + msolValue) / Number(lpSupply); }; + + /** + * Calculate the value of the stake-pool's token in SOL and mSOL + * @param lpTokens + */ + public async calculateBalanceFromLpTokens(lpTokens: BN): Promise { + const totalSupply = await this.poolTokenSupply(); + const proportionOfPool = lpTokens.toNumber() / totalSupply.toNumber(); + const solBalance = await this.poolSolBalance(); + const msolLegBalance = await this.msolLegBalance(); + + const solValue = solBalance * proportionOfPool; + const msolValue = msolLegBalance.toNumber() * proportionOfPool; + + return { + lamports: new BN(solValue), + msolLamports: new BN(msolValue), + }; + } } diff --git a/packages/sdks/marinade-lp/src/state.ts b/packages/sdks/marinade-lp/src/state.ts index 8c310a6..882ab62 100644 --- a/packages/sdks/marinade-lp/src/state.ts +++ b/packages/sdks/marinade-lp/src/state.ts @@ -9,7 +9,7 @@ export class StateAccount implements BeamState { public readonly proxyState: PublicKey; public readonly sunriseState: PublicKey; public readonly vaultAuthorityBump: number; - public readonly treasury: PublicKey; + public readonly msolRecipientBeam: PublicKey; public readonly msolTokenAccount: PublicKey; private constructor( @@ -21,7 +21,7 @@ export class StateAccount implements BeamState { this.proxyState = account.marinadeState; this.sunriseState = account.sunriseState; this.vaultAuthorityBump = account.vaultAuthorityBump; - this.treasury = account.treasury; + this.msolRecipientBeam = account.msolRecipientBeam; this.msolTokenAccount = account.msolTokenAccount; } @@ -43,7 +43,7 @@ export class StateAccount implements BeamState { proxyState: this.proxyState.toBase58(), sunriseState: this.sunriseState.toBase58(), vaultAuthorityBump: this.vaultAuthorityBump.toString(), - treasury: this.treasury.toBase58(), + msolRecipientBeam: this.msolRecipientBeam.toBase58(), msolTokenAccount: this.msolTokenAccount.toBase58(), }; } diff --git a/packages/sdks/marinade-lp/src/utils.ts b/packages/sdks/marinade-lp/src/utils.ts index 6ab7f11..15f16f2 100644 --- a/packages/sdks/marinade-lp/src/utils.ts +++ b/packages/sdks/marinade-lp/src/utils.ts @@ -6,6 +6,7 @@ import { import { getAssociatedTokenAddressSync } from "@solana/spl-token"; import { AnchorProvider } from "@coral-xyz/anchor"; import { deriveAuthorityAddress } from "@sunrisestake/beams-common"; +import BN from "bn.js"; export type MarinadeLpClientParams = { /** The marinade state. */ @@ -19,6 +20,11 @@ const enum Seeds { STATE = "sunrise-marinade-lp", } +export type Balance = { + lamports: BN; + msolLamports: BN; +}; + /** A utility class containing methods for PDA-derivation. */ export class Utils { /** Derive the address of the state account for this beam. */ diff --git a/packages/sdks/marinade-sp/src/index.ts b/packages/sdks/marinade-sp/src/index.ts index ed7e3ae..66261c5 100644 --- a/packages/sdks/marinade-sp/src/index.ts +++ b/packages/sdks/marinade-sp/src/index.ts @@ -8,6 +8,7 @@ import { SYSVAR_CLOCK_PUBKEY, SYSVAR_RENT_PUBKEY, StakeProgram, + SYSVAR_INSTRUCTIONS_PUBKEY, } from "@solana/web3.js"; import { ASSOCIATED_TOKEN_PROGRAM_ID, @@ -183,7 +184,7 @@ export class MarinadeClient extends BeamInterface< recipient?: PublicKey, ): Promise { const depositor = this.provider.publicKey; - const { gsolMint, gsolMintAuthority, instructionsSysvar } = + const { gsolMint, gsolMintAuthority, sysvarInstructions } = this.sunrise.mintGsolAccounts(this.stateAddress, depositor); const transaction = new Transaction(); @@ -207,7 +208,7 @@ export class MarinadeClient extends BeamInterface< vaultAuthority: this.vaultAuthority[0], gsolMint, gsolMintAuthority, - instructionsSysvar, + sysvarInstructions, liqPoolSolLegPda: await this.marinade.state.solLeg(), liqPoolMsolLeg: this.marinade.state.mSolLeg, liqPoolMsolLegAuthority: await this.marinade.state.mSolLegAuthority(), @@ -229,34 +230,35 @@ export class MarinadeClient extends BeamInterface< gsolTokenAccount?: PublicKey, ): Promise { const withdrawer = this.provider.publicKey; - const { gsolMint, instructionsSysvar, burnGsolFrom } = + const { gsolMint, sysvarInstructions, burnGsolFrom } = this.sunrise.burnGsolAccounts( this.stateAddress, withdrawer, gsolTokenAccount, ); + const accounts = { + state: this.stateAddress, + marinadeState: this.state.proxyState, + sunriseState: this.state.sunriseState, + withdrawer, + gsolTokenAccount: burnGsolFrom, + gsolMint, + msolMint: this.marinade.state.mSolMint.address, + msolVault: this.marinade.beamMsolVault, + vaultAuthority: this.vaultAuthority[0], + liqPoolSolLegPda: await this.marinade.state.solLeg(), + liqPoolMsolLeg: this.marinade.state.mSolLeg, + treasuryMsolAccount: this.marinade.state.treasuryMsolAccount, + sysvarInstructions, + sunriseProgram: this.sunrise.program.programId, + marinadeProgram: MARINADE_FINANCE_PROGRAM_ID, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + }; const instruction = await this.program.methods .withdraw(amount) - .accounts({ - state: this.stateAddress, - marinadeState: this.state.proxyState, - sunriseState: this.state.sunriseState, - withdrawer, - gsolTokenAccount: burnGsolFrom, - gsolMint, - msolMint: this.marinade.state.mSolMint.address, - msolVault: this.marinade.beamMsolVault, - vaultAuthority: this.vaultAuthority[0], - liqPoolSolLegPda: await this.marinade.state.solLeg(), - liqPoolMsolLeg: this.marinade.state.mSolLeg, - treasuryMsolAccount: this.marinade.state.treasuryMsolAccount, - instructionsSysvar, - sunriseProgram: this.sunrise.program.programId, - marinadeProgram: MARINADE_FINANCE_PROGRAM_ID, - systemProgram: SystemProgram.programId, - tokenProgram: TOKEN_PROGRAM_ID, - }) + .accounts(accounts) .instruction(); return new Transaction().add(instruction); @@ -274,7 +276,7 @@ export class MarinadeClient extends BeamInterface< proxyTicket: Keypair; }> { const withdrawer = this.provider.publicKey; - const { gsolMint, instructionsSysvar, burnGsolFrom } = + const { gsolMint, sysvarInstructions, burnGsolFrom } = this.sunrise.burnGsolAccounts( this.stateAddress, withdrawer, @@ -311,7 +313,7 @@ export class MarinadeClient extends BeamInterface< msolMint: this.marinade.state.mSolMint.address, msolVault: this.marinade.beamMsolVault, vaultAuthority: this.vaultAuthority[0], - instructionsSysvar, + sysvarInstructions, newTicketAccount: marinadeTicket.publicKey, proxyTicketAccount: sunriseTicket.publicKey, sunriseProgram: this.sunrise.program.programId, @@ -336,7 +338,7 @@ export class MarinadeClient extends BeamInterface< gsolTokenAccount?: PublicKey, ): Promise { const burner = this.provider.publicKey; - const { gsolMint, instructionsSysvar, burnGsolFrom } = + const { gsolMint, sysvarInstructions, burnGsolFrom } = this.sunrise.burnGsolAccounts( this.stateAddress, burner, @@ -350,10 +352,11 @@ export class MarinadeClient extends BeamInterface< sunriseState: this.state.sunriseState, burner, gsolTokenAccount: burnGsolFrom, + vaultAuthority: this.vaultAuthority[0], systemProgram: SystemProgram.programId, tokenProgram: TOKEN_PROGRAM_ID, gsolMint, - instructionsSysvar, + sysvarInstructions, sunriseProgram: this.sunrise.program.programId, }) .instruction(); @@ -391,7 +394,7 @@ export class MarinadeClient extends BeamInterface< recipient?: PublicKey, ): Promise { const stakeOwner = this.provider.publicKey; - const { gsolMint, gsolMintAuthority, instructionsSysvar } = + const { gsolMint, gsolMintAuthority, sysvarInstructions } = this.sunrise.mintGsolAccounts(this.stateAddress, stakeOwner); const transaction = new Transaction(); @@ -436,7 +439,7 @@ export class MarinadeClient extends BeamInterface< vaultAuthority: this.vaultAuthority[0], gsolMint, gsolMintAuthority, - instructionsSysvar, + sysvarInstructions, validatorList: info.validatorSystem.validatorList.account, stakeList: info.stakeSystem.stakeList.account, duplicationFlag: @@ -477,4 +480,54 @@ export class MarinadeClient extends BeamInterface< const PID = programId ?? MARINADE_BEAM_PROGRAM_ID; return Utils.deriveStateAddress(PID, sunriseState); }; + + // Update this beam's extractable yield in the core state's epoch report + public async updateEpochReport(): Promise { + const accounts = { + state: this.stateAddress, + marinadeState: this.state.proxyState, + sunriseState: this.state.sunriseState, + msolMint: this.marinade.state.mSolMint.address, + msolVault: this.marinade.beamMsolVault, + gsolMint: this.sunrise.state.gsolMint, + vaultAuthority: this.vaultAuthority[0], + sysvarInstructions: SYSVAR_INSTRUCTIONS_PUBKEY, + sunriseProgram: this.sunrise.program.programId, + }; + const instruction = await this.program.methods + .updateEpochReport() + .accounts(accounts) + .instruction(); + + return new Transaction().add(instruction); + } + + /** + * Return a transaction to extract any yield from this beam into the yield account + */ + public async extractYield(): Promise { + const accounts = { + state: this.stateAddress, + marinadeState: this.state.proxyState, + sunriseState: this.state.sunriseState, + msolMint: this.marinade.state.mSolMint.address, + msolVault: this.marinade.beamMsolVault, + vaultAuthority: this.vaultAuthority[0], + liqPoolSolLegPda: await this.marinade.state.solLeg(), + liqPoolMsolLeg: this.marinade.state.mSolLeg, + treasuryMsolAccount: this.marinade.state.treasuryMsolAccount, + yieldAccount: this.sunrise.state.yieldAccount, + sysvarInstructions: SYSVAR_INSTRUCTIONS_PUBKEY, + sunriseProgram: this.sunrise.program.programId, + marinadeProgram: MARINADE_FINANCE_PROGRAM_ID, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + }; + const instruction = await this.program.methods + .extractYield() + .accounts(accounts) + .instruction(); + + return new Transaction().add(instruction); + } } diff --git a/packages/sdks/spl/src/index.ts b/packages/sdks/spl/src/index.ts index 6315f45..918083b 100644 --- a/packages/sdks/spl/src/index.ts +++ b/packages/sdks/spl/src/index.ts @@ -191,7 +191,7 @@ export class SplClient extends BeamInterface { ): Promise { const depositor = this.provider.publicKey; - const { gsolMint, gsolMintAuthority, instructionsSysvar } = + const { gsolMint, gsolMintAuthority, sysvarInstructions } = this.sunrise.mintGsolAccounts(this.stateAddress, depositor); const transaction = new Transaction(); @@ -218,7 +218,7 @@ export class SplClient extends BeamInterface { managerFeeAccount: this.spl.stakePoolState.managerFeeAccount, gsolMint, gsolMintAuthority, - instructionsSysvar, + sysvarInstructions, sunriseProgram: this.sunrise.program.programId, splStakePoolProgram: SPL_STAKE_POOL_PROGRAM_ID, systemProgram: SystemProgram.programId, @@ -237,7 +237,7 @@ export class SplClient extends BeamInterface { gsolTokenAccount?: PublicKey, ): Promise { const withdrawer = this.provider.publicKey; - const { gsolMint, instructionsSysvar, burnGsolFrom } = + const { gsolMint, sysvarInstructions, burnGsolFrom } = this.sunrise.burnGsolAccounts( this.stateAddress, withdrawer, @@ -262,7 +262,7 @@ export class SplClient extends BeamInterface { sysvarStakeHistory: SYSVAR_STAKE_HISTORY_PUBKEY, nativeStakeProgram: StakeProgram.programId, gsolMint, - instructionsSysvar, + sysvarInstructions, sunriseProgram: this.sunrise.program.programId, splStakePoolProgram: SPL_STAKE_POOL_PROGRAM_ID, systemProgram: SystemProgram.programId, @@ -281,7 +281,7 @@ export class SplClient extends BeamInterface { recipient?: PublicKey, ): Promise { const stakeOwner = this.provider.publicKey; - const { gsolMint, gsolMintAuthority, instructionsSysvar } = + const { gsolMint, gsolMintAuthority, sysvarInstructions } = this.sunrise.mintGsolAccounts(this.stateAddress, stakeOwner); const stakeAccountInfo = await getParsedStakeAccountInfo( @@ -324,7 +324,7 @@ export class SplClient extends BeamInterface { nativeStakeProgram: StakeProgram.programId, gsolMint, gsolMintAuthority, - instructionsSysvar, + sysvarInstructions, sunriseProgram: this.sunrise.program.programId, splStakePoolProgram: SPL_STAKE_POOL_PROGRAM_ID, systemProgram: SystemProgram.programId, @@ -344,7 +344,7 @@ export class SplClient extends BeamInterface { gsolTokenAccount?: PublicKey, ): Promise { const withdrawer = this.provider.publicKey; - const { gsolMint, instructionsSysvar, burnGsolFrom } = + const { gsolMint, sysvarInstructions, burnGsolFrom } = this.sunrise.burnGsolAccounts( this.stateAddress, withdrawer, @@ -371,7 +371,7 @@ export class SplClient extends BeamInterface { sysvarStakeHistory: SYSVAR_STAKE_HISTORY_PUBKEY, nativeStakeProgram: StakeProgram.programId, gsolMint, - instructionsSysvar, + sysvarInstructions, sunriseProgram: this.sunrise.program.programId, splStakePoolProgram: SPL_STAKE_POOL_PROGRAM_ID, systemProgram: SystemProgram.programId, @@ -401,7 +401,7 @@ export class SplClient extends BeamInterface { gsolTokenAccount?: PublicKey, ): Promise { const burner = this.provider.publicKey; - const { gsolMint, instructionsSysvar, burnGsolFrom } = + const { gsolMint, sysvarInstructions, burnGsolFrom } = this.sunrise.burnGsolAccounts( this.stateAddress, burner, @@ -419,7 +419,7 @@ export class SplClient extends BeamInterface { systemProgram: SystemProgram.programId, tokenProgram: TOKEN_PROGRAM_ID, gsolMint, - instructionsSysvar, + sysvarInstructions, sunriseProgram: this.sunrise.program.programId, }) .instruction(); @@ -427,6 +427,29 @@ export class SplClient extends BeamInterface { return new Transaction().add(instruction); } + /** + * Update this beam's extractable yield in the core state's epoch report + */ + public async updateEpochReport(): Promise { + const accounts = { + state: this.stateAddress, + stakePool: this.spl.stakePoolAddress, + sunriseState: this.state.sunriseState, + poolMint: this.spl.stakePoolState.poolMint, + vaultAuthority: this.vaultAuthority[0], + poolTokenVault: this.spl.beamVault, + gsolMint: this.sunrise.state.gsolMint, + sysvarInstructions: SYSVAR_INSTRUCTIONS_PUBKEY, + sunriseProgram: this.sunrise.program.programId, + }; + const instruction = await this.program.methods + .updateEpochReport() + .accounts(accounts) + .instruction(); + + return new Transaction().add(instruction); + } + /** * Return a transaction to extract any yield from this beam into the yield account */ @@ -449,7 +472,6 @@ export class SplClient extends BeamInterface { validatorStakeList: this.spl.stakePoolState.validatorList, stakeAccountToSplit: this.spl.stakePoolState.reserveStake, managerFeeAccount: this.spl.stakePoolState.managerFeeAccount, - epochReport: this.sunrise.epochReport[0], sysvarClock: SYSVAR_CLOCK_PUBKEY, nativeStakeProgram: StakeProgram.programId, sysvarStakeHistory: SYSVAR_STAKE_HISTORY_PUBKEY, @@ -459,8 +481,6 @@ export class SplClient extends BeamInterface { systemProgram: SystemProgram.programId, tokenProgram: TOKEN_PROGRAM_ID, }; - console.log("EXTRACT YIELD ACCOUNTS"); - console.log(accounts); const instruction = await this.program.methods .extractYield() .accounts(accounts) diff --git a/packages/tests/src/functional/beams/core.test.ts b/packages/tests/src/functional/beams/core.test.ts index 09a892e..e52a5d2 100644 --- a/packages/tests/src/functional/beams/core.test.ts +++ b/packages/tests/src/functional/beams/core.test.ts @@ -13,6 +13,7 @@ import { provider } from "../setup.js"; import { expect } from "chai"; const BEAM_DETAILS_LEN: number = 170; +const EPOCH_REPORT_LEN: number = 24; describe("Sunrise core", () => { let gsolMint: PublicKey; @@ -137,7 +138,9 @@ describe("Sunrise core", () => { const finalLen = await provider.connection .getAccountInfo(client.stateAddress) .then((info) => info!.data.length); - expect(finalLen).to.equal(initialLen + 5 * BEAM_DETAILS_LEN); + expect(finalLen).to.equal( + initialLen + 5 * BEAM_DETAILS_LEN + 5 * EPOCH_REPORT_LEN, + ); await client.refresh(); for (const beam of client.state.pretty().beams.slice(15, 20)) { diff --git a/packages/tests/src/functional/beams/marinade-lp.test.ts b/packages/tests/src/functional/beams/marinade-lp.test.ts index 220a179..6637e1d 100644 --- a/packages/tests/src/functional/beams/marinade-lp.test.ts +++ b/packages/tests/src/functional/beams/marinade-lp.test.ts @@ -8,7 +8,8 @@ import { Keypair, LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js"; import BN from "bn.js"; import { provider, staker, stakerIdentity } from "../setup.js"; import { - createTokenAccount, + expectAmount, + expectSolBalance, expectTokenBalance, fund, logAtLevel, @@ -17,12 +18,14 @@ import { tokenAccountBalance, } from "../../utils.js"; import { expect } from "chai"; -import { MSOL_MINT } from "../consts.js"; +import { MarinadeClient } from "@sunrisestake/beams-marinade-sp"; +import { MARINADE_LP_WITHDRAWAL_FEE_PERCENTAGE, ZERO } from "../consts.js"; describe("Marinade liquidity pool beam", () => { let coreClient: SunriseClient; let beamClient: MarinadeLpClient; let stakerGsolBalance: BN; + let netExtractableYield: BN; let sunriseStateAddress: PublicKey; let vaultBalance: BN; @@ -30,6 +33,7 @@ describe("Marinade liquidity pool beam", () => { const depositAmount = 10 * LAMPORTS_PER_SOL; const failedDepositAmount = 5 * LAMPORTS_PER_SOL; const withdrawalAmount = 5 * LAMPORTS_PER_SOL; + const burnAmount = new BN(1 * LAMPORTS_PER_SOL); before("Set up the sunrise state", async () => { coreClient = await registerSunriseState(); @@ -57,24 +61,29 @@ describe("Marinade liquidity pool beam", () => { }); it("can initialize a state", async () => { - // create an MSol token account for the beam. - // NOTE - when combined with the marinade-sp beam, this should be the msol token account - // associated with the marinade stake pool. - const msolTokenAccount = await createTokenAccount( + // Register the SP beam first. + // Note - the Marinade LP beam cannot exist without the Marinade SP beam (or some other beam + // that stores msol) because it needs somewhere to send the "excess" msol whenever someone withdraws + // gsol from the pool. + const marinadeSpBeamClient = await MarinadeClient.initialize( provider, + provider.publicKey, sunriseStateAddress, - MSOL_MINT, + ); + coreClient = await coreClient.refresh(); + await sendAndConfirmTransaction( + provider, + await coreClient.registerBeam(marinadeSpBeamClient.stateAddress), ); await SunriseClient.get(provider, sunriseStateAddress); - const treasury = Keypair.generate(); beamClient = await MarinadeLpClient.initialize( provider, provider.publicKey, sunriseStateAddress, - treasury.publicKey, - msolTokenAccount, + marinadeSpBeamClient.stateAddress, + marinadeSpBeamClient.marinade.beamMsolVault, // this will be the target of any extracted msol ); const info = beamClient.state.pretty(); @@ -94,13 +103,14 @@ describe("Marinade liquidity pool beam", () => { }); it("can update a state", async () => { - const newTreasury = Keypair.generate(); + const oldTokenAccount = beamClient.state.msolTokenAccount; + const newTokenAccount = Keypair.generate().publicKey; const updateParams = { updateAuthority: beamClient.state.updateAuthority, sunriseState: beamClient.state.sunriseState, vaultAuthorityBump: beamClient.state.vaultAuthorityBump, - treasury: newTreasury.publicKey, - msolTokenAccount: beamClient.state.msolTokenAccount, + msolRecipientBeam: beamClient.state.msolRecipientBeam, + msolTokenAccount: newTokenAccount, marinadeState: beamClient.state.proxyState, }; await sendAndConfirmTransaction( @@ -110,8 +120,18 @@ describe("Marinade liquidity pool beam", () => { ); beamClient = await beamClient.refresh(); - expect(beamClient.state.treasury.toBase58()).to.equal( - newTreasury.publicKey.toBase58(), + expect(beamClient.state.msolTokenAccount.toBase58()).to.equal( + newTokenAccount.toBase58(), + ); + + // change it back :) + await sendAndConfirmTransaction( + provider, + await beamClient.update(provider.publicKey, { + ...updateParams, + msolTokenAccount: oldTokenAccount, + }), + [], ); }); @@ -172,6 +192,20 @@ describe("Marinade liquidity pool beam", () => { ); }); + it("can update the epoch report with zero extractable yield", async () => { + await sendAndConfirmTransaction( + stakerIdentity, + await beamClient.updateEpochReport(), + ); + + // check that the epoch report has been updated + beamClient = await beamClient.refresh(); + // the lp beam is the second one (sp being the first) + const beamEpochDetail = + beamClient.sunrise.state.epochReport.beamEpochDetails[1]; + expectAmount(beamEpochDetail.extractableYield, ZERO); + }); + it("can't deposit due to exceeding allocation", async () => { const shouldFail = sendAndConfirmTransaction( stakerIdentity, @@ -206,7 +240,8 @@ describe("Marinade liquidity pool beam", () => { await beamClient.proportionOfPool(withdrawalAmountBN); const lpSupply = await beamClient.poolTokenSupply(); const withdrawnLpTokens = new BN( - "" + Math.floor(lpSupply.toNumber() * proportionOfPool), + // round up to ensure we withdraw sufficient LP tokens to cover the required SOL amount + "" + Math.ceil(lpSupply.toNumber() * proportionOfPool), ); const expectedLPTokens = vaultBalance.sub(withdrawnLpTokens); @@ -222,6 +257,20 @@ describe("Marinade liquidity pool beam", () => { ); }); + it("can update the epoch report with zero extractable yield", async () => { + await sendAndConfirmTransaction( + stakerIdentity, + await beamClient.updateEpochReport(), + ); + + // check that the epoch report has been updated + beamClient = await beamClient.refresh(); + // the lp beam is the second one (sp being the first) + const beamEpochDetail = + beamClient.sunrise.state.epochReport.beamEpochDetails[1]; + expectAmount(beamEpochDetail.extractableYield, ZERO); + }); + it("can burn gsol", async () => { // burn some gsol to simulate the creation of yield const burnAmount = new BN(1 * LAMPORTS_PER_SOL); @@ -239,14 +288,60 @@ describe("Marinade liquidity pool beam", () => { ); }); - // it("can extract yield", async () => { - // // since we burned some sol - we now have yield to extract (the value of the LPs is higher than the value of the GSOL staked) - // - // await sendAndConfirmTransaction( - // // anyone can extract yield to the yield account, but let's use the staker provider (rather than the admin provider) for this test - // // to show that it doesn't have to be an admin - // stakerIdentity, - // await beamClient.extractYield(), - // ) - // }); + it("can update the epoch report with the extractable yield", async () => { + // in the marinade-lp beam, extractable yield is equivalent to surplus LP tokens + // when LP tokens are redeemed, the result is SOL and mSOL (both sides of the pool) + // the SOL is sent to the yield account, + // and the mSOL is sent to the beam's mSOL token account, which is typically + // the Marinade-SP's beam vault. + // This results in less extractable yield for this beam, and more for the Marinade-SP beam. + // (However, in reality, this beam should rarely be extracted from, as it is + // included as a buffer to allow for fee-less gSOL withdrawals) + const expectedFee = + burnAmount.toNumber() * MARINADE_LP_WITHDRAWAL_FEE_PERCENTAGE; + const effectiveBurnedAmount = burnAmount.subn(expectedFee); + const lpTokenValue = + effectiveBurnedAmount.toNumber() / (await beamClient.poolTokenPrice()); + const lpBalance = await beamClient.calculateBalanceFromLpTokens( + new BN(lpTokenValue), + ); + netExtractableYield = lpBalance.lamports; + + await sendAndConfirmTransaction( + // anyone can update the epoch report, but let's use the staker provider (rather than the admin provider) for this test + // to show that it doesn't have to be an admin + stakerIdentity, + await beamClient.updateEpochReport(), + ); + + // check that the epoch report has been updated + beamClient = await beamClient.refresh(); + // the lp beam is the second one (sp being the first) + const beamEpochDetail = + beamClient.sunrise.state.epochReport.beamEpochDetails[1]; + expectAmount( + netExtractableYield.toNumber(), + beamEpochDetail.extractableYield.toNumber(), + 2, + ); + }); + + it("can extract yield into a stake account", async () => { + // since we burned some sol - we now have yield to extract (the value of the LPs is higher than the value of the GSOL staked) + // The beam performs a delayed unstake to reduce fees, so the result is a stake account with the yield in it. + + await sendAndConfirmTransaction( + // anyone can extract yield to the yield account, but let's use the staker provider (rather than the admin provider) for this test + // to show that it doesn't have to be an admin + stakerIdentity, + await beamClient.extractYield(), + ); + + await expectSolBalance( + beamClient.provider, + beamClient.sunrise.state.yieldAccount, + netExtractableYield, // calculated in the previous test + 1, + ); + }); }); diff --git a/packages/tests/src/functional/beams/marinade-sp.test.ts b/packages/tests/src/functional/beams/marinade-sp.test.ts index 457fd48..50516ca 100644 --- a/packages/tests/src/functional/beams/marinade-sp.test.ts +++ b/packages/tests/src/functional/beams/marinade-sp.test.ts @@ -12,7 +12,7 @@ import { } from "@solana/web3.js"; import BN from "bn.js"; import { - createTokenAccount, + expectSolBalance, expectStakerSolBalance, expectTokenBalance, fund, @@ -25,23 +25,21 @@ import { } from "../../utils.js"; import { provider, staker, stakerIdentity } from "../setup.js"; import { expect } from "chai"; -import { MSOL_MINT } from "../consts.js"; - describe("Marinade stake pool beam", () => { let coreClient: SunriseClient; let beamClient: MarinadeClient; let vaultMsolBalance: BN; let stakerGsolBalance: BN = new BN(0); + let extractableYield: BN; let sunriseStateAddress: PublicKey; - let sunriseDelayedTicket: PublicKey; const depositAmount = 10 * LAMPORTS_PER_SOL; const failedDepositAmount = 5 * LAMPORTS_PER_SOL; const liquidWithdrawalAmount = 5 * LAMPORTS_PER_SOL; - const delayedWithdrawalAmount = 5 * LAMPORTS_PER_SOL; - // const burnAmount = new BN(1 * LAMPORTS_PER_SOL); + const delayedWithdrawalAmount = 1 * LAMPORTS_PER_SOL; + const burnAmount = new BN(1 * LAMPORTS_PER_SOL); before("Set up the sunrise state", async () => { coreClient = await registerSunriseState(); @@ -69,8 +67,6 @@ describe("Marinade stake pool beam", () => { }); it("can initialize a state", async () => { - // create an MSol token account for the beam. - await createTokenAccount(provider, sunriseStateAddress, MSOL_MINT); beamClient = await MarinadeClient.initialize( provider, provider.publicKey, @@ -298,45 +294,57 @@ describe("Marinade stake pool beam", () => { ); }); - // - // it("can burn gsol", async () => { - // // burn some gsol to simulate the creation of yield - // await sendAndConfirmTransaction( - // stakerIdentity, - // await beamClient.burnGSol(burnAmount), - // ); - // - // const expectedGsol = stakerGsolBalance.sub(burnAmount); - // - // await expectTokenBalance( - // beamClient.provider, - // beamClient.sunrise.gsolAssociatedTokenAccount(), - // expectedGsol, - // ); - // }); - // - // it("can extract yield into a stake account", async () => { - // // since we burned some sol - we now have yield to extract (the value of the LPs is higher than the value of the GSOL staked) - // // The beam performs a delayed unstake to reduce fees, so the result is a stake account with the yield in it. - // - // await sendAndConfirmTransaction( - // // anyone can extract yield to the yield account, but let's use the staker provider (rather than the admin provider) for this test - // // to show that it doesn't have to be an admin - // stakerIdentity, - // await beamClient.extractYield(), - // ); - // - // // we burned `burnAmount` gsol, so we should have `burnAmount` - fee in the stake account - // const expectedFee = new BN(0); // TODO - // const expectedExtractedYield = burnAmount.sub(expectedFee); - // - // await expectSolBalance( - // beamClient.provider, - // beamClient.sunrise.state.yieldAccount, - // expectedExtractedYield, - // // // the calculation appears to be slightly inaccurate at present, but in our favour, - // // // so we can leave this as a low priority TODO to improve the accuracy - // // 3000, - // ); - // }); + it("can burn gsol", async () => { + // burn some gsol to simulate the creation of yield + await sendAndConfirmTransaction( + stakerIdentity, + await beamClient.burnGSol(burnAmount), + ); + + const expectedGsol = stakerGsolBalance.sub(burnAmount); + + await expectTokenBalance( + beamClient.provider, + beamClient.sunrise.gsolAssociatedTokenAccount(), + expectedGsol, + ); + }); + + it("can update the epoch report with the extractable yield", async () => { + // we burned `burnAmount` gsol, so we should be able to extract `burnAmount` - estimated fee + const expectedFee = burnAmount.toNumber() * 0.003; + extractableYield = burnAmount.subn(expectedFee); + + await sendAndConfirmTransaction( + // anyone can update the epoch report, but let's use the staker provider (rather than the admin provider) for this test + // to show that it doesn't have to be an admin + stakerIdentity, + await beamClient.updateEpochReport(), + ); + + // check that the epoch report has been updated + beamClient = await beamClient.refresh(); + expect( + beamClient.sunrise.state.epochReport.beamEpochDetails[0].extractableYield.toNumber(), + ).to.equal(extractableYield.toNumber()); + }); + + it("can extract yield into a stake account", async () => { + // since we burned some sol - we now have yield to extract (the value of the LPs is higher than the value of the GSOL staked) + // The beam performs a delayed unstake to reduce fees, so the result is a stake account with the yield in it. + + await sendAndConfirmTransaction( + // anyone can extract yield to the yield account, but let's use the staker provider (rather than the admin provider) for this test + // to show that it doesn't have to be an admin + stakerIdentity, + await beamClient.extractYield(), + ); + + await expectSolBalance( + beamClient.provider, + beamClient.sunrise.state.yieldAccount, + extractableYield, // calculated in the previous test + 1, + ); + }); }); diff --git a/packages/tests/src/functional/beams/spl-stake-pool.test.ts b/packages/tests/src/functional/beams/spl-stake-pool.test.ts index b7d00e0..4c6cead 100644 --- a/packages/tests/src/functional/beams/spl-stake-pool.test.ts +++ b/packages/tests/src/functional/beams/spl-stake-pool.test.ts @@ -24,6 +24,7 @@ describe("SPL stake pool beam", () => { let vaultStakePoolSolBalance: BN; let stakerGsolBalance: BN = new BN(0); let sunriseStateAddress: PublicKey; + let extractableYield: BN; const stakePool: PublicKey = SPL_STAKE_POOL; @@ -202,6 +203,27 @@ describe("SPL stake pool beam", () => { ); }); + it("can update the epoch report with the extractable yield", async () => { + // we burned `burnAmount` gsol, so we should be able to extract `burnAmount` - estimated fee + const expectedFee = burnAmount + .mul(beamClient.spl.stakePoolState.stakeWithdrawalFee.numerator) + .div(beamClient.spl.stakePoolState.stakeWithdrawalFee.denominator); + extractableYield = burnAmount.sub(expectedFee); + + await sendAndConfirmTransaction( + // anyone can update the epoch report, but let's use the staker provider (rather than the admin provider) for this test + // to show that it doesn't have to be an admin + stakerIdentity, + await beamClient.updateEpochReport(), + ); + + // check that the epoch report has been updated + beamClient = await beamClient.refresh(); + expect( + beamClient.sunrise.state.epochReport.beamEpochDetails[0].extractableYield.toNumber(), + ).to.equal(extractableYield.toNumber()); + }); + it("can extract yield into a stake account", async () => { // since we burned some sol - we now have yield to extract (the value of the LPs is higher than the value of the GSOL staked) await sendAndConfirmTransaction( @@ -211,16 +233,10 @@ describe("SPL stake pool beam", () => { await beamClient.extractYield(), ); - // we burned `burnAmount` gsol, so we should have `burnAmount` in the yield account - const expectedFee = burnAmount - .mul(beamClient.spl.stakePoolState.stakeWithdrawalFee.numerator) - .div(beamClient.spl.stakePoolState.stakeWithdrawalFee.denominator); - const expectedExtractedYield = burnAmount.sub(expectedFee); - await expectSolBalance( beamClient.provider, beamClient.sunrise.state.yieldAccount, - expectedExtractedYield, + extractableYield, // the calculation appears to be slightly inaccurate at present, but in our favour, // so we can leave this as a low priority TODO to improve the accuracy 3000, diff --git a/packages/tests/src/functional/consts.ts b/packages/tests/src/functional/consts.ts index b27ed91..45b245d 100644 --- a/packages/tests/src/functional/consts.ts +++ b/packages/tests/src/functional/consts.ts @@ -1,6 +1,11 @@ -// see anchor.toml import { PublicKey } from "@solana/web3.js"; +import BN from "bn.js"; + +export const ZERO = new BN(0); +export const MARINADE_LP_WITHDRAWAL_FEE_PERCENTAGE = 0; + +// see anchor.toml export const SUNRISE_CORE_STATE = new PublicKey( "89wj5p56PTFiKQcHLTkx78jM3Cv4jVRCXgMKJvoFvvp", ); diff --git a/programs/marinade-beam/Cargo.toml b/programs/marinade-beam/Cargo.toml index 707c424..17a5051 100644 --- a/programs/marinade-beam/Cargo.toml +++ b/programs/marinade-beam/Cargo.toml @@ -20,3 +20,4 @@ anchor-lang = '0.29.0' anchor-spl = '0.29.0' marinade-cpi = { git = "https://github.com/sunrise-stake/anchor-gen", branch = "update/anchor-v0.29" } sunrise-core = { path = "../sunrise-core", features = ["cpi"] } +marinade-common = { path = "../../lib/marinade-common" } diff --git a/programs/marinade-beam/src/cpi_interface/marinade.rs b/programs/marinade-beam/src/cpi_interface/marinade.rs index 8d239e1..a29eed5 100644 --- a/programs/marinade-beam/src/cpi_interface/marinade.rs +++ b/programs/marinade-beam/src/cpi_interface/marinade.rs @@ -1,4 +1,8 @@ +use crate::cpi_interface::program::Marinade; +use crate::state::State; +use crate::{ExtractYield, OrderWithdrawal, Withdraw}; use anchor_lang::prelude::*; +use marinade_common::vault_authority_seed::VaultAuthoritySeed; use marinade_cpi::cpi::{ accounts::{ Claim as MarinadeClaim, Deposit as MarinadeDeposit, @@ -22,42 +26,34 @@ pub fn deposit_stake_account(accounts: &crate::DepositStake, validator_index: u3 cpi_deposit_stake_account(cpi_ctx, validator_index) } -pub fn liquid_unstake(accounts: &crate::Withdraw, msol_lamports: u64) -> Result<()> { - let cpi_program = accounts.marinade_program.to_account_info(); - let cpi_ctx = CpiContext::new(cpi_program, accounts.into()); +pub fn liquid_unstake<'info>( + program: &Program<'info, Marinade>, + state: &Account, + accounts: MarinadeLiquidUnstake<'info>, + msol_lamports: u64, +) -> Result<()> { + let cpi_program = program.to_account_info(); + let cpi_ctx = CpiContext::new(cpi_program, accounts); - let bump = &[accounts.state.vault_authority_bump][..]; - let state_address = accounts.state.key(); - let seeds = &[ - state_address.as_ref(), - crate::constants::VAULT_AUTHORITY, - bump, - ][..]; - cpi_liquid_unstake(cpi_ctx.with_signer(&[seeds]), msol_lamports) + let seed_data = VaultAuthoritySeed::new(state); + let seeds = seed_data.as_slices(); + + cpi_liquid_unstake(cpi_ctx.with_signer(&[&seeds[..]]), msol_lamports) } -pub fn order_unstake(accounts: &crate::OrderWithdrawal, msol_lamports: u64) -> Result<()> { - let cpi_program = accounts.marinade_program.to_account_info(); - let cpi_accounts = MarinadeOrderUnstake { - state: accounts.marinade_state.to_account_info(), - msol_mint: accounts.msol_mint.to_account_info(), - burn_msol_from: accounts.msol_vault.to_account_info(), - burn_msol_authority: accounts.vault_authority.to_account_info(), - new_ticket_account: accounts.new_ticket_account.to_account_info(), - token_program: accounts.token_program.to_account_info(), - rent: accounts.rent.to_account_info(), - clock: accounts.clock.to_account_info(), - }; - let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts); - - let bump = &[accounts.state.vault_authority_bump][..]; - let state_address = accounts.state.key(); - let seeds = &[ - state_address.as_ref(), - crate::constants::VAULT_AUTHORITY, - bump, - ][..]; - cpi_order_unstake(cpi_ctx.with_signer(&[seeds]), msol_lamports) +pub fn order_unstake<'info>( + program: &Program<'info, Marinade>, + state: &Account, + accounts: MarinadeOrderUnstake<'info>, + msol_lamports: u64, +) -> Result<()> { + let cpi_program = program.to_account_info(); + let cpi_ctx = CpiContext::new(cpi_program, accounts); + + let seed_data = VaultAuthoritySeed::new(state); + let seeds = seed_data.as_slices(); + + cpi_order_unstake(cpi_ctx.with_signer(&[&seeds[..]]), msol_lamports) } pub fn claim_unstake_ticket(accounts: &crate::RedeemTicket) -> Result<()> { @@ -106,8 +102,8 @@ impl<'a> From<&crate::DepositStake<'a>> for MarinadeDepositStakeAccount<'a> { } } -impl<'a> From<&crate::Withdraw<'a>> for MarinadeLiquidUnstake<'a> { - fn from(accounts: &crate::Withdraw<'a>) -> Self { +impl<'a> From> for MarinadeLiquidUnstake<'a> { + fn from(accounts: Withdraw<'a>) -> Self { Self { state: accounts.marinade_state.to_account_info(), msol_mint: accounts.msol_mint.to_account_info(), @@ -123,8 +119,37 @@ impl<'a> From<&crate::Withdraw<'a>> for MarinadeLiquidUnstake<'a> { } } -impl<'a> From<&crate::OrderWithdrawal<'a>> for MarinadeOrderUnstake<'a> { - fn from(accounts: &crate::OrderWithdrawal<'a>) -> Self { +impl<'a> From<&Withdraw<'a>> for MarinadeLiquidUnstake<'a> { + fn from(accounts: &Withdraw<'a>) -> Self { + accounts.to_owned().into() + } +} + +impl<'a> From> for MarinadeLiquidUnstake<'a> { + fn from(accounts: ExtractYield<'a>) -> Self { + Self { + state: accounts.marinade_state.to_account_info(), + msol_mint: accounts.msol_mint.to_account_info(), + liq_pool_sol_leg_pda: accounts.liq_pool_sol_leg_pda.to_account_info(), + liq_pool_msol_leg: accounts.liq_pool_msol_leg.to_account_info(), + treasury_msol_account: accounts.treasury_msol_account.to_account_info(), + get_msol_from: accounts.msol_vault.to_account_info(), + get_msol_from_authority: accounts.vault_authority.to_account_info(), + transfer_sol_to: accounts.yield_account.to_account_info(), + system_program: accounts.system_program.to_account_info(), + token_program: accounts.token_program.to_account_info(), + } + } +} + +impl<'a> From<&ExtractYield<'a>> for MarinadeLiquidUnstake<'a> { + fn from(accounts: &ExtractYield<'a>) -> Self { + accounts.to_owned().into() + } +} + +impl<'a> From<&OrderWithdrawal<'a>> for MarinadeOrderUnstake<'a> { + fn from(accounts: &OrderWithdrawal<'a>) -> Self { Self { state: accounts.marinade_state.to_account_info(), msol_mint: accounts.msol_mint.to_account_info(), diff --git a/programs/marinade-beam/src/cpi_interface/mod.rs b/programs/marinade-beam/src/cpi_interface/mod.rs index 1e3facc..346ead8 100644 --- a/programs/marinade-beam/src/cpi_interface/mod.rs +++ b/programs/marinade-beam/src/cpi_interface/mod.rs @@ -1,2 +1,3 @@ pub mod marinade; +pub mod program; pub mod sunrise; diff --git a/programs/marinade-beam/src/cpi_interface/program.rs b/programs/marinade-beam/src/cpi_interface/program.rs new file mode 100644 index 0000000..ec627d3 --- /dev/null +++ b/programs/marinade-beam/src/cpi_interface/program.rs @@ -0,0 +1,11 @@ +use anchor_lang::prelude::Pubkey; +use anchor_lang::Id; + +#[derive(Clone)] +pub struct Marinade; + +impl Id for Marinade { + fn id() -> Pubkey { + marinade_cpi::ID + } +} diff --git a/programs/marinade-beam/src/cpi_interface/sunrise.rs b/programs/marinade-beam/src/cpi_interface/sunrise.rs index 2f495cb..0b6455c 100644 --- a/programs/marinade-beam/src/cpi_interface/sunrise.rs +++ b/programs/marinade-beam/src/cpi_interface/sunrise.rs @@ -1,9 +1,10 @@ +use crate::constants::STATE; use anchor_lang::prelude::*; -// TODO: Use actual CPI crate. use sunrise_core as sunrise_core_cpi; use sunrise_core_cpi::cpi::{ - accounts::{BurnGsol, MintGsol}, - burn_gsol as cpi_burn_gsol, mint_gsol as cpi_mint_gsol, + accounts::{BurnGsol, ExtractYield, MintGsol, UpdateEpochReport}, + burn_gsol as cpi_burn_gsol, extract_yield as cpi_extract_yield, mint_gsol as cpi_mint_gsol, + update_epoch_report as cpi_update_epoch_report, }; pub fn mint_gsol<'a>( @@ -48,7 +49,7 @@ impl<'a> From<&crate::Deposit<'a>> for MintGsol<'a> { gsol_mint: accounts.gsol_mint.to_account_info(), gsol_mint_authority: accounts.gsol_mint_authority.to_account_info(), mint_gsol_to: accounts.mint_gsol_to.to_account_info(), - instructions_sysvar: accounts.instructions_sysvar.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), token_program: accounts.token_program.to_account_info(), } } @@ -62,7 +63,7 @@ impl<'a> From<&crate::DepositStake<'a>> for MintGsol<'a> { gsol_mint: accounts.gsol_mint.to_account_info(), gsol_mint_authority: accounts.gsol_mint_authority.to_account_info(), mint_gsol_to: accounts.mint_gsol_to.to_account_info(), - instructions_sysvar: accounts.instructions_sysvar.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), token_program: accounts.token_program.to_account_info(), } } @@ -76,7 +77,7 @@ impl<'a> From<&crate::Withdraw<'a>> for BurnGsol<'a> { gsol_mint: accounts.gsol_mint.to_account_info(), burn_gsol_from_owner: accounts.withdrawer.to_account_info(), burn_gsol_from: accounts.gsol_token_account.to_account_info(), - instructions_sysvar: accounts.instructions_sysvar.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), token_program: accounts.token_program.to_account_info(), } } @@ -90,7 +91,7 @@ impl<'a> From<&crate::OrderWithdrawal<'a>> for BurnGsol<'a> { gsol_mint: accounts.gsol_mint.to_account_info(), burn_gsol_from_owner: accounts.withdrawer.to_account_info(), burn_gsol_from: accounts.gsol_token_account.to_account_info(), - instructions_sysvar: accounts.instructions_sysvar.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), token_program: accounts.token_program.to_account_info(), } } @@ -104,8 +105,63 @@ impl<'a> From<&crate::Burn<'a>> for BurnGsol<'a> { gsol_mint: accounts.gsol_mint.to_account_info(), burn_gsol_from_owner: accounts.burner.to_account_info(), burn_gsol_from: accounts.gsol_token_account.to_account_info(), - instructions_sysvar: accounts.instructions_sysvar.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), token_program: accounts.token_program.to_account_info(), } } } + +pub fn extract_yield<'a>( + accounts: impl Into>, + cpi_program: AccountInfo<'a>, + sunrise_key: Pubkey, + state_bump: u8, + lamports: u64, +) -> Result<()> { + let accounts: ExtractYield<'a> = accounts.into(); + let seeds = [STATE, sunrise_key.as_ref(), &[state_bump]]; + let signer = &[&seeds[..]]; + + cpi_extract_yield( + CpiContext::new(cpi_program, accounts).with_signer(signer), + lamports, + ) +} + +impl<'a> From<&crate::ExtractYield<'a>> for ExtractYield<'a> { + fn from(accounts: &crate::ExtractYield<'a>) -> Self { + Self { + state: accounts.sunrise_state.to_account_info(), + beam: accounts.state.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), + } + } +} + +pub fn update_epoch_report<'a>( + accounts: impl Into>, + cpi_program: AccountInfo<'a>, + sunrise_key: Pubkey, + state_bump: u8, + lamports: u64, +) -> Result<()> { + let accounts: UpdateEpochReport<'a> = accounts.into(); + let seeds = [STATE, sunrise_key.as_ref(), &[state_bump]]; + let signer = &[&seeds[..]]; + + cpi_update_epoch_report( + CpiContext::new(cpi_program, accounts).with_signer(signer), + lamports, + ) +} + +impl<'a> From<&crate::UpdateEpochReport<'a>> for UpdateEpochReport<'a> { + fn from(accounts: &crate::UpdateEpochReport<'a>) -> Self { + Self { + state: accounts.sunrise_state.to_account_info(), + beam: accounts.state.to_account_info(), + gsol_mint: accounts.gsol_mint.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), + } + } +} diff --git a/programs/marinade-beam/src/lib.rs b/programs/marinade-beam/src/lib.rs index 4def027..1423202 100644 --- a/programs/marinade-beam/src/lib.rs +++ b/programs/marinade-beam/src/lib.rs @@ -16,10 +16,10 @@ mod system; use cpi_interface::marinade as marinade_interface; use cpi_interface::sunrise as sunrise_interface; use state::{State, StateEntry}; -use system::accounts::{EpochReport, ProxyTicket}; +use system::accounts::ProxyTicket; use system::utils; -// TODO: Use actual CPI crate. +use crate::cpi_interface::program::Marinade; use sunrise_core as sunrise_core_cpi; declare_id!("G9nMA5HvMa1HLXy1DBA3biH445Zxb2dkqsG4eDfcvgjm"); @@ -29,8 +29,6 @@ mod constants { pub const VAULT_AUTHORITY: &[u8] = b"vault-authority"; /// Seed of this program's state address. pub const STATE: &[u8] = b"sunrise-marinade"; - /// Seed of the epoch report account. - pub const EPOCH_REPORT: &[u8] = b"marinade-epoch-report"; // TODO: RECOVERED_MARGIN is needed because, for some reason, the claim tickets have a couple of lamports less than they should, // probably due to a rounding error converting to and from marinade. // Figure this out and then remove this margin @@ -40,6 +38,8 @@ mod constants { #[program] pub mod marinade_beam { use super::*; + use crate::cpi_interface::marinade; + use marinade_common::calc_msol_from_lamports; pub fn initialize(ctx: Context, input: StateEntry) -> Result<()> { ctx.accounts.state.set_inner(input.into()); @@ -101,12 +101,19 @@ pub mod marinade_beam { pub fn withdraw(ctx: Context, lamports: u64) -> Result<()> { // Calculate how much msol_lamports need to be deposited to unstake `lamports` lamports. - let msol_lamports = - utils::calc_msol_from_lamports(ctx.accounts.marinade_state.as_ref(), lamports)?; + let msol_lamports = calc_msol_from_lamports(ctx.accounts.marinade_state.as_ref(), lamports); + msg!("Liquid Unstake {} msol", msol_lamports); // CPI: Liquid unstake. - marinade_interface::liquid_unstake(ctx.accounts, msol_lamports)?; + let accounts = ctx.accounts.deref().into(); + marinade::liquid_unstake( + &ctx.accounts.marinade_program, + &ctx.accounts.state, + accounts, + msol_lamports, + )?; + msg!("Burn {} lamports", lamports); let bump = ctx.bumps.state; // CPI: Burn GSOL of the same proportion as the number of lamports withdrawn. sunrise_interface::burn_gsol( @@ -122,10 +129,16 @@ pub mod marinade_beam { pub fn order_withdrawal(ctx: Context, lamports: u64) -> Result<()> { // Calculate how much msol_lamports need to be deposited to unstake `lamports` lamports. - let msol_lamports = - utils::calc_msol_from_lamports(ctx.accounts.marinade_state.as_ref(), lamports)?; + let msol_lamports = calc_msol_from_lamports(ctx.accounts.marinade_state.as_ref(), lamports); + // CPI: Order unstake and receive a Marinade unstake ticket. - marinade_interface::order_unstake(ctx.accounts, msol_lamports)?; + let accounts = ctx.accounts.deref().into(); + marinade_interface::order_unstake( + &ctx.accounts.marinade_program, + &ctx.accounts.state, + accounts, + msol_lamports, + )?; // Create a program-owned account mapping the Marinade ticket to the beneficiary that ordered it. let ticket_account = &mut ctx.accounts.proxy_ticket_account; @@ -189,77 +202,67 @@ pub mod marinade_beam { Ok(()) } - pub fn init_epoch_report(ctx: Context, extracted_yield: u64) -> Result<()> { - let extractable_yield = utils::calculate_extractable_yield( + pub fn extract_yield(ctx: Context) -> Result<()> { + let yield_lamports = utils::calculate_extractable_yield( &ctx.accounts.sunrise_state, &ctx.accounts.state, &ctx.accounts.marinade_state, &ctx.accounts.msol_vault, )?; - let mut epoch_report = EpochReport { - state: ctx.accounts.state.key(), - epoch: ctx.accounts.clock.epoch, - tickets: 0, - total_ordered_lamports: 0, - extractable_yield, - extracted_yield: 0, // modified below with remarks - bump: ctx.bumps.epoch_report_account, - }; - // we have to trust that the extracted amount is accurate, - // as extracted yield is no longer managed by the program. - // This is why this instruction is only callable by the update authority - epoch_report.extracted_yield = extracted_yield; + let yield_msol = calc_msol_from_lamports(&ctx.accounts.marinade_state, yield_lamports); - ctx.accounts.epoch_report_account.set_inner(epoch_report); - Ok(()) - } + let yield_account_balance_before = ctx.accounts.yield_account.lamports(); - pub fn update_epoch_report(ctx: Context) -> Result<()> { - // we can update the epoch report if either - // a) the account is at the current epoch or - // b) the account is at the previous epoch and there are no open tickets - - let current_epoch = ctx.accounts.clock.epoch; - let is_previous_epoch = ctx.accounts.epoch_report_account.epoch == current_epoch - 1; - let is_current_epoch = ctx.accounts.epoch_report_account.epoch == current_epoch; - let is_previous_epoch_and_no_open_tickets = - is_previous_epoch && ctx.accounts.epoch_report_account.tickets == 0; - - require!( - is_current_epoch || is_previous_epoch_and_no_open_tickets, - MarinadeBeamError::RemainingUnclaimableTicketAmount - ); + // TODO: Change to use delayed unstake so as not to incur fees. + let accounts = ctx.accounts.deref().into(); + marinade::liquid_unstake( + &ctx.accounts.marinade_program, + &ctx.accounts.state, + accounts, + yield_msol, + )?; - ctx.accounts.epoch_report_account.epoch = ctx.accounts.clock.epoch; + let yield_account_balance_after = ctx.accounts.yield_account.lamports(); + let withdrawn_lamports = + yield_account_balance_after.saturating_sub(yield_account_balance_before); - let extractable_yield = utils::calculate_extractable_yield( - &ctx.accounts.sunrise_state, - &ctx.accounts.state, - &ctx.accounts.marinade_state, - &ctx.accounts.msol_vault, + msg!("Withdrawn {} lamports to yield account", withdrawn_lamports); + + // CPI: update the epoch report with the extracted yield. + let state_bump = ctx.bumps.state; + sunrise_interface::extract_yield( + ctx.accounts.deref(), + ctx.accounts.sunrise_program.to_account_info(), + ctx.accounts.sunrise_state.key(), + state_bump, + withdrawn_lamports, )?; - msg!("Extractable yield: {}", extractable_yield); - ctx.accounts.epoch_report_account.extractable_yield = extractable_yield; + Ok(()) } - pub fn extract_yield(ctx: Context) -> Result<()> { + pub fn update_epoch_report(ctx: Context) -> Result<()> { let yield_lamports = utils::calculate_extractable_yield( &ctx.accounts.sunrise_state, &ctx.accounts.state, &ctx.accounts.marinade_state, &ctx.accounts.msol_vault, )?; - ctx.accounts - .epoch_report_account - .add_extracted_yield(yield_lamports); - let yield_msol = - utils::calc_msol_from_lamports(&ctx.accounts.marinade_state, yield_lamports)?; - // TODO: Change to use delayed unstake so as not to incur fees. - msg!("Withdrawing {} msol to yield account", yield_msol); - // TODO: Legacy code uses liquid-unstake but leaves the note above. - // Move to delayed-unstakes here? + // Reduce by fee + // TODO can we do better than an estimate? + let extractable_lamports = (yield_lamports as f64) * 0.997; // estimated 0.3% unstake fee + + // CPI: update the epoch report with the extracted yield. + let state_bump = ctx.bumps.state; + sunrise_interface::update_epoch_report( + ctx.accounts.deref(), + ctx.accounts.sunrise_program.to_account_info(), + ctx.accounts.sunrise_state.key(), + state_bump, + extractable_lamports as u64, + )?; + Ok(()) } } @@ -354,7 +357,7 @@ pub struct Deposit<'info> { /// CHECK: Checked by Sunrise CPI. pub gsol_mint_authority: UncheckedAccount<'info>, /// CHECK: Checked by Sunrise CPI. - pub instructions_sysvar: UncheckedAccount<'info>, + pub sysvar_instructions: UncheckedAccount<'info>, #[account(mut)] /// CHECK: Checked by Marinade CPI. @@ -371,10 +374,7 @@ pub struct Deposit<'info> { pub reserve_pda: UncheckedAccount<'info>, pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, - #[account(address = marinade_cpi::ID)] - /// CHECK: The Marinade program ID. - pub marinade_program: UncheckedAccount<'info>, - + pub marinade_program: Program<'info, Marinade>, pub system_program: Program<'info, System>, pub token_program: Program<'info, Token>, } @@ -429,7 +429,7 @@ pub struct DepositStake<'info> { /// CHECK: Checked by Sunrise CPI. pub gsol_mint_authority: UncheckedAccount<'info>, /// CHECK: Checked by Sunrise CPI. - pub instructions_sysvar: UncheckedAccount<'info>, + pub sysvar_instructions: UncheckedAccount<'info>, #[account(mut)] /// CHECK: Checked by Marinade CPI. @@ -446,17 +446,14 @@ pub struct DepositStake<'info> { pub stake_program: UncheckedAccount<'info>, pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, - #[account(address = marinade_cpi::ID)] - /// CHECK: The Marinade program ID. - pub marinade_program: UncheckedAccount<'info>, - + pub marinade_program: Program<'info, Marinade>, pub clock: Sysvar<'info, Clock>, pub rent: Sysvar<'info, Rent>, pub system_program: Program<'info, System>, pub token_program: Program<'info, Token>, } -#[derive(Accounts)] +#[derive(Accounts, Clone)] pub struct Withdraw<'info> { #[account( mut, @@ -510,13 +507,10 @@ pub struct Withdraw<'info> { pub treasury_msol_account: UncheckedAccount<'info>, /// CHECK: Checked by Sunrise CPI. - pub instructions_sysvar: UncheckedAccount<'info>, + pub sysvar_instructions: UncheckedAccount<'info>, pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, - #[account(address = marinade_cpi::ID)] - /// CHECK: The Marinade Program ID. - pub marinade_program: UncheckedAccount<'info>, - + pub marinade_program: Program<'info, Marinade>, pub system_program: Program<'info, System>, pub token_program: Program<'info, Token>, } @@ -565,7 +559,7 @@ pub struct OrderWithdrawal<'info> { pub vault_authority: UncheckedAccount<'info>, /// CHECK: Checked by Sunrise CPI. - pub instructions_sysvar: UncheckedAccount<'info>, + pub sysvar_instructions: UncheckedAccount<'info>, /// CHECK: Checked by Marinade CPI. #[account(mut)] @@ -578,10 +572,7 @@ pub struct OrderWithdrawal<'info> { pub proxy_ticket_account: Box>, pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, - #[account(address = marinade_cpi::ID)] - /// CHECK: The Marinade Program ID. - pub marinade_program: UncheckedAccount<'info>, - + pub marinade_program: Program<'info, Marinade>, pub clock: Sysvar<'info, Clock>, pub rent: Sysvar<'info, Rent>, pub system_program: Program<'info, System>, @@ -621,10 +612,7 @@ pub struct RedeemTicket<'info> { )] pub vault_authority: UncheckedAccount<'info>, - #[account(address = marinade_cpi::ID)] - /// CHECK: The Marinade program ID. - pub marinade_program: UncheckedAccount<'info>, - + pub marinade_program: Program<'info, Marinade>, pub clock: Sysvar<'info, Clock>, pub system_program: Program<'info, System>, } @@ -666,7 +654,7 @@ pub struct Burn<'info> { /// Verified in CPI to Sunrise program. pub gsol_mint: Box>, /// CHECK: Checked by Sunrise CPI. - pub instructions_sysvar: UncheckedAccount<'info>, + pub sysvar_instructions: UncheckedAccount<'info>, pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, } @@ -674,12 +662,15 @@ pub struct Burn<'info> { #[derive(Accounts, Clone)] pub struct ExtractYield<'info> { #[account( - has_one = marinade_state, - has_one = sunrise_state + has_one = marinade_state, + has_one = sunrise_state, + seeds = [constants::STATE, sunrise_state.key().as_ref()], + bump )] pub state: Box>, #[account( - has_one = yield_account + mut, // Update the extracted yield on the state's epoch report. + has_one = yield_account )] pub sunrise_state: Box>, #[account(mut)] @@ -711,115 +702,73 @@ pub struct ExtractYield<'info> { /// CHECK: Matches the yield account key stored in the state. pub yield_account: UncheckedAccount<'info>, - #[account( - mut, - seeds = [state.key().as_ref(), constants::EPOCH_REPORT], - bump = epoch_report_account.bump, - constraint = epoch_report_account.epoch == clock.epoch @ MarinadeBeamError::InvalidEpochReportAccount - )] - pub epoch_report_account: Box>, - - pub clock: Sysvar<'info, Clock>, - //pub system_program: Program<'info, System>, - //pub token_program: Program<'info, Token>, - //pub marinade_program: Program<'info, MarinadeFinance>, -} - -#[derive(Accounts, Clone)] -pub struct InitEpochReport<'info> { - #[account( - has_one = marinade_state, - has_one = sunrise_state, - has_one = update_authority - )] - pub state: Box>, - pub sunrise_state: Box>, - #[account(has_one = msol_mint)] - pub marinade_state: Box>, - #[account(mut)] - pub payer: Signer<'info>, - pub update_authority: Signer<'info>, - - #[account( - init, - space = EpochReport::SPACE, - payer = payer, - seeds = [state.key().as_ref(), constants::EPOCH_REPORT], - bump, - )] - pub epoch_report_account: Box>, - + /// CHECK: Checked by Marinade CPI. + pub liq_pool_sol_leg_pda: UncheckedAccount<'info>, #[account(mut)] - pub msol_mint: Box>, - #[account( - mut, - token::mint = msol_mint, - token::authority = vault_authority, - )] - pub msol_vault: Box>, - /// CHECK: Seeds of the MSOL vault authority. - #[account( - seeds = [ - state.key().as_ref(), - constants::VAULT_AUTHORITY - ], - bump = state.vault_authority_bump - )] - pub vault_authority: UncheckedAccount<'info>, + /// CHECK: Checked by Marinade CPI. + pub liq_pool_msol_leg: UncheckedAccount<'info>, + #[account(mut)] + /// CHECK: Checked by Marinade CPI. + pub treasury_msol_account: UncheckedAccount<'info>, - pub clock: Sysvar<'info, Clock>, + /// CHECK: Checked by Sunrise CPI. + pub sysvar_instructions: UncheckedAccount<'info>, + + pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, + pub marinade_program: Program<'info, Marinade>, pub system_program: Program<'info, System>, + pub token_program: Program<'info, Token>, } -#[derive(Accounts)] +#[derive(Accounts, Clone)] pub struct UpdateEpochReport<'info> { #[account( has_one = marinade_state, - has_one = sunrise_state + has_one = sunrise_state, + seeds = [constants::STATE, sunrise_state.key().as_ref()], + bump )] pub state: Box>, + #[account( + mut, // Update the extractable yield on the state's epoch report. + )] pub sunrise_state: Box>, - #[account(has_one = msol_mint)] - pub marinade_state: Box>, - - #[account(mut)] - pub payer: Signer<'info>, - #[account( - mut, - seeds = [state.key().as_ref(), constants::EPOCH_REPORT], - bump = epoch_report_account.bump, + has_one = msol_mint, )] - pub epoch_report_account: Box>, + pub marinade_state: Box>, - #[account(mut)] pub msol_mint: Box>, #[account( - mut, token::mint = msol_mint, token::authority = vault_authority, )] pub msol_vault: Box>, /// CHECK: Seeds of the MSOL vault authority. #[account( - seeds = [ - state.key().as_ref(), - constants::VAULT_AUTHORITY - ], - bump = state.vault_authority_bump + seeds = [ + state.key().as_ref(), + constants::VAULT_AUTHORITY + ], + bump = state.vault_authority_bump )] pub vault_authority: UncheckedAccount<'info>, - pub clock: Sysvar<'info, Clock>, + /// Required to update the core state epoch report + /// Verified in CPI to Sunrise program. + pub gsol_mint: Account<'info, Mint>, + + /// CHECK: Checked by Sunrise CPI. + pub sysvar_instructions: UncheckedAccount<'info>, + + pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, } #[error_code] pub enum MarinadeBeamError { #[msg("No delegation for stake account deposit")] NotDelegated, - #[msg("An error occurred during calculation")] - CalculationFailure, #[msg("The epoch report account has not been updated to the current epoch yet")] InvalidEpochReportAccount, #[msg("The total ordered ticket amount exceeds the amount in all found tickets")] diff --git a/programs/marinade-beam/src/state.rs b/programs/marinade-beam/src/state.rs index 91cb12f..03b71a4 100644 --- a/programs/marinade-beam/src/state.rs +++ b/programs/marinade-beam/src/state.rs @@ -1,4 +1,5 @@ use anchor_lang::prelude::*; +use marinade_common::vault_authority_seed::HasVaultAuthority; #[account] pub struct State { @@ -16,6 +17,12 @@ pub struct State { pub vault_authority_bump: u8, } +impl HasVaultAuthority for State { + fn vault_authority_bump(&self) -> u8 { + self.vault_authority_bump + } +} + // Anchor-ts only supports deserialization(in instruction arguments) for types // that explicitly derive AnchorSerialize & AnchorDeserialize. // https://github.com/coral-xyz/anchor/issues/2545 diff --git a/programs/marinade-beam/src/system/accounts.rs b/programs/marinade-beam/src/system/accounts.rs index 1b057d8..648b91c 100644 --- a/programs/marinade-beam/src/system/accounts.rs +++ b/programs/marinade-beam/src/system/accounts.rs @@ -1,4 +1,3 @@ -use crate::MarinadeBeamError; use anchor_lang::prelude::*; /// Maps a Marinade ticket account to a GSOL token holder @@ -11,47 +10,3 @@ pub struct ProxyTicket { impl ProxyTicket { pub const SPACE: usize = 32 + 32 + 32 + 8 /* DISCRIMINATOR */; } - -#[account] -pub struct EpochReport { - pub state: Pubkey, - pub epoch: u64, - pub tickets: u64, - pub total_ordered_lamports: u64, - pub extractable_yield: u64, - pub extracted_yield: u64, - pub bump: u8, -} - -impl EpochReport { - pub const SPACE: usize = 32 + 8 + 8 + 8 + 8 + 8 + 1 + 8 /* DISCRIMINATOR */ ; - - pub fn all_extractable_yield(&self) -> u64 { - self.extractable_yield - .checked_add(self.extracted_yield) - .unwrap() - } - - pub fn add_ticket(&mut self, ticket_amount_lamports: u64, clock: &Sysvar) -> Result<()> { - require_eq!( - self.epoch, - clock.epoch, - MarinadeBeamError::InvalidEpochReportAccount - ); - self.tickets = self.tickets.checked_add(1).unwrap(); - self.total_ordered_lamports = self - .total_ordered_lamports - .checked_add(ticket_amount_lamports) - .unwrap(); - Ok(()) - } - - pub fn add_extracted_yield(&mut self, extracted_yield: u64) { - self.extracted_yield = self.extracted_yield.checked_add(extracted_yield).unwrap(); - } - - pub fn update_report(&mut self, extractable_yield: u64, add_extracted_yield: u64) { - self.extractable_yield = extractable_yield; - self.add_extracted_yield(add_extracted_yield); - } -} diff --git a/programs/marinade-beam/src/system/utils.rs b/programs/marinade-beam/src/system/utils.rs index 5f7caab..77d38af 100644 --- a/programs/marinade-beam/src/system/utils.rs +++ b/programs/marinade-beam/src/system/utils.rs @@ -4,6 +4,7 @@ use anchor_lang::solana_program::{ borsh0_10::try_from_slice_unchecked, stake::state::StakeStateV2, }; use anchor_spl::token::TokenAccount; +use marinade_common::calc_lamports_from_msol_amount; use marinade_cpi::state::State as MarinadeState; use sunrise_core::BeamError; @@ -14,8 +15,7 @@ pub fn calculate_extractable_yield( marinade_state: &MarinadeState, msol_vault: &TokenAccount, ) -> Result { - let staked_value = - super::utils::calc_lamports_from_msol_amount(marinade_state, msol_vault.amount)?; + let staked_value = calc_lamports_from_msol_amount(marinade_state, msol_vault.amount); let details = sunrise_state .get_beam_details(&beam_state.key()) .ok_or(BeamError::UnidentifiedBeam)?; @@ -23,45 +23,6 @@ pub fn calculate_extractable_yield( Ok(staked_value.saturating_sub(staked_sol)) } -/// calculate amount*numerator/denominator -/// as value = shares * share_price where share_price=total_value/total_shares -/// or shares = amount_value / share_price where share_price=total_value/total_shares -/// => shares = amount_value * 1/share_price where 1/share_price=total_shares/total_value -pub fn proportional(amount: u64, numerator: u64, denominator: u64) -> Result { - if denominator == 0 { - return Ok(amount); - } - u64::try_from((amount as u128) * (numerator as u128) / (denominator as u128)) - .map_err(|_| error!(crate::MarinadeBeamError::CalculationFailure)) -} - -// All lifted from https://github.com/marinade-finance/liquid-staking-program/blob/447f9607a8c755cac7ad63223febf047142c6c8f/programs/marinade-finance/src/state.rs#L227 -pub fn calc_msol_from_lamports(marinade_state: &MarinadeState, stake_lamports: u64) -> Result { - msg!("calc_msol_from_lamports"); - msg!("stake_lamports: {}", stake_lamports); - msg!("marinade_state.msol_supply: {}", marinade_state.msol_supply); - msg!( - "total_virtual_staked_lamports: {}", - total_virtual_staked_lamports(marinade_state) - ); - proportional( - stake_lamports, - marinade_state.msol_supply, - total_virtual_staked_lamports(marinade_state), - ) -} - -pub fn calc_lamports_from_msol_amount( - marinade_state: &MarinadeState, - msol_amount: u64, -) -> Result { - proportional( - msol_amount, - total_virtual_staked_lamports(marinade_state), - marinade_state.msol_supply, - ) -} - pub fn get_delegated_stake_amount(stake_account: &AccountInfo) -> Result { // Gets the active stake amount of the stake account. We need this to determine how much gSol to mint. let stake_state = try_from_slice_unchecked::(&stake_account.data.borrow())?; @@ -71,27 +32,3 @@ pub fn get_delegated_stake_amount(stake_account: &AccountInfo) -> Result { None => Err(crate::MarinadeBeamError::NotDelegated.into()), } } - -fn total_cooling_down(marinade_state: &MarinadeState) -> u64 { - marinade_state - .stake_system - .delayed_unstake_cooling_down - .checked_add(marinade_state.emergency_cooling_down) - .expect("Total cooling down overflow") -} - -fn total_lamports_under_control(marinade_state: &MarinadeState) -> u64 { - marinade_state - .validator_system - .total_active_balance - .checked_add(total_cooling_down(marinade_state)) - .expect("Stake balance overflow") - .checked_add(marinade_state.available_reserve_balance) // reserve_pda.lamports() - self.rent_exempt_for_token_acc - .expect("Total SOLs under control overflow") -} - -fn total_virtual_staked_lamports(marinade_state: &MarinadeState) -> u64 { - // if we get slashed it may be negative but we must use 0 instead - total_lamports_under_control(marinade_state) - .saturating_sub(marinade_state.circulating_ticket_balance) //tickets created -> cooling down lamports or lamports already in reserve and not claimed yet -} diff --git a/programs/marinade-lp-beam/Cargo.toml b/programs/marinade-lp-beam/Cargo.toml index dab7720..14a3e05 100644 --- a/programs/marinade-lp-beam/Cargo.toml +++ b/programs/marinade-lp-beam/Cargo.toml @@ -19,4 +19,5 @@ default = [] anchor-lang = '0.29.0' anchor-spl = '0.29.0' marinade-cpi = { git = "https://github.com/sunrise-stake/anchor-gen", branch = "update/anchor-v0.29" } -sunrise-core = { path = "../sunrise-core", features = ["cpi"] } \ No newline at end of file +sunrise-core = { path = "../sunrise-core", features = ["cpi"] } +marinade-common = { path = "../../lib/marinade-common" } \ No newline at end of file diff --git a/programs/marinade-lp-beam/src/cpi_interface/marinade_lp.rs b/programs/marinade-lp-beam/src/cpi_interface/marinade_lp.rs index b662635..a8267bb 100644 --- a/programs/marinade-lp-beam/src/cpi_interface/marinade_lp.rs +++ b/programs/marinade-lp-beam/src/cpi_interface/marinade_lp.rs @@ -1,4 +1,7 @@ +use crate::cpi_interface::program::Marinade; +use crate::state::State; use anchor_lang::prelude::*; +use marinade_common::vault_authority_seed::VaultAuthoritySeed; use marinade_cpi::cpi::{ accounts::{AddLiquidity as MarinadeAddLiquidity, RemoveLiquidity as MarinadeRemoveLiquidity}, add_liquidity as marinade_add_liquidity, remove_liquidity as marinade_remove_liquidity, @@ -19,18 +22,19 @@ pub fn add_liquidity( Ok(()) } -pub fn remove_liquidity(accounts: &crate::Withdraw, liq_pool_tokens_amount: u64) -> Result<()> { - let bump = &[accounts.state.vault_authority_bump][..]; - let state_address = accounts.state.key(); - let seeds = &[ - state_address.as_ref(), - crate::constants::VAULT_AUTHORITY, - bump, - ][..]; +pub fn remove_liquidity<'info>( + program: &Program<'info, Marinade>, + state: &Account, + accounts: MarinadeRemoveLiquidity<'info>, + liq_pool_tokens_amount: u64, +) -> Result<()> { + let cpi_program = program.to_account_info(); + let cpi_ctx = CpiContext::new(cpi_program, accounts); - let cpi_program = accounts.marinade_program.to_account_info(); - let cpi_ctx = CpiContext::new(cpi_program, accounts.into()); - marinade_remove_liquidity(cpi_ctx.with_signer(&[seeds]), liq_pool_tokens_amount)?; + let seed_data = VaultAuthoritySeed::new(state); + let seeds = seed_data.as_slices(); + + marinade_remove_liquidity(cpi_ctx.with_signer(&[&seeds[..]]), liq_pool_tokens_amount)?; Ok(()) } @@ -68,3 +72,21 @@ impl<'a> From<&crate::Withdraw<'a>> for MarinadeRemoveLiquidity<'a> { } } } + +impl<'a> From<&crate::ExtractYield<'a>> for MarinadeRemoveLiquidity<'a> { + fn from(accounts: &crate::ExtractYield<'a>) -> MarinadeRemoveLiquidity<'a> { + Self { + state: accounts.marinade_state.to_account_info(), + lp_mint: accounts.liq_pool_mint.to_account_info(), + burn_from: accounts.liq_pool_token_vault.to_account_info(), + burn_from_authority: accounts.vault_authority.to_account_info(), + transfer_sol_to: accounts.yield_account.to_account_info(), + transfer_msol_to: accounts.transfer_msol_to.to_account_info(), + liq_pool_sol_leg_pda: accounts.liq_pool_sol_leg_pda.to_account_info(), + liq_pool_msol_leg: accounts.liq_pool_msol_leg.to_account_info(), + liq_pool_msol_leg_authority: accounts.liq_pool_msol_leg_authority.to_account_info(), + system_program: accounts.system_program.to_account_info(), + token_program: accounts.token_program.to_account_info(), + } + } +} diff --git a/programs/marinade-lp-beam/src/cpi_interface/mod.rs b/programs/marinade-lp-beam/src/cpi_interface/mod.rs index 8204ba7..0e040a1 100644 --- a/programs/marinade-lp-beam/src/cpi_interface/mod.rs +++ b/programs/marinade-lp-beam/src/cpi_interface/mod.rs @@ -1,2 +1,3 @@ pub mod marinade_lp; +pub mod program; pub mod sunrise; diff --git a/programs/marinade-lp-beam/src/cpi_interface/program.rs b/programs/marinade-lp-beam/src/cpi_interface/program.rs new file mode 100644 index 0000000..ec627d3 --- /dev/null +++ b/programs/marinade-lp-beam/src/cpi_interface/program.rs @@ -0,0 +1,11 @@ +use anchor_lang::prelude::Pubkey; +use anchor_lang::Id; + +#[derive(Clone)] +pub struct Marinade; + +impl Id for Marinade { + fn id() -> Pubkey { + marinade_cpi::ID + } +} diff --git a/programs/marinade-lp-beam/src/cpi_interface/sunrise.rs b/programs/marinade-lp-beam/src/cpi_interface/sunrise.rs index 11ca030..b36eb57 100644 --- a/programs/marinade-lp-beam/src/cpi_interface/sunrise.rs +++ b/programs/marinade-lp-beam/src/cpi_interface/sunrise.rs @@ -1,9 +1,12 @@ use anchor_lang::prelude::*; // TODO: Use actual CPI crate. +use crate::constants::STATE; use sunrise_core as sunrise_core_cpi; +use sunrise_core::cpi::accounts::{ExtractYield, UpdateEpochReport}; use sunrise_core_cpi::cpi::{ - accounts::{BurnGsol, MintGsol}, - burn_gsol as cpi_burn_gsol, mint_gsol as cpi_mint_gsol, + accounts::{BurnGsol, MintGsol, TransferGsol}, + burn_gsol as cpi_burn_gsol, extract_yield as cpi_extract_yield, mint_gsol as cpi_mint_gsol, + transfer_gsol as cpi_transfer_gsol, update_epoch_report as cpi_update_epoch_report, }; pub fn mint_gsol<'a>( @@ -23,6 +26,20 @@ pub fn mint_gsol<'a>( ) } +impl<'a> From<&crate::Deposit<'a>> for MintGsol<'a> { + fn from(accounts: &crate::Deposit<'a>) -> Self { + Self { + state: accounts.sunrise_state.to_account_info(), + beam: accounts.state.to_account_info(), + gsol_mint: accounts.gsol_mint.to_account_info(), + gsol_mint_authority: accounts.gsol_mint_authority.to_account_info(), + mint_gsol_to: accounts.mint_gsol_to.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), + token_program: accounts.token_program.to_account_info(), + } + } +} + pub fn burn_gsol<'a>( accounts: impl Into>, cpi_program: AccountInfo<'a>, @@ -40,44 +57,115 @@ pub fn burn_gsol<'a>( ) } -impl<'a> From<&crate::Deposit<'a>> for MintGsol<'a> { - fn from(accounts: &crate::Deposit<'a>) -> Self { +impl<'a> From<&crate::Withdraw<'a>> for BurnGsol<'a> { + fn from(accounts: &crate::Withdraw<'a>) -> Self { Self { state: accounts.sunrise_state.to_account_info(), beam: accounts.state.to_account_info(), gsol_mint: accounts.gsol_mint.to_account_info(), - gsol_mint_authority: accounts.gsol_mint_authority.to_account_info(), - mint_gsol_to: accounts.mint_gsol_to.to_account_info(), - instructions_sysvar: accounts.instructions_sysvar.to_account_info(), + burn_gsol_from_owner: accounts.withdrawer.to_account_info(), + burn_gsol_from: accounts.gsol_token_account.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), token_program: accounts.token_program.to_account_info(), } } } -impl<'a> From<&crate::Withdraw<'a>> for BurnGsol<'a> { - fn from(accounts: &crate::Withdraw<'a>) -> Self { +impl<'a> From<&crate::Burn<'a>> for BurnGsol<'a> { + fn from(accounts: &crate::Burn<'a>) -> Self { Self { state: accounts.sunrise_state.to_account_info(), beam: accounts.state.to_account_info(), gsol_mint: accounts.gsol_mint.to_account_info(), - burn_gsol_from_owner: accounts.withdrawer.to_account_info(), + burn_gsol_from_owner: accounts.burner.to_account_info(), burn_gsol_from: accounts.gsol_token_account.to_account_info(), - instructions_sysvar: accounts.instructions_sysvar.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), token_program: accounts.token_program.to_account_info(), } } } -impl<'a> From<&crate::Burn<'a>> for BurnGsol<'a> { - fn from(accounts: &crate::Burn<'a>) -> Self { +pub fn transfer_gsol<'a>( + accounts: impl Into>, + cpi_program: AccountInfo<'a>, + sunrise_key: Pubkey, + state_bump: u8, + recipient_beam: Pubkey, + lamports: u64, +) -> Result<()> { + let accounts: TransferGsol<'a> = accounts.into(); + let seeds = [crate::constants::STATE, sunrise_key.as_ref(), &[state_bump]]; + let signer = &[&seeds[..]]; + + cpi_transfer_gsol( + CpiContext::new(cpi_program, accounts).with_signer(signer), + recipient_beam, + lamports, + ) +} + +impl<'a> From<&crate::Withdraw<'a>> for TransferGsol<'a> { + fn from(accounts: &crate::Withdraw<'a>) -> Self { Self { state: accounts.sunrise_state.to_account_info(), beam: accounts.state.to_account_info(), gsol_mint: accounts.gsol_mint.to_account_info(), - burn_gsol_from_owner: accounts.burner.to_account_info(), - burn_gsol_from: accounts.gsol_token_account.to_account_info(), - instructions_sysvar: accounts.instructions_sysvar.to_account_info(), - token_program: accounts.token_program.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), + } + } +} + +pub fn extract_yield<'a>( + accounts: impl Into>, + cpi_program: AccountInfo<'a>, + sunrise_key: Pubkey, + state_bump: u8, + lamports: u64, +) -> Result<()> { + let accounts: ExtractYield<'a> = accounts.into(); + let seeds = [STATE, sunrise_key.as_ref(), &[state_bump]]; + let signer = &[&seeds[..]]; + + cpi_extract_yield( + CpiContext::new(cpi_program, accounts).with_signer(signer), + lamports, + ) +} + +impl<'a> From<&crate::ExtractYield<'a>> for ExtractYield<'a> { + fn from(accounts: &crate::ExtractYield<'a>) -> Self { + Self { + state: accounts.sunrise_state.to_account_info(), + beam: accounts.state.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), + } + } +} + +pub fn update_epoch_report<'a>( + accounts: impl Into>, + cpi_program: AccountInfo<'a>, + sunrise_key: Pubkey, + state_bump: u8, + lamports: u64, +) -> Result<()> { + let accounts: UpdateEpochReport<'a> = accounts.into(); + let seeds = [STATE, sunrise_key.as_ref(), &[state_bump]]; + let signer = &[&seeds[..]]; + + cpi_update_epoch_report( + CpiContext::new(cpi_program, accounts).with_signer(signer), + lamports, + ) +} + +impl<'a> From<&crate::UpdateEpochReport<'a>> for UpdateEpochReport<'a> { + fn from(accounts: &crate::UpdateEpochReport<'a>) -> Self { + Self { + state: accounts.sunrise_state.to_account_info(), + beam: accounts.state.to_account_info(), + gsol_mint: accounts.gsol_mint.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), } } } diff --git a/programs/marinade-lp-beam/src/lib.rs b/programs/marinade-lp-beam/src/lib.rs index e24e3a9..b70111f 100644 --- a/programs/marinade-lp-beam/src/lib.rs +++ b/programs/marinade-lp-beam/src/lib.rs @@ -2,24 +2,23 @@ // Temporarily allow to pass clippy ci #![allow(dead_code)] +use crate::cpi_interface::program::Marinade; use anchor_lang::prelude::*; use anchor_spl::associated_token::{AssociatedToken, Create}; use anchor_spl::token::{Mint, Token, TokenAccount}; +use cpi_interface::marinade_lp as marinade_lp_interface; +use cpi_interface::sunrise as sunrise_interface; use marinade_cpi::State as MarinadeState; +use state::{State, StateEntry}; +use std::cmp::max; use std::ops::Deref; +use sunrise_core as sunrise_core_cpi; +use system::utils; mod cpi_interface; mod state; mod system; -use cpi_interface::marinade_lp as marinade_lp_interface; -use cpi_interface::sunrise as sunrise_interface; -use state::{State, StateEntry}; -use system::utils; - -// TODO: Use actual CPI crate. -use sunrise_core as sunrise_core_cpi; - declare_id!("9Xek4q2hsdPm4yaRt4giQnVTTgRGwGhXQ1HBXbinuPTP"); mod constants { @@ -32,6 +31,9 @@ mod constants { #[program] pub mod marinade_lp_beam { use super::*; + use crate::cpi_interface::marinade_lp; + use crate::system::utils::get_extractable_yield_from_excess_balance; + use marinade_common::calc_lamports_from_msol_amount; pub fn initialize(ctx: Context, input: StateEntry) -> Result<()> { ctx.accounts.state.set_inner(input.into()); @@ -72,16 +74,37 @@ pub mod marinade_lp_beam { } pub fn withdraw(ctx: Context, lamports: u64) -> Result<()> { - // Calculate the number of liq_pool tokens `lamports` is worth. - let liq_pool_tokens = utils::liq_pool_tokens_from_lamports( - &ctx.accounts.marinade_state, - &ctx.accounts.liq_pool_mint, - &ctx.accounts.liq_pool_sol_leg_pda, + // Calculate the number of liq_pool tokens that would be needed to withdraw `lamports` + let liq_pool_balance_to_withdraw = + utils::calculate_liq_pool_balance_required_to_withdraw_lamports( + &ctx.accounts.marinade_state, + &ctx.accounts.liq_pool_mint, + &ctx.accounts.liq_pool_sol_leg_pda, + &ctx.accounts.liq_pool_msol_leg, + lamports, + )?; + + msg!( + "Balance required to withdraw {} lamports: {:?}", lamports, - )?; + liq_pool_balance_to_withdraw + ); // CPI: Remove liquidity from Marinade liq_pool. The liq_pool tokens vault owned by // this vault is the source burning lp tokens. - marinade_lp_interface::remove_liquidity(ctx.accounts, liq_pool_tokens)?; + // The result is a combination of SOL and mSOL. + // The SOL goes to the withdrawer, and the mSOL goes to the designated mSOL token account. + // which is typically the Marinade-SP's beam vault. + // NOTE: This results in an asymmetry in the amount of gSOL each beam is responsible for + // The lamport amount is burned, and the mSOL amount has been effectively moved from + // the beam to the Marinade-SP (or whichever beam owns the msol token account) + let accounts = ctx.accounts.deref().into(); + marinade_lp::remove_liquidity( + &ctx.accounts.marinade_program, + &ctx.accounts.state, + accounts, + // this is safe, because the liq_pool_balance is guaranteed to be non-negative + liq_pool_balance_to_withdraw.liq_pool_token as u64, + )?; let state_bump = ctx.bumps.state; // CPI: Burn GSOL of the same proportion as the lamports withdrawn from the depositor. @@ -93,6 +116,19 @@ pub mod marinade_lp_beam { lamports, )?; + let lamport_value_of_msol = calc_lamports_from_msol_amount( + &ctx.accounts.marinade_state, + liq_pool_balance_to_withdraw.msol as u64, + ); + sunrise_interface::transfer_gsol( + ctx.accounts.deref(), + ctx.accounts.sunrise_program.to_account_info(), + ctx.accounts.sunrise_state.key(), + state_bump, + ctx.accounts.state.msol_recipient_beam, + lamport_value_of_msol, + )?; + Ok(()) } @@ -121,6 +157,83 @@ pub mod marinade_lp_beam { // Marinade liq_pool only supports immediate withdrawals. Err(MarinadeLpBeamError::Unimplemented.into()) } + + pub fn extract_yield(ctx: Context) -> Result<()> { + let yield_balance = utils::calculate_extractable_yield( + &ctx.accounts.sunrise_state, + &ctx.accounts.state, + &ctx.accounts.marinade_state, + &ctx.accounts.liq_pool_mint, + &ctx.accounts.liq_pool_token_vault, + &ctx.accounts.liq_pool_sol_leg_pda, + &ctx.accounts.liq_pool_msol_leg, + )?; + + // if there is insufficient balance to extract, return an error + // note - if this is negative, we have a shortfall in the pool + // this should only ever be max 1 or 2 lamports due to rounding + require_gt!( + yield_balance.liq_pool_token, + 0, + MarinadeLpBeamError::InsufficientYieldBalance + ); + + let yield_account_balance_before = ctx.accounts.yield_account.lamports(); + + let accounts = ctx.accounts.deref().into(); + marinade_lp::remove_liquidity( + &ctx.accounts.marinade_program, + &ctx.accounts.state, + accounts, + // checked by the assert above - guaranteed to be positive + yield_balance.liq_pool_token as u64, + )?; + + let yield_account_balance_after = ctx.accounts.yield_account.lamports(); + let withdrawn_lamports = + yield_account_balance_after.saturating_sub(yield_account_balance_before); + + msg!("Withdrawn {} lamports to yield account", withdrawn_lamports); + + // CPI: update the epoch report with the extracted yield. + let state_bump = ctx.bumps.state; + sunrise_interface::extract_yield( + ctx.accounts.deref(), + ctx.accounts.sunrise_program.to_account_info(), + ctx.accounts.sunrise_state.key(), + state_bump, + withdrawn_lamports, + )?; + + Ok(()) + } + + pub fn update_epoch_report(ctx: Context) -> Result<()> { + let yield_balance = utils::calculate_extractable_yield( + &ctx.accounts.sunrise_state, + &ctx.accounts.state, + &ctx.accounts.marinade_state, + &ctx.accounts.liq_pool_mint, + &ctx.accounts.liq_pool_token_vault, + &ctx.accounts.liq_pool_sol_leg_pda, + &ctx.accounts.liq_pool_msol_leg, + )?; + + let excess_lamports = max(0, yield_balance.lamports) as u64; + let extractable_lamports = get_extractable_yield_from_excess_balance(excess_lamports); + + // CPI: update the epoch report with the extracted yield. + let state_bump = ctx.bumps.state; + sunrise_interface::update_epoch_report( + ctx.accounts.deref(), + ctx.accounts.sunrise_program.to_account_info(), + ctx.accounts.sunrise_state.key(), + state_bump, + extractable_lamports as u64, + )?; + + Ok(()) + } } #[derive(Accounts)] @@ -171,7 +284,6 @@ pub struct Update<'info> { #[derive(Accounts)] pub struct Deposit<'info> { #[account( - mut, has_one = sunrise_state, has_one = marinade_state, seeds = [constants::STATE, sunrise_state.key().as_ref()], @@ -182,8 +294,7 @@ pub struct Deposit<'info> { /// CHECK: The registered Marinade state. pub marinade_state: UncheckedAccount<'info>, #[account(mut)] - /// CHECK: The main Sunrise beam state. - pub sunrise_state: UncheckedAccount<'info>, + pub sunrise_state: Box>, #[account(mut)] pub depositor: Signer<'info>, @@ -214,7 +325,7 @@ pub struct Deposit<'info> { /// CHECK: Checked by Sunrise CPI. pub gsol_mint_authority: UncheckedAccount<'info>, /// CHECK: Checked by Sunrise CPI. - pub instructions_sysvar: UncheckedAccount<'info>, + pub sysvar_instructions: UncheckedAccount<'info>, #[account(mut)] /// CHECK: Checked by Marinade CPI. @@ -231,15 +342,12 @@ pub struct Deposit<'info> { pub token_program: UncheckedAccount<'info>, pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, - #[account(address = marinade_cpi::ID)] - /// CHECK: The Marinade ProgramID. - pub marinade_program: UncheckedAccount<'info>, + pub marinade_program: Program<'info, Marinade>, } #[derive(Accounts)] pub struct Withdraw<'info> { #[account( - mut, has_one = sunrise_state, has_one = marinade_state, seeds = [constants::STATE, sunrise_state.key().as_ref()], @@ -285,7 +393,7 @@ pub struct Withdraw<'info> { pub liq_pool_sol_leg_pda: UncheckedAccount<'info>, #[account(mut)] /// CHECK: Checked by Marinade CPI. - pub liq_pool_msol_leg: UncheckedAccount<'info>, + pub liq_pool_msol_leg: Box>, #[account(mut)] /// CHECK: Checked by Marinade CPI. pub liq_pool_msol_leg_authority: UncheckedAccount<'info>, @@ -298,12 +406,10 @@ pub struct Withdraw<'info> { /// Verified in CPI to Sunrise program. pub gsol_mint: Box>, /// CHECK: Checked by Sunrise CPI. - pub instructions_sysvar: UncheckedAccount<'info>, + pub sysvar_instructions: UncheckedAccount<'info>, pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, - #[account(address = marinade_cpi::ID)] - /// CHECK: The Marinade program ID. - pub marinade_program: UncheckedAccount<'info>, + pub marinade_program: Program<'info, Marinade>, } #[derive(Accounts)] @@ -316,8 +422,7 @@ pub struct Burn<'info> { )] pub state: Box>, #[account(mut)] - /// CHECK: The main Sunrise beam state. - pub sunrise_state: UncheckedAccount<'info>, + pub sunrise_state: Box>, #[account(mut)] pub burner: Signer<'info>, @@ -333,7 +438,128 @@ pub struct Burn<'info> { /// Verified in CPI to Sunrise program. pub gsol_mint: Box>, /// CHECK: Checked by Sunrise CPI. - pub instructions_sysvar: UncheckedAccount<'info>, + pub sysvar_instructions: UncheckedAccount<'info>, + + pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, +} + +#[derive(Accounts)] +pub struct ExtractYield<'info> { + #[account( + has_one = sunrise_state, + has_one = marinade_state, + seeds = [constants::STATE, sunrise_state.key().as_ref()], + bump + )] + pub state: Box>, + #[account(mut)] + pub marinade_state: Box>, + #[account( + mut, // Update the extracted yield on the state's epoch report. + has_one = yield_account + )] + pub sunrise_state: Box>, + + #[account(mut)] + pub payer: Signer<'info>, + + #[account(mut)] + /// CHECK: Matches the yield account key stored in the state. + pub yield_account: UncheckedAccount<'info>, + + #[account(mut)] + pub liq_pool_mint: Box>, + #[account( + mut, + token::mint = liq_pool_mint, + token::authority = vault_authority, + )] + pub liq_pool_token_vault: Box>, + #[account( + seeds = [ + state.key().as_ref(), + constants::VAULT_AUTHORITY + ], + bump = state.vault_authority_bump + )] + /// CHECK: The vault authority PDA with verified seeds. + pub vault_authority: UncheckedAccount<'info>, + + /// When withdrawing from the Marinade LP, the withdrawal is part SOL, part mSOL. + /// The SOL portion is transferred to the user (withdrawer) and the mSOL portion + /// is transferred to the msol_token_account owned by the marinade stake pool. + #[account(mut, address = state.msol_token_account)] + pub transfer_msol_to: Box>, + #[account(mut)] + /// CHECK: Checked by Marinade CPI. + pub liq_pool_sol_leg_pda: UncheckedAccount<'info>, + #[account(mut)] + /// CHECK: Checked by Marinade CPI. + pub liq_pool_msol_leg: Box>, + #[account(mut)] + /// CHECK: Checked by Marinade CPI. + pub liq_pool_msol_leg_authority: UncheckedAccount<'info>, + /// CHECK: Checked by Marinade CPI. + pub system_program: UncheckedAccount<'info>, + /// CHECK: Checked by Marinade CPI. + pub token_program: UncheckedAccount<'info>, + + /// CHECK: Checked by Sunrise CPI. + pub sysvar_instructions: UncheckedAccount<'info>, + + pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, + pub marinade_program: Program<'info, Marinade>, +} + +#[derive(Accounts)] +pub struct UpdateEpochReport<'info> { + #[account( + has_one = sunrise_state, + has_one = marinade_state, + seeds = [constants::STATE, sunrise_state.key().as_ref()], + bump + )] + pub state: Box>, + pub marinade_state: Box>, + #[account( + mut, // Update the extracted yield on the state's epoch report. + )] + pub sunrise_state: Box>, + + /// Required to update the core state epoch report + /// Verified in CPI to Sunrise program. + pub gsol_mint: Account<'info, Mint>, + + #[account(mut)] + pub liq_pool_mint: Box>, + #[account( + mut, + token::mint = liq_pool_mint, + token::authority = vault_authority, + )] + pub liq_pool_token_vault: Box>, + #[account( + seeds = [ + state.key().as_ref(), + constants::VAULT_AUTHORITY + ], + bump = state.vault_authority_bump + )] + /// CHECK: The vault authority PDA with verified seeds. + pub vault_authority: UncheckedAccount<'info>, + + #[account(mut)] + /// CHECK: Checked by Marinade CPI. + pub liq_pool_sol_leg_pda: UncheckedAccount<'info>, + #[account(mut)] + /// CHECK: Checked by Marinade CPI. + pub liq_pool_msol_leg: Box>, + #[account(mut)] + /// CHECK: Checked by Marinade CPI. + pub liq_pool_msol_leg_authority: UncheckedAccount<'info>, + + /// CHECK: Checked by Sunrise CPI. + pub sysvar_instructions: UncheckedAccount<'info>, pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, } @@ -347,4 +573,6 @@ pub enum MarinadeLpBeamError { CalculationFailure, #[msg("This feature is unimplemented for this beam")] Unimplemented, + #[msg("The yield balance is insufficient to extract yield")] + InsufficientYieldBalance, } diff --git a/programs/marinade-lp-beam/src/state.rs b/programs/marinade-lp-beam/src/state.rs index cbb4507..a6ed465 100644 --- a/programs/marinade-lp-beam/src/state.rs +++ b/programs/marinade-lp-beam/src/state.rs @@ -1,4 +1,5 @@ use anchor_lang::prelude::*; +use marinade_common::vault_authority_seed::HasVaultAuthority; #[account] pub struct State { @@ -15,20 +16,27 @@ pub struct State { /// that holds pool tokens(both liq_pool and marinade stake pool). pub vault_authority_bump: u8, - /// This state's SOL vault. - pub treasury: Pubkey, + /// The beam address of the recipient of msol when withdrawing liquidity. + /// Typically the marinade-sp beam + pub msol_recipient_beam: Pubkey, /// The token-account that receives msol when withdrawing liquidity. pub msol_token_account: Pubkey, } +impl HasVaultAuthority for State { + fn vault_authority_bump(&self) -> u8 { + self.vault_authority_bump + } +} + impl State { pub const SPACE: usize = 8 + /*discriminator*/ 32 + /*update_authority*/ 32 + /*marinade_state*/ 32 + /*sunrise_state*/ 1 + /*vault_authority_bump*/ - 32 + /*treasury*/ + 32 + /*msol_recipient_beam*/ 32; /*msol_token_account*/ } @@ -41,7 +49,7 @@ pub struct StateEntry { pub marinade_state: Pubkey, pub sunrise_state: Pubkey, pub vault_authority_bump: u8, - pub treasury: Pubkey, + pub msol_recipient_beam: Pubkey, pub msol_token_account: Pubkey, } @@ -52,7 +60,7 @@ impl From for State { marinade_state: se.marinade_state, sunrise_state: se.sunrise_state, vault_authority_bump: se.vault_authority_bump, - treasury: se.treasury, + msol_recipient_beam: se.msol_recipient_beam, msol_token_account: se.msol_token_account, } } diff --git a/programs/marinade-lp-beam/src/system/balance.rs b/programs/marinade-lp-beam/src/system/balance.rs index 9defc6b..6209bcf 100644 --- a/programs/marinade-lp-beam/src/system/balance.rs +++ b/programs/marinade-lp-beam/src/system/balance.rs @@ -1,15 +1,15 @@ -use super::utils::{calc_lamports_from_msol_amount, proportional}; use anchor_lang::prelude::*; +use marinade_common::{calc_lamports_from_msol_amount, proportional}; use marinade_cpi::State as MarinadeState; #[derive(Debug, Clone, Copy, Eq, PartialEq)] -pub(super) struct LiquidityPoolBalance { - pub lamports: u64, - pub msol: u64, - pub liq_pool_token: u64, +pub struct LiquidityPoolBalance { + pub lamports: i128, + pub msol: i128, + pub liq_pool_token: i128, } impl LiquidityPoolBalance { - pub fn new(sol_leg: u64, msol_leg: u64, total_liq_pool_tokens: u64) -> Self { + pub fn new(sol_leg: i128, msol_leg: i128, total_liq_pool_tokens: i128) -> Self { LiquidityPoolBalance { lamports: sol_leg, msol: msol_leg, @@ -18,53 +18,63 @@ impl LiquidityPoolBalance { } pub fn value_of(&self, liq_pool_token: u64) -> Result { - let lamports = proportional(self.lamports, liq_pool_token, self.liq_pool_token)?; - let msol = proportional(self.msol, liq_pool_token, self.liq_pool_token)?; + let lamports = proportional(self.lamports, liq_pool_token as i128, self.liq_pool_token); + let msol = proportional(self.msol, liq_pool_token as i128, self.liq_pool_token); Ok(LiquidityPoolBalance { lamports, msol, - liq_pool_token, + liq_pool_token: liq_pool_token as i128, }) } // The value of both legs of the liquidity pool balance in SOL pub fn sol_value(&self, marinade_state: &MarinadeState) -> u64 { let lamports = self.lamports; - let msol = calc_lamports_from_msol_amount(marinade_state, self.msol).unwrap(); - lamports.checked_add(msol).expect("sol_value") + let msol = calc_lamports_from_msol_amount(marinade_state, self.msol as u64); + lamports.checked_add(msol as i128).expect("sol_value") as u64 } // if this balance in lamports is smaller than other_lamports, return this, // otherwise return a liquidity pool balance with lamports = other_lamports // and liq_pool_token = the amount of liq_pool_token that would be needed to withdraw // other_lamports from the liquidity pool - pub fn min_lamports(&self, other_lamports: u64) -> Result { + pub fn min_lamports(&self, other_lamports: i128) -> Self { if self.lamports < other_lamports { - return Ok(*self); + return *self; } - let other_liq_pool_token = - proportional(self.liq_pool_token, other_lamports, self.lamports)?; - let other_msol = proportional(self.msol, other_lamports, self.lamports)?; - Ok(Self { + let other_liq_pool_token = proportional(self.liq_pool_token, other_lamports, self.lamports); + let other_msol = proportional(self.msol, other_lamports, self.lamports); + Self { lamports: other_lamports, msol: other_msol, liq_pool_token: other_liq_pool_token, - }) + } } // returns a new balance that is the result of subtracting other_lamports from this balance - pub fn checked_sub_lamports(&self, other_lamports: u64) -> Result { + pub fn checked_sub_lamports(&self, other_lamports: i128) -> Result { let new_lamports = self .lamports .checked_sub(other_lamports) .expect("checked_sub_lamports"); - let new_liq_pool_token = proportional(self.liq_pool_token, new_lamports, self.lamports)?; + let new_liq_pool_token = proportional(self.liq_pool_token, new_lamports, self.lamports); - let new_msol = proportional(self.msol, new_lamports, self.lamports)?; + let new_msol = proportional(self.msol, new_lamports, self.lamports); Ok(Self { lamports: new_lamports, msol: new_msol, liq_pool_token: new_liq_pool_token, }) } + + pub fn sub(&self, other: Self) -> Self { + let new_lamports = self.lamports.saturating_sub(other.lamports); + let new_liq_pool_token = self.liq_pool_token.saturating_sub(other.liq_pool_token); + let new_msol = self.msol.saturating_sub(other.msol); + Self { + lamports: new_lamports, + msol: new_msol, + liq_pool_token: new_liq_pool_token, + } + } } diff --git a/programs/marinade-lp-beam/src/system/utils.rs b/programs/marinade-lp-beam/src/system/utils.rs index cd20cd9..c6fe140 100644 --- a/programs/marinade-lp-beam/src/system/utils.rs +++ b/programs/marinade-lp-beam/src/system/utils.rs @@ -2,20 +2,15 @@ use super::balance::LiquidityPoolBalance; use crate::state::State; use anchor_lang::prelude::*; use anchor_spl::token::{Mint, TokenAccount}; +use marinade_common::{ + calc_lamports_from_msol_amount, proportional, proportional_with_rounding, RoundingMode, +}; use marinade_cpi::State as MarinadeState; use sunrise_core::BeamError; -/// calculate amount*numerator/denominator -/// as value = shares * share_price where share_price=total_value/total_shares -/// or shares = amount_value / share_price where share_price=total_value/total_shares -/// => shares = amount_value * 1/share_price where 1/share_price=total_shares/total_value -pub(super) fn proportional(amount: u64, numerator: u64, denominator: u64) -> Result { - if denominator == 0 { - return Ok(amount); - } - u64::try_from((amount as u128) * (numerator as u128) / (denominator as u128)) - .map_err(|_| error!(crate::MarinadeLpBeamError::CalculationFailure)) -} +// estimated 0.3% unstake fee +// const WITHDRAWAL_FRACTION : f64 = 0.997; +const WITHDRAWAL_FRACTION: f64 = 1.0; /// Calculates the amount that can be extracted as yield, in lamports. pub fn calculate_extractable_yield( @@ -26,30 +21,95 @@ pub fn calculate_extractable_yield( liq_pool_token_account: &TokenAccount, liq_pool_sol_leg_pda: &AccountInfo, liq_pool_msol_leg: &TokenAccount, -) -> Result { - let staked_value = current_liq_pool_balance( +) -> Result { + // the liquidity pool balance owned by the beam (LP tokens, SOL and mSOL) + let staked_balance = current_liq_pool_balance( marinade_state, liq_pool_mint, liq_pool_token_account, liq_pool_sol_leg_pda, liq_pool_msol_leg, - )? - .sol_value(marinade_state); + )?; + + // the amount of SOL that this beam is responsible for. + // The value of the liquidity pool is at least this high. However, the lp value is split across + // SOL and mSOL let details = sunrise_state .get_beam_details(&beam_state.key()) .ok_or(BeamError::UnidentifiedBeam)?; let staked_sol = details.partial_gsol_supply; - Ok(staked_value.saturating_sub(staked_sol)) + + // the lp balance that is required to cover the staked SOL. This is a combination of SOL and mSOL. + let required_lp_tokens_to_cover_staked_sol = calculate_liq_pool_token_value_of_lamports( + marinade_state, + liq_pool_mint, + liq_pool_sol_leg_pda, + liq_pool_msol_leg, + staked_sol, + ); + + msg!("staked_balance: {:?}", staked_balance); + + let required_liq_pool_balance = liq_pool_balance_for_tokens( + required_lp_tokens_to_cover_staked_sol, + marinade_state, + liq_pool_mint, + liq_pool_sol_leg_pda, + liq_pool_msol_leg, + )?; + msg!("required_liq_pool_balance: {:?}", required_liq_pool_balance); + + // return the difference between the staked balance and the required balance + let diff = staked_balance.sub(required_liq_pool_balance); + + msg!("diff: {:?}", diff); + Ok(diff) +} + +// in the marinade-lp beam, extractable yield is equivalent to surplus LP tokens +// when LP tokens are redeemed, the result is SOL and mSOL (both sides of the pool) +// the SOL is sent to the yield account, +// and the mSOL is sent to the beam's mSOL token account, which is typically +// the Marinade-SP's beam vault. +// This results in less extractable yield for this beam, and more for the Marinade-SP beam. +// (However, in reality, this beam should rarely be extracted from, as it is +// included as a buffer to allow for fee-less gSOL withdrawals) +// Subtract fee TODO can we do better than an estimate? +pub fn get_extractable_yield_from_excess_balance(excess_balance: u64) -> u64 { + let extractable_lamports = (excess_balance as f64) * WITHDRAWAL_FRACTION; + msg!("Excess balance: {:?}", excess_balance); + msg!("Extractable yield: {}", extractable_lamports); + extractable_lamports as u64 } // Prevent the compiler from enlarging the stack and potentially triggering an Access violation #[inline(never)] -pub(super) fn current_liq_pool_balance( +/// Returns the current liquidity pool balance owned by the beam +pub fn current_liq_pool_balance( marinade_state: &MarinadeState, liq_pool_mint: &Mint, liq_pool_token_account: &TokenAccount, liq_pool_sol_leg_pda: &AccountInfo, liq_pool_msol_leg: &TokenAccount, +) -> Result { + liq_pool_balance_for_tokens( + liq_pool_token_account.amount, + marinade_state, + liq_pool_mint, + liq_pool_sol_leg_pda, + liq_pool_msol_leg, + ) +} + +// Prevent the compiler from enlarging the stack and potentially triggering an Access violation +#[inline(never)] +/// Returns the liquidity pool balance for a given amount of lp tokens +pub(super) fn liq_pool_balance_for_tokens( + tokens: u64, + marinade_state: &MarinadeState, + liq_pool_mint: &Mint, + liq_pool_sol_leg_pda: &AccountInfo, + liq_pool_msol_leg: &TokenAccount, ) -> Result { //compute current liq-pool total value let total_balance = total_liq_pool( @@ -61,22 +121,40 @@ pub(super) fn current_liq_pool_balance( // The SOL amount held by sunrise in the liquidity pool is the total value of the pool in SOL // multiplied by the proportion of the pool owned by this SunshineStake instance - let sunrise_liq_pool_balance = total_balance.value_of(liq_pool_token_account.amount)?; + let sunrise_liq_pool_balance = total_balance.value_of(tokens)?; msg!("Total LP: {:?}", total_balance); - msg!("Sunrise LP: {:?}", sunrise_liq_pool_balance); + msg!("LP for token: {:?}", sunrise_liq_pool_balance); msg!( "Total LP value: {:?}", total_balance.sol_value(marinade_state) ); msg!( - "Sunrise LP value: {:?}", + "LP value: {:?}", sunrise_liq_pool_balance.sol_value(marinade_state) ); Ok(sunrise_liq_pool_balance) } +pub fn calculate_liq_pool_token_value_of_lamports( + marinade_state: &MarinadeState, + liq_pool_mint: &Mint, + liq_pool_sol_leg_pda: &AccountInfo, + liq_pool_msol_leg: &TokenAccount, + lamports: u64, +) -> u64 { + let total_lamports = liq_pool_sol_leg_pda + .lamports() + .checked_sub(marinade_state.rent_exempt_for_token_acc) + .unwrap(); + let total_msol = liq_pool_msol_leg.amount; + let lamports_value_of_msol = calc_lamports_from_msol_amount(marinade_state, total_msol); + let total_value_of_pool = total_lamports.checked_add(lamports_value_of_msol).unwrap(); + + proportional(liq_pool_mint.supply, lamports, total_value_of_pool) +} + fn total_liq_pool( marinade_state: &MarinadeState, liq_pool_mint: &Mint, @@ -89,56 +167,38 @@ fn total_liq_pool( .expect("sol_leg_lamports"); LiquidityPoolBalance::new( - sol_leg_lamports, - liq_pool_msol_leg.amount, - liq_pool_mint.supply, + sol_leg_lamports as i128, + liq_pool_msol_leg.amount as i128, + liq_pool_mint.supply as i128, ) } -pub fn liq_pool_tokens_from_lamports( +pub fn calculate_liq_pool_balance_required_to_withdraw_lamports( marinade_state: &MarinadeState, liq_pool_mint: &Mint, liq_pool_sol_leg_pda: &AccountInfo, + liq_pool_msol_leg: &TokenAccount, lamports: u64, -) -> Result { +) -> Result { let liq_pool_lamports = liq_pool_sol_leg_pda .lamports() .checked_sub(marinade_state.rent_exempt_for_token_acc) .unwrap(); let liq_pool_mint_supply = liq_pool_mint.supply; - proportional(liq_pool_mint_supply, lamports, liq_pool_lamports) -} + // Round up to ensure that the amount of LP tokens is enough to withdraw the required amount of SOL + let liq_pool_tokens = proportional_with_rounding( + liq_pool_mint_supply, + lamports, + liq_pool_lamports, + RoundingMode::Up, + ); -// The following are lifted from https://github.com/marinade-finance/liquid-staking-program/blob/447f9607a8c755cac7ad63223febf047142c6c8f/programs/marinade-finance/src/state.rs#L227 -pub fn calc_lamports_from_msol_amount( - marinade_state: &MarinadeState, - msol_amount: u64, -) -> Result { - proportional( - msol_amount, - total_virtual_staked_lamports(marinade_state), - marinade_state.msol_supply, + liq_pool_balance_for_tokens( + liq_pool_tokens, + marinade_state, + liq_pool_mint, + liq_pool_sol_leg_pda, + liq_pool_msol_leg, ) } -fn total_lamports_under_control(marinade_state: &MarinadeState) -> u64 { - marinade_state - .validator_system - .total_active_balance - .checked_add(total_cooling_down(marinade_state)) - .expect("Stake balance overflow") - .checked_add(marinade_state.available_reserve_balance) // reserve_pda.lamports() - self.rent_exempt_for_token_acc - .expect("Total SOLs under control overflow") -} -fn total_virtual_staked_lamports(marinade_state: &MarinadeState) -> u64 { - // if we get slashed it may be negative but we must use 0 instead - total_lamports_under_control(marinade_state) - .saturating_sub(marinade_state.circulating_ticket_balance) //tickets created -> cooling down lamports or lamports already in reserve and not claimed yet -} -fn total_cooling_down(marinade_state: &MarinadeState) -> u64 { - marinade_state - .stake_system - .delayed_unstake_cooling_down - .checked_add(marinade_state.emergency_cooling_down) - .expect("Total cooling down overflow") -} diff --git a/programs/spl-beam/src/cpi_interface/sunrise.rs b/programs/spl-beam/src/cpi_interface/sunrise.rs index 29d32c2..e16c7c8 100644 --- a/programs/spl-beam/src/cpi_interface/sunrise.rs +++ b/programs/spl-beam/src/cpi_interface/sunrise.rs @@ -2,8 +2,9 @@ use crate::seeds::*; use anchor_lang::prelude::*; use sunrise_core as sunrise_core_cpi; use sunrise_core_cpi::cpi::{ - accounts::{BurnGsol, ExtractYield, MintGsol}, + accounts::{BurnGsol, ExtractYield, MintGsol, UpdateEpochReport}, burn_gsol as cpi_burn_gsol, extract_yield as cpi_extract_yield, mint_gsol as cpi_mint_gsol, + update_epoch_report as cpi_update_epoch_report, }; pub fn mint_gsol<'a>( @@ -37,7 +38,7 @@ impl<'a> From<&crate::Deposit<'a>> for MintGsol<'a> { gsol_mint: accounts.gsol_mint.to_account_info(), gsol_mint_authority: accounts.gsol_mint_authority.to_account_info(), mint_gsol_to: accounts.mint_gsol_to.to_account_info(), - instructions_sysvar: accounts.instructions_sysvar.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), token_program: accounts.token_program.to_account_info(), } } @@ -50,7 +51,7 @@ impl<'a> From<&crate::DepositStake<'a>> for MintGsol<'a> { gsol_mint: accounts.gsol_mint.to_account_info(), gsol_mint_authority: accounts.gsol_mint_authority.to_account_info(), mint_gsol_to: accounts.mint_gsol_to.to_account_info(), - instructions_sysvar: accounts.instructions_sysvar.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), token_program: accounts.token_program.to_account_info(), } } @@ -87,7 +88,7 @@ impl<'a> From<&crate::Withdraw<'a>> for BurnGsol<'a> { gsol_mint: accounts.gsol_mint.to_account_info(), burn_gsol_from_owner: accounts.withdrawer.to_account_info(), burn_gsol_from: accounts.gsol_token_account.to_account_info(), - instructions_sysvar: accounts.instructions_sysvar.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), token_program: accounts.token_program.to_account_info(), } } @@ -101,7 +102,7 @@ impl<'a> From<&crate::WithdrawStake<'a>> for BurnGsol<'a> { gsol_mint: accounts.gsol_mint.to_account_info(), burn_gsol_from_owner: accounts.withdrawer.to_account_info(), burn_gsol_from: accounts.gsol_token_account.to_account_info(), - instructions_sysvar: accounts.instructions_sysvar.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), token_program: accounts.token_program.to_account_info(), } } @@ -115,7 +116,7 @@ impl<'a> From<&crate::Burn<'a>> for BurnGsol<'a> { gsol_mint: accounts.gsol_mint.to_account_info(), burn_gsol_from_owner: accounts.burner.to_account_info(), burn_gsol_from: accounts.gsol_token_account.to_account_info(), - instructions_sysvar: accounts.instructions_sysvar.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), token_program: accounts.token_program.to_account_info(), } } @@ -149,8 +150,40 @@ impl<'a> From<&crate::ExtractYield<'a>> for ExtractYield<'a> { Self { state: accounts.sunrise_state.to_account_info(), beam: accounts.state.to_account_info(), - epoch_report: accounts.epoch_report.to_account_info(), - sysvar_clock: accounts.sysvar_clock.to_account_info(), + sysvar_instructions: accounts.sysvar_instructions.to_account_info(), + } + } +} + +pub fn update_epoch_report<'a>( + accounts: impl Into>, + cpi_program: AccountInfo<'a>, + sunrise_key: Pubkey, + stake_pool: Pubkey, + state_bump: u8, + extractable_yield: u64, +) -> Result<()> { + let accounts: UpdateEpochReport<'a> = accounts.into(); + let seeds = [ + STATE, + sunrise_key.as_ref(), + stake_pool.as_ref(), + &[state_bump], + ]; + let signer = &[&seeds[..]]; + + cpi_update_epoch_report( + CpiContext::new(cpi_program, accounts).with_signer(signer), + extractable_yield, + ) +} + +impl<'a> From<&crate::UpdateEpochReport<'a>> for UpdateEpochReport<'a> { + fn from(accounts: &crate::UpdateEpochReport<'a>) -> Self { + Self { + state: accounts.sunrise_state.to_account_info(), + beam: accounts.state.to_account_info(), + gsol_mint: accounts.gsol_mint.to_account_info(), sysvar_instructions: accounts.sysvar_instructions.to_account_info(), } } diff --git a/programs/spl-beam/src/lib.rs b/programs/spl-beam/src/lib.rs index ec07df5..e52693f 100644 --- a/programs/spl-beam/src/lib.rs +++ b/programs/spl-beam/src/lib.rs @@ -30,6 +30,7 @@ declare_id!("EUZfY4LePXSZVMvRuiVzbxazw9yBDYU99DpGJKCthxbS"); pub mod spl_beam { use super::*; use crate::cpi_interface::stake_account::claim_stake_account; + use crate::utils::proportional; pub fn initialize(ctx: Context, input: StateEntry) -> Result<()> { ctx.accounts.state.set_inner(input.into()); @@ -166,6 +167,37 @@ pub mod spl_beam { Err(SplBeamError::Unimplemented.into()) } + pub fn update_epoch_report(ctx: Context) -> Result<()> { + // Calculate how much yield can be extracted from the pool. + let gross_extractable_yield = utils::calculate_extractable_yield( + &ctx.accounts.sunrise_state, + &ctx.accounts.state, + &ctx.accounts.stake_pool, + &ctx.accounts.pool_token_vault, + )?; + + // Reduce by fee + let fee = proportional( + gross_extractable_yield, + ctx.accounts.stake_pool.stake_withdrawal_fee.numerator, + ctx.accounts.stake_pool.stake_withdrawal_fee.denominator, + )?; + let net_extractable_yield = gross_extractable_yield.saturating_sub(fee); + + // CPI: update the epoch report with the extracted yield. + let state_bump = ctx.bumps.state; + sunrise_interface::update_epoch_report( + ctx.accounts.deref(), + ctx.accounts.sunrise_program.to_account_info(), + ctx.accounts.sunrise_state.key(), + ctx.accounts.stake_pool.key(), + state_bump, + net_extractable_yield, + )?; + + Ok(()) + } + pub fn extract_yield(ctx: Context) -> Result<()> { // Calculate how much yield can be extracted from the pool. let extractable_yield = utils::calculate_extractable_yield( @@ -301,7 +333,7 @@ pub struct Deposit<'info> { /// CHECK: Checked by CPI to Sunrise. pub gsol_mint_authority: UncheckedAccount<'info>, /// CHECK: Checked by CPI to Sunrise. - pub instructions_sysvar: UncheckedAccount<'info>, + pub sysvar_instructions: UncheckedAccount<'info>, pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, pub spl_stake_pool_program: Program<'info, SplStakePool>, @@ -379,7 +411,7 @@ pub struct DepositStake<'info> { /// CHECK: Checked by CPI to Sunrise. pub gsol_mint_authority: UncheckedAccount<'info>, /// CHECK: Checked by CPI to Sunrise. - pub instructions_sysvar: UncheckedAccount<'info>, + pub sysvar_instructions: UncheckedAccount<'info>, pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, pub spl_stake_pool_program: Program<'info, SplStakePool>, @@ -445,7 +477,7 @@ pub struct Withdraw<'info> { /// Verified in CPI to Sunrise program. pub gsol_mint: Account<'info, Mint>, /// CHECK: Checked by CPI to Sunrise. - pub instructions_sysvar: UncheckedAccount<'info>, + pub sysvar_instructions: UncheckedAccount<'info>, pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, pub spl_stake_pool_program: Program<'info, SplStakePool>, @@ -520,7 +552,7 @@ pub struct WithdrawStake<'info> { /// Verified in CPI to Sunrise program. pub gsol_mint: Account<'info, Mint>, /// CHECK: Checked by CPI to Sunrise. - pub instructions_sysvar: UncheckedAccount<'info>, + pub sysvar_instructions: UncheckedAccount<'info>, pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, pub spl_stake_pool_program: Program<'info, SplStakePool>, @@ -539,10 +571,14 @@ pub struct ExtractYield<'info> { )] pub state: Box>, #[account( + mut, // Update the extracted yield on the state's epoch report. has_one = yield_account )] pub sunrise_state: Box>, - #[account(mut)] + #[account( + mut, + has_one = pool_mint + )] pub stake_pool: Box>, #[account(mut)] @@ -603,12 +639,6 @@ pub struct ExtractYield<'info> { /// CHECK: Checked by CPI to SPL StakePool Program. pub manager_fee_account: UncheckedAccount<'info>, - /// The epoch report account. This is updated with the latest extracted yield value. - /// It must be up to date with the current epoch. If not, run updateEpochReport before it. - /// CHECK: Address checked by CIP to the core Sunrise program. - #[account(mut)] - pub epoch_report: UncheckedAccount<'info>, - pub sysvar_clock: Sysvar<'info, Clock>, pub native_stake_program: Program<'info, NativeStakeProgram>, /// CHECK: Checked by CPI to SPL Stake program. @@ -623,6 +653,47 @@ pub struct ExtractYield<'info> { pub token_program: Program<'info, Token>, } +#[derive(Accounts, Clone)] +pub struct UpdateEpochReport<'info> { + #[account( + has_one = stake_pool, + has_one = sunrise_state, + seeds = [STATE, sunrise_state.key().as_ref(), stake_pool.key().as_ref()], + bump + )] + pub state: Box>, + #[account( + mut, // Update the extractable yield on the state's epoch report. + )] + pub sunrise_state: Box>, + pub stake_pool: Box>, + pub pool_mint: Box>, + + #[account( + seeds = [ + state.key().as_ref(), + VAULT_AUTHORITY + ], + bump = state.vault_authority_bump + )] + /// CHECK: The vault authority PDA with verified seeds. + pub vault_authority: UncheckedAccount<'info>, + + #[account( + token::mint = pool_mint, + token::authority = vault_authority + )] + pub pool_token_vault: Box>, + + /// Required to update the core state epoch report + /// Verified in CPI to Sunrise program. + pub gsol_mint: Account<'info, Mint>, + + pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, + /// CHECK: Checked by CPI to Sunrise. + pub sysvar_instructions: UncheckedAccount<'info>, +} + #[derive(Accounts)] pub struct Burn<'info> { #[account( @@ -645,7 +716,7 @@ pub struct Burn<'info> { /// Verified in CPI to Sunrise program. pub gsol_mint: Account<'info, Mint>, /// CHECK: Checked by CPI to Sunrise. - pub instructions_sysvar: UncheckedAccount<'info>, + pub sysvar_instructions: UncheckedAccount<'info>, pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, diff --git a/programs/sunrise-core/src/instructions/burn_gsol.rs b/programs/sunrise-core/src/instructions/burn_gsol.rs index beaae65..0ff4e03 100644 --- a/programs/sunrise-core/src/instructions/burn_gsol.rs +++ b/programs/sunrise-core/src/instructions/burn_gsol.rs @@ -2,31 +2,33 @@ use crate::{system, token, utils, BeamError, BurnGsol}; use anchor_lang::prelude::*; pub fn handler(ctx: Context, amount_in_lamports: u64) -> Result<()> { - let amount = amount_in_lamports; let state = &mut ctx.accounts.state; // Check that the requesting program is valid. let cpi_program = - utils::get_cpi_program_id(&ctx.accounts.instructions_sysvar.to_account_info())?; - system::check_beam_validity(state, &ctx.accounts.beam, &cpi_program)?; + utils::get_cpi_program_id(&ctx.accounts.sysvar_instructions.to_account_info())?; + system::checked_find_beam_idx(state, &ctx.accounts.beam, &cpi_program)?; let details = state .get_mut_beam_details(&ctx.accounts.beam.key()) .ok_or(BeamError::UnidentifiedBeam)?; // Can't burn more gsol than this beam is responsible for. - if details.partial_gsol_supply < amount { + if details.partial_gsol_supply < amount_in_lamports { msg!( "Beam supply {}, requested burn {}", details.partial_gsol_supply, - amount + amount_in_lamports ); return Err(BeamError::BurnWindowExceeded.into()); } - details.partial_gsol_supply = details.partial_gsol_supply.checked_sub(amount).unwrap(); + details.partial_gsol_supply = details + .partial_gsol_supply + .checked_sub(amount_in_lamports) + .unwrap(); token::burn( - amount, + amount_in_lamports, &ctx.accounts.gsol_mint.to_account_info(), &ctx.accounts.burn_gsol_from_owner.to_account_info(), &ctx.accounts.burn_gsol_from.to_account_info(), diff --git a/programs/sunrise-core/src/instructions/extract_yield.rs b/programs/sunrise-core/src/instructions/extract_yield.rs index e07670e..d1eacec 100644 --- a/programs/sunrise-core/src/instructions/extract_yield.rs +++ b/programs/sunrise-core/src/instructions/extract_yield.rs @@ -7,26 +7,32 @@ use crate::{system, utils, BeamError, ExtractYield}; /// It does not send funds to the yield account (that is done by the beam program itself) /// It only updates the extracted yield on the epoch report. pub fn handler(ctx: Context, amount_in_lamports: u64) -> Result<()> { - let state = &ctx.accounts.state; - let epoch_report = &mut ctx.accounts.epoch_report; - let current_epoch = ctx.accounts.sysvar_clock.epoch; + let state = &mut ctx.accounts.state; + let current_epoch = Clock::get()?.epoch; // Check that the executing program is valid. let cpi_program = utils::get_cpi_program_id(&ctx.accounts.sysvar_instructions.to_account_info())?; - system::check_beam_validity(state, &ctx.accounts.beam, &cpi_program)?; + let beam_idx = system::checked_find_beam_idx(state, &ctx.accounts.beam, &cpi_program)?; + let beam_epoch_details = &state.epoch_report.beam_epoch_details[beam_idx]; - // The epoch report must be already updated for this epoch + // The epoch report must be already updated for this epoch and beam require!( - epoch_report.epoch == current_epoch, + beam_epoch_details.epoch == current_epoch, BeamError::EpochReportNotUpToDate ); - // Update the extracted yield on the report - epoch_report - .extracted_yield - .checked_add(amount_in_lamports) - .ok_or(BeamError::Overflow)?; + msg!( + "Registering extracted yield of {} lamports for beam {}", + amount_in_lamports, + beam_idx + ); + + // Update the extracted yield on the epoch report for the beam, + // if the epoch report has already been updated for this epoch and beam (TODO is this check strictly necessary?) + state + .epoch_report + .extract_yield_for_beam(beam_idx, amount_in_lamports, current_epoch)?; Ok(()) } diff --git a/programs/sunrise-core/src/instructions/mint_gsol.rs b/programs/sunrise-core/src/instructions/mint_gsol.rs index 21d98d5..466a104 100644 --- a/programs/sunrise-core/src/instructions/mint_gsol.rs +++ b/programs/sunrise-core/src/instructions/mint_gsol.rs @@ -8,8 +8,8 @@ pub fn handler(ctx: Context, amount_in_lamports: u64) -> Result<()> { // Check that the executing program is valid. let cpi_program = - utils::get_cpi_program_id(&ctx.accounts.instructions_sysvar.to_account_info())?; - system::check_beam_validity(state, &ctx.accounts.beam, &cpi_program)?; + utils::get_cpi_program_id(&ctx.accounts.sysvar_instructions.to_account_info())?; + system::checked_find_beam_idx(state, &ctx.accounts.beam, &cpi_program)?; let pre_supply = state.pre_supply; let effective_supply = gsol_mint.supply.checked_sub(pre_supply).unwrap(); diff --git a/programs/sunrise-core/src/instructions/mod.rs b/programs/sunrise-core/src/instructions/mod.rs index 5965923..1771990 100644 --- a/programs/sunrise-core/src/instructions/mod.rs +++ b/programs/sunrise-core/src/instructions/mod.rs @@ -6,6 +6,7 @@ pub mod register_beam; pub mod register_state; pub mod remove_beam; pub mod resize_allocations; +pub mod transfer_gsol; pub mod update_allocations; pub mod update_epoch_report; pub mod update_state; @@ -18,6 +19,7 @@ pub use register_beam::*; pub use register_state::*; pub use remove_beam::*; pub use resize_allocations::*; +pub use transfer_gsol::*; pub use update_allocations::*; pub use update_epoch_report::*; pub use update_state::*; diff --git a/programs/sunrise-core/src/instructions/register_state.rs b/programs/sunrise-core/src/instructions/register_state.rs index b0ee1e8..82f0bba 100644 --- a/programs/sunrise-core/src/instructions/register_state.rs +++ b/programs/sunrise-core/src/instructions/register_state.rs @@ -5,10 +5,9 @@ pub fn handler(ctx: Context, input: RegisterStateInput) -> Result let state_account = &mut ctx.accounts.state; let auth_bump = ctx.bumps.gsol_mint_authority; - let epoch_report_bump = ctx.bumps.epoch_report; let mint_key = ctx.accounts.gsol_mint.key(); let mint_supply = ctx.accounts.gsol_mint.supply; - state_account.register(input, auth_bump, &mint_key, mint_supply, epoch_report_bump); + state_account.register(input, auth_bump, &mint_key, mint_supply)?; Ok(()) } diff --git a/programs/sunrise-core/src/instructions/transfer_gsol.rs b/programs/sunrise-core/src/instructions/transfer_gsol.rs new file mode 100644 index 0000000..1daea16 --- /dev/null +++ b/programs/sunrise-core/src/instructions/transfer_gsol.rs @@ -0,0 +1,44 @@ +use crate::{system, utils, BeamError, TransferGsol}; +use anchor_lang::prelude::*; + +pub fn handler( + ctx: Context, + recipient_beam: Pubkey, + amount_in_lamports: u64, +) -> Result<()> { + let amount = amount_in_lamports; + let state = &mut ctx.accounts.state; + + // Check that the requesting program is valid. + let cpi_program = + utils::get_cpi_program_id(&ctx.accounts.sysvar_instructions.to_account_info())?; + system::checked_find_beam_idx(state, &ctx.accounts.beam, &cpi_program)?; + + let source_beam_details = state + .get_mut_beam_details(&ctx.accounts.beam.key()) + .ok_or(BeamError::UnidentifiedBeam)?; + + // Can't transfer more gsol than this beam is responsible for. + if source_beam_details.partial_gsol_supply < amount { + msg!( + "Beam supply {}, requested burn {}", + source_beam_details.partial_gsol_supply, + amount + ); + return Err(BeamError::BurnWindowExceeded.into()); + } + source_beam_details.partial_gsol_supply = source_beam_details + .partial_gsol_supply + .checked_sub(amount) + .unwrap(); + + let target_beam_details = state + .get_mut_beam_details(&recipient_beam) + .ok_or(BeamError::UnidentifiedBeam)?; + target_beam_details.partial_gsol_supply = target_beam_details + .partial_gsol_supply + .checked_add(amount) + .unwrap(); + + Ok(()) +} diff --git a/programs/sunrise-core/src/instructions/update_epoch_report.rs b/programs/sunrise-core/src/instructions/update_epoch_report.rs index 18d6dc2..d5e0444 100644 --- a/programs/sunrise-core/src/instructions/update_epoch_report.rs +++ b/programs/sunrise-core/src/instructions/update_epoch_report.rs @@ -1,55 +1,35 @@ use anchor_lang::prelude::*; -use crate::{BeamError, EpochReport, UpdateEpochReport}; - -pub fn handler<'c: 'info, 'info>(ctx: Context<'_, '_, 'c, 'info, UpdateEpochReport>) -> Result<()> { - let state = &ctx.accounts.state; - let epoch_report = &mut ctx.accounts.epoch_report; - let current_epoch = ctx.accounts.clock.epoch; - - // The epoch report must not be already updated for this epoch - require!( - epoch_report.epoch < current_epoch, - BeamError::EpochReportAlreadyUpdated - ); - - // Remaining accounts must all be beam-owned Epoch Report accounts. - // The count must equal the number of beams in the state. - let beam_count = state.beam_count(); - let beam_owned_epoch_report_count = ctx.remaining_accounts.len(); - require!( - beam_count == beam_owned_epoch_report_count, - BeamError::IncorrectBeamEpochReportCount +use crate::{system, utils, UpdateEpochReport}; + +/// Called by a beam via CPI - to update its epoch report. +/// Once all beams have called in via CPI, then the epoch report is considered updated for the given epoch +/// However, a beam is allowed to call in multiple times to update its epoch report for a given epoch. +pub fn handler<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateEpochReport>, + extractable_yield: u64, +) -> Result<()> { + let state = &mut ctx.accounts.state; + let current_epoch = Clock::get().unwrap().epoch; + + // Check that the executing program is valid. + let cpi_program = + utils::get_cpi_program_id(&ctx.accounts.sysvar_instructions.to_account_info())?; + let beam_idx = system::checked_find_beam_idx(state, &ctx.accounts.beam, &cpi_program)?; + + msg!( + "Updating extractable yield for beam {} to {}", + beam_idx, + extractable_yield ); - // Iterate through the remaining accounts, convert them to Epoch Reports - for (index, beam_owned_epoch_report_account) in ctx.remaining_accounts.iter().enumerate() { - // The epoch of each beam-owned epoch report must match the current epoch - let beam_owned_epoch_report: Account = - Account::try_from(beam_owned_epoch_report_account)?; - require!( - beam_owned_epoch_report.epoch == current_epoch, - BeamError::IncorrectBeamEpochReportEpoch - ); - - // Check that they are the expected beam account - require_keys_eq!( - *beam_owned_epoch_report_account.key, - state.allocations[index].key, - BeamError::IncorrectBeamEpochReport - ); - - // Add the totals to the epoch report - epoch_report - .extractable_yield - .checked_add(beam_owned_epoch_report.extractable_yield) - .ok_or(BeamError::Overflow)?; - } + // Update the epoch report with the current extractable yield + state + .epoch_report + .update_extractable_yield_and_epoch_for_beam(beam_idx, current_epoch, extractable_yield); - // Update the epoch to the current one - epoch_report.epoch = current_epoch; - // Set the current gsol supply - epoch_report.current_gsol_supply = ctx.accounts.gsol_mint.supply; + // Update the current gsol supply + state.epoch_report.current_gsol_supply = ctx.accounts.gsol_mint.supply; Ok(()) } diff --git a/programs/sunrise-core/src/lib.rs b/programs/sunrise-core/src/lib.rs index e685bcd..0bb6296 100644 --- a/programs/sunrise-core/src/lib.rs +++ b/programs/sunrise-core/src/lib.rs @@ -74,6 +74,18 @@ pub mod sunrise_core { burn_gsol::handler(ctx, amount) } + /// CPI request from a beam program to transfer gSol. + /// + /// Same invariants as for [minting][sunrise_core::mint_gsol()]. + /// Errors if the recipient beam is not registered in the state. + pub fn transfer_gsol( + ctx: Context, + recipient_beam: Pubkey, + amount: u64, + ) -> Result<()> { + transfer_gsol::handler(ctx, recipient_beam, amount) + } + /// Removes a beam from the state. /// /// Errors if the beam's allocation is not set to zero. @@ -89,8 +101,9 @@ pub mod sunrise_core { /// Updates the Epoch Report Account, which stores the amount of yield extracted or extractable over time pub fn update_epoch_report<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, UpdateEpochReport>, + extractable_yield: u64, ) -> Result<()> { - update_epoch_report::handler(ctx) + update_epoch_report::handler(ctx, extractable_yield) } /// CPI request from a beam program to extract yield from Sunrise @@ -112,18 +125,6 @@ pub struct RegisterState<'info> { )] pub state: Account<'info, State>, - #[account( - init, - payer = payer, - space = EpochReport::SIZE, - seeds = [ - state.key().as_ref(), - EPOCH_REPORT - ], - bump - )] - pub epoch_report: Account<'info, EpochReport>, - pub gsol_mint: Account<'info, Mint>, /// CHECK: Valid PDA seeds. @@ -200,11 +201,33 @@ pub struct BurnGsol<'info> { /// CHECK: Verified Instructions Sysvar. #[account(address = sysvar::instructions::ID)] - pub instructions_sysvar: UncheckedAccount<'info>, + pub sysvar_instructions: UncheckedAccount<'info>, pub token_program: Program<'info, Token>, } +/// Moves the supply of gsol from one beam to another. +/// gsol is not transferred between accounts in this instruction. +/// Used if a beam moves underlying assets from itself to another, thus +/// implying a move in the amount of gsol it's responsible for. +#[derive(Accounts)] +pub struct TransferGsol<'info> { + #[account( + mut, + has_one = gsol_mint, + )] + pub state: Account<'info, State>, + + pub beam: Signer<'info>, + + #[account(mut)] + pub gsol_mint: Account<'info, Mint>, + + /// CHECK: Verified Instructions Sysvar. + #[account(address = sysvar::instructions::ID)] + pub sysvar_instructions: UncheckedAccount<'info>, +} + #[derive(Accounts)] #[instruction(amount_in_lamports: u64)] pub struct MintGsol<'info> { @@ -233,7 +256,7 @@ pub struct MintGsol<'info> { /// CHECK: Verified Instructions Sysvar. #[account(address = sysvar::instructions::ID)] - pub instructions_sysvar: UncheckedAccount<'info>, + pub sysvar_instructions: UncheckedAccount<'info>, pub token_program: Program<'info, Token>, } @@ -291,50 +314,35 @@ pub struct ResizeAllocations<'info> { } #[derive(Accounts)] +#[instruction(extractable_yield: u64)] pub struct UpdateEpochReport<'info> { #[account( + mut, has_one = gsol_mint, )] pub state: Box>, - #[account( - mut, - seeds = [ - state.key().as_ref(), - EPOCH_REPORT - ], - bump = state.epoch_report_bump - )] - pub epoch_report: Account<'info, EpochReport>, + /// The beam updating its epoch report. + /// This is verified in the handler to be a beam attached to this state. + pub beam: Signer<'info>, pub gsol_mint: Box>, - pub clock: Sysvar<'info, Clock>, + /// CHECK: Verified Instructions Sysvar. + #[account(address = sysvar::instructions::ID)] + pub sysvar_instructions: UncheckedAccount<'info>, } -#[derive(Accounts)] -#[instruction(amount_in_lamports: u64)] +#[derive(Accounts, Clone)] pub struct ExtractYield<'info> { + /// The core sunrise state - will have its epoch report updated. + #[account(mut)] pub state: Box>, /// The beam contributing the extracted yield. /// This is verified in the handler to be a beam attached to this state. pub beam: Signer<'info>, - /// The epoch report account. This is updated with the latest extracted yield value. - /// It must be up to date with the current epoch. If not, run updateEpochReport before it. - #[account( - mut, - seeds = [ - state.key().as_ref(), - EPOCH_REPORT - ], - bump = state.epoch_report_bump - )] - pub epoch_report: Account<'info, EpochReport>, - - pub sysvar_clock: Sysvar<'info, Clock>, - /// CHECK: Verified Instructions Sysvar. #[account(address = sysvar::instructions::ID)] pub sysvar_instructions: UncheckedAccount<'info>, @@ -378,14 +386,6 @@ pub enum BeamError { #[msg("Can't remove a beam with a non-zero allocation")] NonZeroAllocation, - /// Thrown if the epoch report is updated with an incorrect amount of beam epoch reports - #[msg("Incorrect amount of beam epoch reports")] - IncorrectBeamEpochReportCount, - - /// Thrown if the epoch report is updated with beam epoch reports from the wrong epoch - #[msg("Incorrect epoch for beam epoch reports")] - IncorrectBeamEpochReportEpoch, - /// Thrown if the epoch report is updated by an incorrect beam epoch report #[msg("Incorrect beam epoch report")] IncorrectBeamEpochReport, diff --git a/programs/sunrise-core/src/seeds.rs b/programs/sunrise-core/src/seeds.rs index 22bf191..1721149 100644 --- a/programs/sunrise-core/src/seeds.rs +++ b/programs/sunrise-core/src/seeds.rs @@ -1,2 +1 @@ pub const GSOL_AUTHORITY: &[u8] = b"gsol_mint_authority"; -pub const EPOCH_REPORT: &[u8] = b"epoch_report"; diff --git a/programs/sunrise-core/src/state.rs b/programs/sunrise-core/src/state.rs index 60269a8..2843c3f 100644 --- a/programs/sunrise-core/src/state.rs +++ b/programs/sunrise-core/src/state.rs @@ -18,9 +18,6 @@ pub struct State { /// Bump of the gSol mint authority PDA. pub gsol_mint_authority_bump: u8, - /// Bump of the eppch report PDA. - pub epoch_report_bump: u8, - /// The Sunrise yield account. pub yield_account: Pubkey, @@ -29,9 +26,11 @@ pub struct State { /// Holds [BeamDetails] for all supported beams. pub allocations: Vec, + + pub epoch_report: EpochReport, } -/// Holds information about a registed beam. +/// Holds information about a registered beam. #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, Default, Eq, Hash, PartialEq)] pub struct BeamDetails { /// The beam's signer for mint and burn requests. @@ -77,14 +76,16 @@ impl State { 32 + // gsol_mint 8 + // pre_supply 1 + // gsol_mint_authority_bump - 1 + // epoch_report_bump 32 + // yield_account 128 + // reserved_space - 4; // vec size + 4; // allocations vec size + // Does not include epoch_report min size (included in size() and size /// Calculate the borsh-serialized size of a state with `beam_count` number of beams. pub fn size(beam_count: usize) -> usize { - Self::SIZE_WITH_ZERO_BEAMS + (BeamDetails::SIZE * beam_count) // allocations vec + Self::SIZE_WITH_ZERO_BEAMS + + (BeamDetails::SIZE * beam_count) +// allocations vec + EpochReport::size(beam_count) // epoch_reports } /// Calculate the size of a state account. @@ -99,8 +100,7 @@ impl State { gsol_mint_auth_bump: u8, gsol_mint: &Pubkey, gsol_mint_supply: u64, - epoch_report_bump: u8, - ) { + ) -> Result<()> { self.update_authority = input.update_authority; self.yield_account = input.yield_account; self.gsol_mint = *gsol_mint; @@ -110,11 +110,7 @@ impl State { self.pre_supply = gsol_mint_supply; self.gsol_mint_authority_bump = gsol_mint_auth_bump; - // The epoch report bump is used to derive the PDA of this state's epoch report account. - // Storing it here reduces the cost of subsequent update operations. - self.epoch_report_bump = epoch_report_bump; - - // We fill up the vec because deserialization of an empty vec would result + // We fill up the vecs because deserialization of an empty vec would result // in the active capacity being lost. i.e a vector with capacity 10 but length // 4 would be deserialized as having both length and capacity of 4. // @@ -125,6 +121,11 @@ impl State { // * Add a beam by finding and replacing a default BeamDetails struct. // * Remove a beam by replacing it with a default BeamDetails struct. self.allocations = vec![BeamDetails::default(); input.initial_capacity as usize]; + + let current_epoch = Clock::get()?.epoch; + self.epoch_report = EpochReport::new(input.initial_capacity as usize, current_epoch); + + Ok(()) } /// Update the fields of a [State] object. @@ -206,6 +207,10 @@ impl State { pub fn contains_beam(&self, key: &Pubkey) -> bool { self.get_beam_details(key).is_some() } + + pub fn find_beam_index(&self, key: &Pubkey) -> Option { + self.allocations.iter().position(|x| x.key == *key) + } } #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] @@ -232,34 +237,135 @@ pub struct AllocationUpdate { pub new_allocation: u8, } -#[account] +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, Default, Eq, Hash, PartialEq)] pub struct EpochReport { + pub current_gsol_supply: u64, + + /// Holds [BeamEpochDetails] for all supported beams. + pub beam_epoch_details: Vec, +} +impl EpochReport { + pub const SIZE_WITH_ZERO_BEAMS: usize = 8 + // current_gsol_supply + 4; // vec size + + pub fn new(beam_count: usize, current_epoch: u64) -> EpochReport { + EpochReport { + current_gsol_supply: 0, + // see details in the State::register method + beam_epoch_details: vec![BeamEpochDetails::new(current_epoch); beam_count], + } + } + + /// Calculate the borsh-serialized size of a state with `beam_count` number of beams. + pub fn size(beam_count: usize) -> usize { + Self::SIZE_WITH_ZERO_BEAMS + (BeamEpochDetails::SIZE * beam_count) // allocations vec + } + + /// Calculate the size of a state account. + pub fn size_inner(&self) -> usize { + EpochReport::size(self.beam_epoch_details.len()) + } + + pub fn extractable_yield(&self) -> u64 { + self.beam_epoch_details + .iter() + .map(|x| x.extractable_yield) + .sum() + } + + pub fn extracted_yield(&self) -> u64 { + self.beam_epoch_details + .iter() + .map(|x| x.extracted_yield) + .sum() + } + + pub fn is_epoch_reported(&self, epoch: u64) -> bool { + self.beam_epoch_details.iter().all(|x| x.epoch == epoch) + } + + pub fn is_epoch_reported_for_beam_idx(&self, epoch: u64, beam_idx: usize) -> bool { + self.beam_epoch_details[beam_idx].epoch == epoch + } + + pub fn extract_yield_for_beam( + &mut self, + beam_idx: usize, + yield_amount: u64, + epoch: u64, + ) -> Result<()> { + let beam_details = &mut self.beam_epoch_details[beam_idx]; + + // The epoch report must be already updated for this epoch and beam + require!( + beam_details.epoch == epoch, + BeamError::EpochReportNotUpToDate + ); + + beam_details.extracted_yield = beam_details + .extracted_yield + .checked_add(yield_amount) + .ok_or(BeamError::Overflow) + .unwrap(); + + // The extractable yield should be reduced (most likely to zero) + beam_details.extractable_yield = + beam_details.extractable_yield.saturating_sub(yield_amount); + + Ok(()) + } + + pub fn update_extractable_yield_and_epoch_for_beam( + &mut self, + beam_idx: usize, + epoch: u64, + extractable_yield: u64, + ) { + self.beam_epoch_details[beam_idx].epoch = epoch; + self.beam_epoch_details[beam_idx].extractable_yield = extractable_yield; + } +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, Default, Eq, Hash, PartialEq)] +pub struct BeamEpochDetails { + /// The most recent epoch that this beam has reported its extractable yield for pub epoch: u64, pub extractable_yield: u64, pub extracted_yield: u64, - pub current_gsol_supply: u64, - pub bump: u8, } -impl EpochReport { - pub const SIZE: usize = 8 + // discriminator - 8 + // epoch - 8 + // extractable_yield - 8 + // extracted_yield - 8 + // current_gsol_supply - 1; // bump +impl BeamEpochDetails { + pub const SIZE: usize = 8 + // epoch + 8 + // extractable_yield + 8; // extracted_yield + + pub fn new(epoch: u64) -> BeamEpochDetails { + BeamEpochDetails { + epoch, + extractable_yield: 0, + extracted_yield: 0, + } + } } #[cfg(test)] mod internal_tests { use super::*; + // Stub the Clock sysvar + struct SyscallStubs {} + impl solana_sdk::program_stubs::SyscallStubs for SyscallStubs { + fn sol_get_clock_sysvar(&self, _var_addr: *mut u8) -> u64 { + 0 + } + } + #[test] fn test_register_beam() { let mut state = State::default(); let mut input = RegisterStateInput::default(); input.initial_capacity = 10; - state.register(input, 0, &Pubkey::default(), 1000, 0); + state.register(input, 0, &Pubkey::default(), 1000).unwrap(); assert_eq!(state.allocations, vec![BeamDetails::default(); 10]); } #[test] @@ -292,13 +398,17 @@ mod internal_tests { assert_eq!(state.beam_count(), 2); } + #[test] fn test_add_beam() { + solana_sdk::program_stubs::set_syscall_stubs(Box::new(SyscallStubs {})); let mut state = State::default(); let mut input = RegisterStateInput::default(); input.initial_capacity = 2; - state.register(input, 0, &Pubkey::new_unique(), 1000, 0); + state + .register(input, 0, &Pubkey::new_unique(), 1000) + .unwrap(); let beam_key = Pubkey::new_unique(); let new_beam = BeamDetails::new(beam_key, 10); @@ -314,13 +424,13 @@ mod internal_tests { ) ); - assert!(state.contains_beam(&beam_key) == true); - assert!(state.beam_count() == 1); + assert_eq!(state.contains_beam(&beam_key), true); + assert_eq!(state.beam_count(), 1); assert!(state .add_beam(BeamDetails::new(Pubkey::new_unique(), 20)) .is_ok()); - assert!(state.beam_count() == 2); + assert_eq!(state.beam_count(), 2); // Should fail because no space let expect_to_fail2 = state.add_beam(BeamDetails::new(Pubkey::new_unique(), 20)); @@ -388,15 +498,15 @@ mod internal_tests { )); if let Some(res) = state.get_beam_details(&key) { - assert!(res.key == key); - assert!(res.allocation == 90); + assert_eq!(res.key, key); + assert_eq!(res.allocation, 90); } else { panic!("") } if let Some(res) = state.get_mut_beam_details(&key) { - assert!(res.key == key); - assert!(res.allocation == 90); + assert_eq!(res.key, key); + assert_eq!(res.allocation, 90); } else { panic!("") } @@ -406,13 +516,11 @@ mod internal_tests { let mut state = State::default(); let initial_size = state.size_inner(); - assert!(state.allocations.len() == 0); - assert!(initial_size == State::SIZE_WITH_ZERO_BEAMS); - assert!(initial_size == State::size(0)); + assert_eq!(state.allocations.len(), 0); + assert_eq!(initial_size, State::size(0)); state.allocations = vec![BeamDetails::default(); 10]; let size = state.size_inner(); - assert!(size == State::size(10)); - assert!(size == initial_size + (10 * BeamDetails::SIZE)) + assert_eq!(size, State::size(10)); } } diff --git a/programs/sunrise-core/src/system.rs b/programs/sunrise-core/src/system.rs index 2437ca2..e656701 100644 --- a/programs/sunrise-core/src/system.rs +++ b/programs/sunrise-core/src/system.rs @@ -1,20 +1,18 @@ use crate::{state::State, BeamError}; -use anchor_lang::prelude::*; +use anchor_lang::prelude::{Account, AccountInfo, Pubkey}; /// Verifies that a mint request is valid by: /// - Checking that the beam is present in the state. /// - Checking that the executing program owns the beam. -pub fn check_beam_validity( +pub fn checked_find_beam_idx( state: &Account<'_, State>, beam: &AccountInfo, cpi_program_id: &Pubkey, -) -> Result<()> { - if !state.contains_beam(beam.key) { - return Err(BeamError::UnidentifiedBeam.into()); - } +) -> Result { if beam.owner != cpi_program_id { - return Err(BeamError::UnidentifiedCallingProgram.into()); + return Err(BeamError::UnidentifiedCallingProgram); } - Ok(()) + let index = state.find_beam_index(beam.key); + index.ok_or(BeamError::UnidentifiedBeam) } diff --git a/programs/sunrise-core/tests/helpers/context.rs b/programs/sunrise-core/tests/helpers/context.rs index 7f049a1..4244e58 100644 --- a/programs/sunrise-core/tests/helpers/context.rs +++ b/programs/sunrise-core/tests/helpers/context.rs @@ -13,7 +13,6 @@ pub struct SunriseContext { pub update_authority: Keypair, pub state: Pubkey, pub gsol_mint_authority: Option<(Pubkey, u8)>, - pub epoch_report: Option<(Pubkey, u8)>, } impl SunriseContext { @@ -26,12 +25,10 @@ impl SunriseContext { initial_capacity: u8, ) -> Result { let gsol_mint_authority = Self::find_gsol_mint_authority_pda(&state.pubkey()); - let epoch_report = Self::find_epoch_report_pda(&state.pubkey()); let (_, instruction) = register_state( &ctx.payer.pubkey(), &gsol_mint.pubkey(), - &epoch_report.0, update_authority, &state.pubkey(), yield_account, @@ -43,7 +40,6 @@ impl SunriseContext { ctx: RefCell::new(ctx), state: state.pubkey(), gsol_mint_authority: Some(gsol_mint_authority), - epoch_report: Some(epoch_report), update_authority: Keypair::new(), }; @@ -180,16 +176,4 @@ impl SunriseContext { let seeds = &[state.as_ref(), sunrise_core::seeds::GSOL_AUTHORITY]; Pubkey::find_program_address(seeds, &sunrise_core::id()) } - - #[allow(dead_code)] - fn epoch_report(&self) -> Pubkey { - self.epoch_report - .map(|a| a.0) - .unwrap_or(Self::find_epoch_report_pda(&self.state).0) - } - - pub fn find_epoch_report_pda(state: &Pubkey) -> (Pubkey, u8) { - let seeds = &[state.as_ref(), sunrise_core::seeds::EPOCH_REPORT]; - Pubkey::find_program_address(seeds, &sunrise_core::id()) - } } diff --git a/programs/sunrise-core/tests/helpers/instructions.rs b/programs/sunrise-core/tests/helpers/instructions.rs index e5b158a..6165cdd 100644 --- a/programs/sunrise-core/tests/helpers/instructions.rs +++ b/programs/sunrise-core/tests/helpers/instructions.rs @@ -5,7 +5,6 @@ use sunrise_core::{accounts as sunrise_accounts, instruction as sunrise_instruct pub fn register_state( payer: &Pubkey, gsol_mint: &Pubkey, - epoch_report: &Pubkey, update_authority: &Pubkey, state: &Pubkey, yield_account: &Pubkey, @@ -15,7 +14,6 @@ pub fn register_state( let accounts = sunrise_accounts::RegisterState { payer: *payer, state: *state, - epoch_report: *epoch_report, gsol_mint: *gsol_mint, gsol_mint_authority: *gsol_mint_auth_pda, system_program: system_program::id(), diff --git a/yarn.lock b/yarn.lock index 33456a0..6cd8074 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2305,10 +2305,10 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shiki@^0.14.1: - version "0.14.4" - resolved "https://registry.yarnpkg.com/shiki/-/shiki-0.14.4.tgz#2454969b466a5f75067d0f2fa0d7426d32881b20" - integrity sha512-IXCRip2IQzKwxArNNq1S+On4KPML3Yyn8Zzs/xRgcgOWIr8ntIK3IKzjFPfjy/7kt9ZMjc+FItfqHRBg8b6tNQ== +shiki@^0.14.7: + version "0.14.7" + resolved "https://registry.yarnpkg.com/shiki/-/shiki-0.14.7.tgz#c3c9e1853e9737845f1d2ef81b31bcfb07056d4e" + integrity sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg== dependencies: ansi-sequence-parser "^1.1.0" jsonc-parser "^3.2.0" @@ -2567,15 +2567,15 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -typedoc@^0.25.1: - version "0.25.1" - resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.25.1.tgz#50de2d8fb93623fbfb59e2fa6407ff40e3d3f438" - integrity sha512-c2ye3YUtGIadxN2O6YwPEXgrZcvhlZ6HlhWZ8jQRNzwLPn2ylhdGqdR8HbyDRyALP8J6lmSANILCkkIdNPFxqA== +typedoc@^0.25.8: + version "0.25.8" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.25.8.tgz#7d0e1bf12d23bf1c459fd4893c82cb855911ff12" + integrity sha512-mh8oLW66nwmeB9uTa0Bdcjfis+48bAjSH3uqdzSuSawfduROQLlXw//WSNZLYDdhmMVB7YcYZicq6e8T0d271A== dependencies: lunr "^2.3.9" marked "^4.3.0" minimatch "^9.0.3" - shiki "^0.14.1" + shiki "^0.14.7" typescript@^5.3.2: version "5.3.2"