From 4162b090ebe7d500f9c39392c7145de2c13858c5 Mon Sep 17 00:00:00 2001 From: dankelleher Date: Sun, 31 Dec 2023 12:24:05 +0100 Subject: [PATCH 01/10] Update epoch report and extract yield core support. --- Anchor.toml | 2 +- .../sdks/common/src/types/sunrise_core.ts | 48 +++++++++++++++++++ packages/sdks/core/src/constants.ts | 2 +- packages/tests/fixtures/core/empty-state.json | 6 +-- packages/tests/fixtures/core/gsol-mint.json | 2 +- .../tests/src/functional/beams/core.test.ts | 2 +- programs/sunrise-core/src/state.rs | 13 ++++- 7 files changed, 66 insertions(+), 9 deletions(-) diff --git a/Anchor.toml b/Anchor.toml index 1ae13a0..42a4a8e 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -39,7 +39,7 @@ program = "packages/tests/fixtures/spl/spl_stake_pool.so" address = "89wj5p56PTFiKQcHLTkx78jM3Cv4jVRCXgMKJvoFvvp" filename = "packages/tests/fixtures/core/empty-state.json" [[test.validator.account]] #gsol mint -address = "HGFi2ubkrCKdxqeAHXDTRAMzyquKqWj3xdT9kz2ux7gi" +address = "EqhYZpTHLvfKgn5oBz1jxLVB3kQH33W8eDLbBNCJqfzS" filename = "packages/tests/fixtures/core/gsol-mint.json" # Beam Accounts diff --git a/packages/sdks/common/src/types/sunrise_core.ts b/packages/sdks/common/src/types/sunrise_core.ts index 4839d35..62c2a13 100644 --- a/packages/sdks/common/src/types/sunrise_core.ts +++ b/packages/sdks/common/src/types/sunrise_core.ts @@ -470,6 +470,18 @@ export type SunriseCore = { ], "type": "publicKey" }, + { + "name": "reservedSpace", + "docs": [ + "Reserved space for adding future fields." + ], + "type": { + "array": [ + "u32", + 32 + ] + } + }, { "name": "allocations", "docs": [ @@ -549,6 +561,18 @@ export type SunriseCore = { "A beam in drain accepts withdrawals but not deposits." ], "type": "bool" + }, + { + "name": "reservedSpace", + "docs": [ + "Reserved space for adding future fields." + ], + "type": { + "array": [ + "u32", + 32 + ] + } } ] } @@ -1182,6 +1206,18 @@ export const IDL: SunriseCore = { ], "type": "publicKey" }, + { + "name": "reservedSpace", + "docs": [ + "Reserved space for adding future fields." + ], + "type": { + "array": [ + "u32", + 32 + ] + } + }, { "name": "allocations", "docs": [ @@ -1261,6 +1297,18 @@ export const IDL: SunriseCore = { "A beam in drain accepts withdrawals but not deposits." ], "type": "bool" + }, + { + "name": "reservedSpace", + "docs": [ + "Reserved space for adding future fields." + ], + "type": { + "array": [ + "u32", + 32 + ] + } } ] } diff --git a/packages/sdks/core/src/constants.ts b/packages/sdks/core/src/constants.ts index d792177..5f0273c 100644 --- a/packages/sdks/core/src/constants.ts +++ b/packages/sdks/core/src/constants.ts @@ -7,4 +7,4 @@ 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"; \ No newline at end of file +export const EPOCH_REPORT_SEED = "epoch_report"; diff --git a/packages/tests/fixtures/core/empty-state.json b/packages/tests/fixtures/core/empty-state.json index 3e72662..0339d9a 100644 --- a/packages/tests/fixtures/core/empty-state.json +++ b/packages/tests/fixtures/core/empty-state.json @@ -1,14 +1,14 @@ { "pubkey": "89wj5p56PTFiKQcHLTkx78jM3Cv4jVRCXgMKJvoFvvp", "account": { - "lamports": 6090000, + "lamports": 6096960, "data": [ - "2JJrXmhLtrHJi086QhFaVlGIukKID4tkZHDOjVITr4RH7vx5aT2XqfGjVS5s9M0otXTMzigTnRm3wxj5+4qWm7acOEGIRU8PAAAAAAAAAAD/DZVjk9SBDG8vBV/2PAd7V82Q8jKb95Vcw/R5gMEdinIPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "2JJrXmhLtrHJi086QhFaVlGIukKID4tkZHDOjVITr4RH7vx5aT2Xqc2hpwNXs05dP6XFw8EiAxG+oWnkP5J1x/Y3KJwMAngLAAAAAAAAAAD//XTkcY2+Bb9ZFikxQyz/hY58xeSkY6ATmfCxiGP0DyhBDwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", "base64" ], "owner": "suncPB4RR39bMwnRhCym6ZLKqMfnFG83vjzVVuXNhCq", "executable": false, "rentEpoch": 18446744073709551615, - "space": 747 + "space": 748 } } \ No newline at end of file diff --git a/packages/tests/fixtures/core/gsol-mint.json b/packages/tests/fixtures/core/gsol-mint.json index 40a6867..c5be148 100644 --- a/packages/tests/fixtures/core/gsol-mint.json +++ b/packages/tests/fixtures/core/gsol-mint.json @@ -1,5 +1,5 @@ { - "pubkey": "HGFi2ubkrCKdxqeAHXDTRAMzyquKqWj3xdT9kz2ux7gi", + "pubkey": "EqhYZpTHLvfKgn5oBz1jxLVB3kQH33W8eDLbBNCJqfzS", "account": { "lamports": 1461600, "data": [ diff --git a/packages/tests/src/functional/beams/core.test.ts b/packages/tests/src/functional/beams/core.test.ts index 7fbf373..09a892e 100644 --- a/packages/tests/src/functional/beams/core.test.ts +++ b/packages/tests/src/functional/beams/core.test.ts @@ -12,7 +12,7 @@ import BN from "bn.js"; import { provider } from "../setup.js"; import { expect } from "chai"; -const BEAM_DETAILS_LEN: number = 42; +const BEAM_DETAILS_LEN: number = 170; describe("Sunrise core", () => { let gsolMint: PublicKey; diff --git a/programs/sunrise-core/src/state.rs b/programs/sunrise-core/src/state.rs index d0543ba..1b604ec 100644 --- a/programs/sunrise-core/src/state.rs +++ b/programs/sunrise-core/src/state.rs @@ -3,7 +3,7 @@ use anchor_lang::prelude::*; /// The state for the Sunrise beam controller program. #[account] -#[derive(Debug, Default)] +#[derive(Debug)] pub struct State { /// Update authority for this state. pub update_authority: Pubkey, @@ -24,6 +24,9 @@ pub struct State { /// The Sunrise yield account. pub yield_account: Pubkey, + /// Reserved space for adding future fields. + pub reserved_space: [u32; 32], // 128 bytes - used u32;32 over u8;128 to take advantage of rust's built-in default trait implementation for 32-sized arrays + /// Holds [BeamDetails] for all supported beams. pub allocations: Vec, } @@ -42,6 +45,9 @@ pub struct BeamDetails { /// A beam in drain accepts withdrawals but not deposits. pub draining_mode: bool, + + /// Reserved space for adding future fields. + pub reserved_space: [u32; 32], // 128 bytes - used u32;32 over u8;128 to take advantage of rust's built-in default trait implementation for 32-sized arrays } impl BeamDetails { @@ -49,7 +55,8 @@ impl BeamDetails { pub const SIZE: usize = 32 + // key 1 + // allocation 8 + // minted - 1; // draining_mode + 1 + // draining_mode + 128; // reserved_space /// Create a new instance of Self. pub fn new(key: Pubkey, allocation: u8) -> Self { @@ -58,6 +65,7 @@ impl BeamDetails { allocation, partial_gsol_supply: 0, draining_mode: false, // initially set draining_mode to false. + reserved_space: Default::default(), } } } @@ -71,6 +79,7 @@ impl State { 1 + // gsol_mint_authority_bump 1 + // epoch_report_bump 32 + // yield_account + 128 + // reserved_space 4; // vec size /// Calculate the borsh-serialized size of a state with `beam_count` number of beams. From aed04660a8ac1aeb8a7f6ade5959a7b749579919 Mon Sep 17 00:00:00 2001 From: dankelleher Date: Thu, 4 Jan 2024 07:37:41 +0000 Subject: [PATCH 02/10] SPL Stake Pool refactoring and ExtractYield WIP --- packages/sdks/common/src/BeamInterface.ts | 2 - .../sdks/common/src/types/marinade_beam.ts | 36 +-- packages/sdks/common/src/types/spl_beam.ts | 256 +++++++++++++++--- packages/sdks/marinade-sp/src/index.ts | 2 - packages/sdks/marinade-sp/src/state.ts | 3 - packages/sdks/spl/src/index.ts | 18 +- packages/sdks/spl/src/state.ts | 3 - packages/sdks/spl/src/utils.ts | 5 +- packages/tests/package.json | 2 +- .../src/functional/beams/marinade-sp.test.ts | 22 +- .../functional/beams/spl-stake-pool.test.ts | 11 +- .../combined-beams/marinade-beams.test.ts | 3 + programs/marinade-beam/src/lib.rs | 21 +- programs/marinade-beam/src/state.rs | 6 - programs/spl-beam/README.md | 19 ++ programs/spl-beam/src/cpi_interface/mod.rs | 2 + .../spl-beam/src/cpi_interface/program.rs | 25 ++ programs/spl-beam/src/cpi_interface/spl.rs | 155 ++++++++--- .../spl-beam/src/cpi_interface/stake_pool.rs | 32 +++ .../spl-beam/src/cpi_interface/sunrise.rs | 6 +- programs/spl-beam/src/lib.rs | 208 +++++++++----- programs/spl-beam/src/seeds.rs | 6 + programs/spl-beam/src/state.rs | 6 - programs/spl-beam/src/utils.rs | 32 ++- programs/sunrise-core/src/state.rs | 8 +- 25 files changed, 649 insertions(+), 240 deletions(-) create mode 100644 programs/spl-beam/README.md create mode 100644 programs/spl-beam/src/cpi_interface/program.rs create mode 100644 programs/spl-beam/src/cpi_interface/stake_pool.rs create mode 100644 programs/spl-beam/src/seeds.rs diff --git a/packages/sdks/common/src/BeamInterface.ts b/packages/sdks/common/src/BeamInterface.ts index 65f18fd..6bb7ca9 100644 --- a/packages/sdks/common/src/BeamInterface.ts +++ b/packages/sdks/common/src/BeamInterface.ts @@ -70,8 +70,6 @@ export interface BeamState { sunriseState: PublicKey; /** Bump of the PDA account that owns the vault where tokens are stored.*/ vaultAuthorityBump: number; - /** SOL treasury account for the beam. */ - treasury: PublicKey; } /** @type {BeamCapability}: supports sol deposits.*/ diff --git a/packages/sdks/common/src/types/marinade_beam.ts b/packages/sdks/common/src/types/marinade_beam.ts index 90929c7..4bd8c31 100644 --- a/packages/sdks/common/src/types/marinade_beam.ts +++ b/packages/sdks/common/src/types/marinade_beam.ts @@ -692,6 +692,11 @@ export type MarinadeBeam = { "isMut": false, "isSigner": false }, + { + "name": "sunriseState", + "isMut": false, + "isSigner": false + }, { "name": "marinadeState", "isMut": true, @@ -718,7 +723,7 @@ export type MarinadeBeam = { "isSigner": false }, { - "name": "treasury", + "name": "yieldAccount", "isMut": true, "isSigner": false }, @@ -771,13 +776,6 @@ export type MarinadeBeam = { ], "type": "u8" }, - { - "name": "treasury", - "docs": [ - "This state's SOL vault." - ], - "type": "publicKey" - }, { "name": "partialGsolSupply", "docs": [ @@ -875,10 +873,6 @@ export type MarinadeBeam = { { "name": "vaultAuthorityBump", "type": "u8" - }, - { - "name": "treasury", - "type": "publicKey" } ] } @@ -1612,6 +1606,11 @@ export const IDL: MarinadeBeam = { "isMut": false, "isSigner": false }, + { + "name": "sunriseState", + "isMut": false, + "isSigner": false + }, { "name": "marinadeState", "isMut": true, @@ -1638,7 +1637,7 @@ export const IDL: MarinadeBeam = { "isSigner": false }, { - "name": "treasury", + "name": "yieldAccount", "isMut": true, "isSigner": false }, @@ -1691,13 +1690,6 @@ export const IDL: MarinadeBeam = { ], "type": "u8" }, - { - "name": "treasury", - "docs": [ - "This state's SOL vault." - ], - "type": "publicKey" - }, { "name": "partialGsolSupply", "docs": [ @@ -1795,10 +1787,6 @@ export const IDL: MarinadeBeam = { { "name": "vaultAuthorityBump", "type": "u8" - }, - { - "name": "treasury", - "type": "publicKey" } ] } diff --git a/packages/sdks/common/src/types/spl_beam.ts b/packages/sdks/common/src/types/spl_beam.ts index 4a2c041..24e55c8 100644 --- a/packages/sdks/common/src/types/spl_beam.ts +++ b/packages/sdks/common/src/types/spl_beam.ts @@ -21,7 +21,7 @@ export type SplBeam = { "isSigner": false }, { - "name": "poolTokensVault", + "name": "poolTokenVault", "isMut": true, "isSigner": false }, @@ -112,7 +112,7 @@ export type SplBeam = { "isSigner": false }, { - "name": "poolTokensVault", + "name": "poolTokenVault", "isMut": true, "isSigner": false }, @@ -155,7 +155,7 @@ export type SplBeam = { "isSigner": false }, { - "name": "beamProgram", + "name": "sunriseProgram", "isMut": false, "isSigner": false }, @@ -221,7 +221,7 @@ export type SplBeam = { "isSigner": false }, { - "name": "poolTokensVault", + "name": "poolTokenVault", "isMut": true, "isSigner": false }, @@ -294,7 +294,7 @@ export type SplBeam = { "isSigner": false }, { - "name": "beamProgram", + "name": "sunriseProgram", "isMut": false, "isSigner": false }, @@ -350,7 +350,7 @@ export type SplBeam = { "isSigner": false }, { - "name": "poolTokensVault", + "name": "poolTokenVault", "isMut": true, "isSigner": false }, @@ -469,7 +469,7 @@ export type SplBeam = { "isSigner": false }, { - "name": "poolTokensVault", + "name": "poolTokenVault", "isMut": true, "isSigner": false }, @@ -527,7 +527,7 @@ export type SplBeam = { "isSigner": false }, { - "name": "beamProgram", + "name": "sunriseProgram", "isMut": false, "isSigner": false }, @@ -563,6 +563,107 @@ export type SplBeam = { "name": "redeemTicket", "accounts": [], "args": [] + }, + { + "name": "extractYield", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "sunriseState", + "isMut": false, + "isSigner": false + }, + { + "name": "stakePool", + "isMut": true, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "poolMint", + "isMut": true, + "isSigner": false + }, + { + "name": "yieldAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "newStakeAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "vaultAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "poolTokenVault", + "isMut": true, + "isSigner": false + }, + { + "name": "stakePoolWithdrawAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "validatorStakeList", + "isMut": false, + "isSigner": false + }, + { + "name": "stakeAccountToSplit", + "isMut": true, + "isSigner": false + }, + { + "name": "managerFeeAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "sysvarClock", + "isMut": false, + "isSigner": false + }, + { + "name": "nativeStakeProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "sunriseProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "splStakePoolProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] } ], "accounts": [ @@ -600,13 +701,6 @@ export type SplBeam = { ], "type": "u8" }, - { - "name": "treasury", - "docs": [ - "This state's SOL vault." - ], - "type": "publicKey" - }, { "name": "partialGsolSupply", "docs": [ @@ -641,10 +735,6 @@ export type SplBeam = { { "name": "vaultAuthorityBump", "type": "u8" - }, - { - "name": "treasury", - "type": "publicKey" } ] } @@ -692,7 +782,7 @@ export const IDL: SplBeam = { "isSigner": false }, { - "name": "poolTokensVault", + "name": "poolTokenVault", "isMut": true, "isSigner": false }, @@ -783,7 +873,7 @@ export const IDL: SplBeam = { "isSigner": false }, { - "name": "poolTokensVault", + "name": "poolTokenVault", "isMut": true, "isSigner": false }, @@ -826,7 +916,7 @@ export const IDL: SplBeam = { "isSigner": false }, { - "name": "beamProgram", + "name": "sunriseProgram", "isMut": false, "isSigner": false }, @@ -892,7 +982,7 @@ export const IDL: SplBeam = { "isSigner": false }, { - "name": "poolTokensVault", + "name": "poolTokenVault", "isMut": true, "isSigner": false }, @@ -965,7 +1055,7 @@ export const IDL: SplBeam = { "isSigner": false }, { - "name": "beamProgram", + "name": "sunriseProgram", "isMut": false, "isSigner": false }, @@ -1021,7 +1111,7 @@ export const IDL: SplBeam = { "isSigner": false }, { - "name": "poolTokensVault", + "name": "poolTokenVault", "isMut": true, "isSigner": false }, @@ -1140,7 +1230,7 @@ export const IDL: SplBeam = { "isSigner": false }, { - "name": "poolTokensVault", + "name": "poolTokenVault", "isMut": true, "isSigner": false }, @@ -1198,7 +1288,7 @@ export const IDL: SplBeam = { "isSigner": false }, { - "name": "beamProgram", + "name": "sunriseProgram", "isMut": false, "isSigner": false }, @@ -1234,6 +1324,107 @@ export const IDL: SplBeam = { "name": "redeemTicket", "accounts": [], "args": [] + }, + { + "name": "extractYield", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "sunriseState", + "isMut": false, + "isSigner": false + }, + { + "name": "stakePool", + "isMut": true, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "poolMint", + "isMut": true, + "isSigner": false + }, + { + "name": "yieldAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "newStakeAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "vaultAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "poolTokenVault", + "isMut": true, + "isSigner": false + }, + { + "name": "stakePoolWithdrawAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "validatorStakeList", + "isMut": false, + "isSigner": false + }, + { + "name": "stakeAccountToSplit", + "isMut": true, + "isSigner": false + }, + { + "name": "managerFeeAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "sysvarClock", + "isMut": false, + "isSigner": false + }, + { + "name": "nativeStakeProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "sunriseProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "splStakePoolProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] } ], "accounts": [ @@ -1271,13 +1462,6 @@ export const IDL: SplBeam = { ], "type": "u8" }, - { - "name": "treasury", - "docs": [ - "This state's SOL vault." - ], - "type": "publicKey" - }, { "name": "partialGsolSupply", "docs": [ @@ -1312,10 +1496,6 @@ export const IDL: SplBeam = { { "name": "vaultAuthorityBump", "type": "u8" - }, - { - "name": "treasury", - "type": "publicKey" } ] } diff --git a/packages/sdks/marinade-sp/src/index.ts b/packages/sdks/marinade-sp/src/index.ts index 8840703..6906b95 100644 --- a/packages/sdks/marinade-sp/src/index.ts +++ b/packages/sdks/marinade-sp/src/index.ts @@ -68,7 +68,6 @@ export class MarinadeClient extends BeamInterface< provider: AnchorProvider, updateAuthority: PublicKey, sunriseState: PublicKey, - treasury: PublicKey, programId = MARINADE_BEAM_PROGRAM_ID, ): Promise { const program = new Program( @@ -95,7 +94,6 @@ export class MarinadeClient extends BeamInterface< marinadeState, sunriseState, vaultAuthorityBump, - treasury, }) .accounts({ payer: provider.publicKey, diff --git a/packages/sdks/marinade-sp/src/state.ts b/packages/sdks/marinade-sp/src/state.ts index 863a1c1..8d0c305 100644 --- a/packages/sdks/marinade-sp/src/state.ts +++ b/packages/sdks/marinade-sp/src/state.ts @@ -9,7 +9,6 @@ export class StateAccount implements BeamState { public readonly proxyState: PublicKey; public readonly sunriseState: PublicKey; public readonly vaultAuthorityBump: number; - public readonly treasury: PublicKey; private constructor( _address: PublicKey, @@ -20,7 +19,6 @@ export class StateAccount implements BeamState { this.proxyState = account.marinadeState; this.sunriseState = account.sunriseState; this.vaultAuthorityBump = account.vaultAuthorityBump; - this.treasury = account.treasury; } /** Create a new instance from an anchor-deserialized account. */ @@ -41,7 +39,6 @@ export class StateAccount implements BeamState { proxyState: this.proxyState.toBase58(), sunriseState: this.sunriseState.toBase58(), vaultAuthorityBump: this.vaultAuthorityBump.toString(), - treasury: this.treasury.toBase58(), }; } } diff --git a/packages/sdks/spl/src/index.ts b/packages/sdks/spl/src/index.ts index 4924e88..cd4254f 100644 --- a/packages/sdks/spl/src/index.ts +++ b/packages/sdks/spl/src/index.ts @@ -97,7 +97,6 @@ export class SplClient extends BeamInterface { provider: AnchorProvider, updateAuthority: PublicKey, sunriseState: PublicKey, - treasury: PublicKey, stakePool: PublicKey, programId = SPL_BEAM_PROGRAM_ID, ): Promise { @@ -130,13 +129,12 @@ export class SplClient extends BeamInterface { stakePool, sunriseState, vaultAuthorityBump, - treasury, }) .accounts({ payer: provider.publicKey, state: stateAddress, poolMint: splClientParams.stakePoolState.poolMint, - poolTokensVault: splClientParams.beamVault, + poolTokenVault: splClientParams.beamVault, vaultAuthority, tokenProgram: TOKEN_PROGRAM_ID, systemProgram: SystemProgram.programId, @@ -212,7 +210,7 @@ export class SplClient extends BeamInterface { depositor, mintGsolTo: gsolATA, poolMint: this.spl.stakePoolState.poolMint, - poolTokensVault: this.spl.beamVault, + poolTokenVault: this.spl.beamVault, vaultAuthority: this.vaultAuthority[0], stakePoolWithdrawAuthority: this.spl.withdrawAuthority, reserveStakeAccount: this.spl.stakePoolState.reserveStake, @@ -220,7 +218,7 @@ export class SplClient extends BeamInterface { gsolMint, gsolMintAuthority, instructionsSysvar, - beamProgram: this.sunrise.program.programId, + sunriseProgram: this.sunrise.program.programId, splStakePoolProgram: SPL_STAKE_POOL_PROGRAM_ID, systemProgram: SystemProgram.programId, tokenProgram: TOKEN_PROGRAM_ID, @@ -254,7 +252,7 @@ export class SplClient extends BeamInterface { withdrawer, gsolTokenAccount: burnGsolFrom, poolMint: this.spl.stakePoolState.poolMint, - poolTokensVault: this.spl.beamVault, + poolTokenVault: this.spl.beamVault, vaultAuthority: this.vaultAuthority[0], stakePoolWithdrawAuthority: this.spl.withdrawAuthority, reserveStakeAccount: this.spl.stakePoolState.reserveStake, @@ -312,7 +310,7 @@ export class SplClient extends BeamInterface { stakeAccount, mintGsolTo: gsolATA, poolMint: this.spl.stakePoolState.poolMint, - poolTokensVault: this.spl.beamVault, + poolTokenVault: this.spl.beamVault, vaultAuthority: this.vaultAuthority[0], validatorList: this.spl.stakePoolState.validatorList, stakePoolDepositAuthority: this.spl.depositAuthority, @@ -326,7 +324,7 @@ export class SplClient extends BeamInterface { gsolMint, gsolMintAuthority, instructionsSysvar, - beamProgram: this.sunrise.program.programId, + sunriseProgram: this.sunrise.program.programId, splStakePoolProgram: SPL_STAKE_POOL_PROGRAM_ID, systemProgram: SystemProgram.programId, tokenProgram: TOKEN_PROGRAM_ID, @@ -362,7 +360,7 @@ export class SplClient extends BeamInterface { gsolTokenAccount: burnGsolFrom, newStakeAccount, poolMint: this.spl.stakePoolState.poolMint, - poolTokensVault: this.spl.beamVault, + poolTokenVault: this.spl.beamVault, vaultAuthority: this.vaultAuthority[0], stakePoolWithdrawAuthority: this.spl.withdrawAuthority, validatorStakeList: this.spl.stakePoolState.validatorList, @@ -373,7 +371,7 @@ export class SplClient extends BeamInterface { nativeStakeProgram: StakeProgram.programId, gsolMint, instructionsSysvar, - beamProgram: this.sunrise.program.programId, + sunriseProgram: this.sunrise.program.programId, splStakePoolProgram: SPL_STAKE_POOL_PROGRAM_ID, systemProgram: SystemProgram.programId, tokenProgram: TOKEN_PROGRAM_ID, diff --git a/packages/sdks/spl/src/state.ts b/packages/sdks/spl/src/state.ts index fbfeb45..511f21f 100644 --- a/packages/sdks/spl/src/state.ts +++ b/packages/sdks/spl/src/state.ts @@ -9,7 +9,6 @@ export class StateAccount implements BeamState { public readonly proxyState: PublicKey; public readonly sunriseState: PublicKey; public readonly vaultAuthorityBump: number; - public readonly treasury: PublicKey; private constructor( _address: PublicKey, @@ -20,7 +19,6 @@ export class StateAccount implements BeamState { this.proxyState = account.stakePool; this.sunriseState = account.sunriseState; this.vaultAuthorityBump = account.vaultAuthorityBump; - this.treasury = account.treasury; } /** Create a new instance from an anchor-deserialized account. */ @@ -41,7 +39,6 @@ export class StateAccount implements BeamState { proxyState: this.proxyState.toBase58(), sunriseState: this.sunriseState.toBase58(), vaultAuthorityBump: this.vaultAuthorityBump.toString(), - treasury: this.treasury.toBase58(), }; } } diff --git a/packages/sdks/spl/src/utils.ts b/packages/sdks/spl/src/utils.ts index 088051e..125460b 100644 --- a/packages/sdks/spl/src/utils.ts +++ b/packages/sdks/spl/src/utils.ts @@ -21,8 +21,8 @@ export type SplClientParams = { * All the constant seeds used for the PDAs of the on-chain program. */ const enum Seeds { - STATE = "sunrise-spl", - VAULT_AUTHORITY = "vault-authority", + STATE = "sunrise_spl", + VAULT_AUTHORITY = "vault_authority", } /** @@ -58,7 +58,6 @@ export class Utils { stateAddress: PublicKey, stakePoolAddress: PublicKey, ): Promise => { - // const marinadeState = await this.loadMarinadeState(provider); const vaultAuthority = Utils.deriveAuthorityAddress( beamProgramId, stateAddress, diff --git a/packages/tests/package.json b/packages/tests/package.json index d4f0ba1..198960e 100644 --- a/packages/tests/package.json +++ b/packages/tests/package.json @@ -6,7 +6,7 @@ "scripts": { "build": "echo NOOP", "lint": "tsc --noEmit && eslint --ext .ts,.tsx src", - "test": "./run-all.sh", + "test": "tsc --noEmit && ./run-all.sh", "test-one": "yarn anchor test --skip-build" }, "dependencies": { diff --git a/packages/tests/src/functional/beams/marinade-sp.test.ts b/packages/tests/src/functional/beams/marinade-sp.test.ts index 24d36b4..6605fd7 100644 --- a/packages/tests/src/functional/beams/marinade-sp.test.ts +++ b/packages/tests/src/functional/beams/marinade-sp.test.ts @@ -3,7 +3,13 @@ * to generate yield. */ import { MarinadeClient } from "@sunrisestake/beams-marinade-sp"; -import { Keypair, LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js"; +import { SunriseClient } from "@sunrisestake/beams-core"; +import { + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + AccountInfo, +} from "@solana/web3.js"; import BN from "bn.js"; import { createTokenAccount, @@ -20,7 +26,6 @@ import { import { provider, staker, stakerIdentity } from "../setup.js"; import { expect } from "chai"; import { MSOL_MINT } from "../consts.js"; -import { SunriseClient } from "@sunrisestake/beams-core"; describe("Marinade stake pool beam", () => { let coreClient: SunriseClient; @@ -65,12 +70,10 @@ 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); - const treasury = Keypair.generate(); beamClient = await MarinadeClient.initialize( provider, provider.publicKey, sunriseStateAddress, - treasury.publicKey, ); const info = beamClient.state.pretty(); @@ -90,12 +93,11 @@ describe("Marinade stake pool beam", () => { }); it("can update a state", async () => { - const newTreasury = Keypair.generate(); + const newUpdateAuthority = Keypair.generate(); const updateParams = { - updateAuthority: beamClient.state.updateAuthority, + updateAuthority: newUpdateAuthority.publicKey, sunriseState: beamClient.state.sunriseState, vaultAuthorityBump: beamClient.state.vaultAuthorityBump, - treasury: newTreasury.publicKey, marinadeState: beamClient.state.proxyState, }; await sendAndConfirmTransaction( @@ -105,8 +107,8 @@ describe("Marinade stake pool beam", () => { ); beamClient = await beamClient.refresh(); - expect(beamClient.state.treasury.toBase58()).to.equal( - newTreasury.publicKey.toBase58(), + expect(beamClient.state.updateAuthority.toBase58()).to.equal( + newUpdateAuthority.publicKey.toBase58(), ); }); @@ -276,7 +278,7 @@ describe("Marinade stake pool beam", () => { const sunriseLamports = await beamClient.provider.connection .getAccountInfo(sunriseDelayedTicket) - .then((account) => account?.lamports); + .then((account: AccountInfo | null) => account?.lamports); await sendAndConfirmTransaction( beamClient.provider, await beamClient.redeemTicket(sunriseDelayedTicket), 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 7d67c4b..c5c12d4 100644 --- a/packages/tests/src/functional/beams/spl-stake-pool.test.ts +++ b/packages/tests/src/functional/beams/spl-stake-pool.test.ts @@ -38,12 +38,10 @@ describe("SPL stake pool beam", () => { before("Fund the staker", () => fund(provider, staker.publicKey, 100)); it("can initialize a state", async () => { - const treasury = Keypair.generate(); beamClient = await SplClient.initialize( provider, provider.publicKey, sunriseStateAddress, - treasury.publicKey, stakePool, ); @@ -64,12 +62,11 @@ describe("SPL stake pool beam", () => { }); it("can update a state", async () => { - const newTreasury = Keypair.generate(); + const newUpdateAuthority = Keypair.generate(); const updateParams = { - updateAuthority: beamClient.state.updateAuthority, + updateAuthority: newUpdateAuthority.publicKey, sunriseState: beamClient.state.sunriseState, vaultAuthorityBump: beamClient.state.vaultAuthorityBump, - treasury: newTreasury.publicKey, stakePool: beamClient.spl.stakePoolAddress, }; await sendAndConfirmTransaction( @@ -79,8 +76,8 @@ describe("SPL stake pool beam", () => { ); beamClient = await beamClient.refresh(); - expect(beamClient.state.treasury.toBase58()).to.equal( - newTreasury.publicKey.toBase58(), + expect(beamClient.state.updateAuthority.toBase58()).to.equal( + newUpdateAuthority.publicKey.toBase58(), ); }); diff --git a/packages/tests/src/functional/combined-beams/marinade-beams.test.ts b/packages/tests/src/functional/combined-beams/marinade-beams.test.ts index 7cbfaed..62b5863 100644 --- a/packages/tests/src/functional/combined-beams/marinade-beams.test.ts +++ b/packages/tests/src/functional/combined-beams/marinade-beams.test.ts @@ -34,6 +34,9 @@ describe("Marinade beams", () => { // Get core client coreClient = await SunriseClient.get(provider, SUNRISE_CORE_STATE); + // Update capacity + await sendAsAdmin(await coreClient.resizeAllocations(15)); + // register marinade stake pool & marinade liquidity pool beams marinadeLPClient = await MarinadeLpClient.get( provider, diff --git a/programs/marinade-beam/src/lib.rs b/programs/marinade-beam/src/lib.rs index 07ccf5a..08a11ff 100644 --- a/programs/marinade-beam/src/lib.rs +++ b/programs/marinade-beam/src/lib.rs @@ -259,7 +259,7 @@ pub mod marinade_beam { Ok(()) } - pub fn extract_yield(ctx: Context) -> Result<()> { + pub fn extract_yield(ctx: Context) -> Result<()> { let yield_lamports = utils::extractable_yield( &ctx.accounts.state, &ctx.accounts.marinade_state, @@ -272,7 +272,7 @@ pub mod marinade_beam { 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 treasury", yield_msol); + msg!("Withdrawing {} msol to yield account", yield_msol); // TODO: Legacy code uses liquid-unstake but leaves the note above. // Move to delayed-unstakes here? Ok(()) @@ -653,9 +653,16 @@ pub struct RedeemTicket<'info> { } #[derive(Accounts, Clone)] -pub struct ExtractToTreasury<'info> { - #[account(has_one = marinade_state)] +pub struct ExtractYield<'info> { + #[account( + has_one = marinade_state, + has_one = sunrise_state + )] pub state: Box>, + #[account( + has_one = yield_account + )] + pub sunrise_state: Box>, #[account(mut)] pub marinade_state: Box>, @@ -681,9 +688,9 @@ pub struct ExtractToTreasury<'info> { )] pub vault_authority: UncheckedAccount<'info>, - #[account(mut,constraint = treasury.key() == state.treasury)] - /// CHECK: Matches the treasury key stored in the state. - pub treasury: UncheckedAccount<'info>, + #[account(mut)] + /// CHECK: Matches the yield account key stored in the state. + pub yield_account: UncheckedAccount<'info>, #[account( mut, diff --git a/programs/marinade-beam/src/state.rs b/programs/marinade-beam/src/state.rs index ae50db4..f9b774e 100644 --- a/programs/marinade-beam/src/state.rs +++ b/programs/marinade-beam/src/state.rs @@ -15,9 +15,6 @@ pub struct State { /// that holds pool tokens (msol in this case). pub vault_authority_bump: u8, - /// This state's SOL vault. - pub treasury: Pubkey, - /// The amount of the current gsol supply this beam is responsible for. /// This field is also tracked in the matching beam-details struct in the /// sunrise program's state and is expected to match that value. @@ -35,7 +32,6 @@ pub struct StateEntry { pub marinade_state: Pubkey, pub sunrise_state: Pubkey, pub vault_authority_bump: u8, - pub treasury: Pubkey, } impl From for State { @@ -45,7 +41,6 @@ impl From for State { marinade_state: se.marinade_state, sunrise_state: se.sunrise_state, vault_authority_bump: se.vault_authority_bump, - treasury: se.treasury, partial_gsol_supply: 0, } } @@ -57,6 +52,5 @@ impl State { 32 + /*marinade_state*/ 32 + /*sunrise_state*/ 1 + /*vault_authority_bump*/ - 32 + /*treasury*/ 8; /*partial_gsol_supply*/ } diff --git a/programs/spl-beam/README.md b/programs/spl-beam/README.md new file mode 100644 index 0000000..4902cfb --- /dev/null +++ b/programs/spl-beam/README.md @@ -0,0 +1,19 @@ +# SPL Stake Pool + +## How Yield Extraction Works + +Yield extraction is delayed in this Beam. This means that a yield extraction must first be ordered, +resulting in a stake account, whose withdraw authority is a PDA derived from the beam state. + +The stake account is on "cooldown", meaning it is available for withdrawal after a certain number of +epochs, typically one (2-3 days). + +After this time, the stake account can be redeemed (withdrawn) to the yield account. + +Yield extraction cannot happen if there is already an unredeemed stake account. + +# How Rebalancing works + +Similarly to yield extraction, rebalancing is delayed and in two steps. + +Step 1: A rebalance order is placed, resulting in a stake account, whose withdraw authority is the same \ No newline at end of file diff --git a/programs/spl-beam/src/cpi_interface/mod.rs b/programs/spl-beam/src/cpi_interface/mod.rs index ce049ca..a51f9bd 100644 --- a/programs/spl-beam/src/cpi_interface/mod.rs +++ b/programs/spl-beam/src/cpi_interface/mod.rs @@ -1,2 +1,4 @@ +pub mod program; pub mod spl; +pub mod stake_pool; pub mod sunrise; diff --git a/programs/spl-beam/src/cpi_interface/program.rs b/programs/spl-beam/src/cpi_interface/program.rs new file mode 100644 index 0000000..454fb63 --- /dev/null +++ b/programs/spl-beam/src/cpi_interface/program.rs @@ -0,0 +1,25 @@ +use anchor_lang::prelude::Pubkey; +use anchor_lang::solana_program::stake; +use anchor_lang::Id; + +// SPoo1Ku8WFXoNDMHPsrGSTSG1Y47rzgn41SLUNakuHy +pub const SPL_STAKE_POOL_PROGRAM_ID: Pubkey = Pubkey::new_from_array([ + 6, 129, 78, 212, 202, 246, 138, 23, 70, 114, 253, 172, 134, 3, 26, 99, 232, 78, 161, 94, 250, + 29, 68, 183, 34, 147, 246, 219, 219, 0, 22, 80, +]); + +pub struct SplStakePool; + +impl Id for SplStakePool { + fn id() -> Pubkey { + SPL_STAKE_POOL_PROGRAM_ID + } +} + +pub struct NativeStakeProgram; + +impl Id for NativeStakeProgram { + fn id() -> Pubkey { + stake::program::ID + } +} diff --git a/programs/spl-beam/src/cpi_interface/spl.rs b/programs/spl-beam/src/cpi_interface/spl.rs index 37cb7e6..d384bd9 100644 --- a/programs/spl-beam/src/cpi_interface/spl.rs +++ b/programs/spl-beam/src/cpi_interface/spl.rs @@ -1,3 +1,7 @@ +use crate::cpi_interface::stake_pool::StakePool; +use crate::seeds::*; +use crate::state::State; +use crate::{ExtractYield, WithdrawStake}; use anchor_lang::{ prelude::*, solana_program::program::{invoke, invoke_signed}, @@ -7,13 +11,13 @@ pub fn deposit(accounts: &crate::Deposit, lamports: u64) -> Result<()> { invoke( &spl_stake_pool::instruction::deposit_sol( &spl_stake_pool::ID, - accounts.stake_pool.key, + &accounts.stake_pool.key(), accounts.stake_pool_withdraw_authority.key, accounts.reserve_stake_account.key, accounts.depositor.key, - &accounts.pool_tokens_vault.key(), + &accounts.pool_token_vault.key(), accounts.manager_fee_account.key, - &accounts.pool_tokens_vault.key(), + &accounts.pool_token_vault.key(), &accounts.pool_mint.key(), accounts.token_program.key, lamports, @@ -25,7 +29,7 @@ pub fn deposit(accounts: &crate::Deposit, lamports: u64) -> Result<()> { accounts.reserve_stake_account.to_account_info(), accounts.depositor.to_account_info(), accounts.manager_fee_account.to_account_info(), - accounts.pool_tokens_vault.to_account_info(), + accounts.pool_token_vault.to_account_info(), accounts.pool_mint.to_account_info(), accounts.system_program.to_account_info(), accounts.token_program.to_account_info(), @@ -49,9 +53,9 @@ pub fn deposit_stake(accounts: &crate::DepositStake) -> Result<()> { accounts.stake_owner.key, accounts.validator_stake_account.key, accounts.reserve_stake_account.key, - &accounts.pool_tokens_vault.key(), + &accounts.pool_token_vault.key(), accounts.manager_fee_account.key, - &accounts.pool_tokens_vault.key(), + &accounts.pool_token_vault.key(), &accounts.pool_mint.key(), accounts.token_program.key, ); @@ -81,7 +85,7 @@ pub fn deposit_stake(accounts: &crate::DepositStake) -> Result<()> { accounts.validator_stake_account.to_account_info(), accounts.reserve_stake_account.to_account_info(), accounts.manager_fee_account.to_account_info(), - accounts.pool_tokens_vault.to_account_info(), + accounts.pool_token_vault.to_account_info(), accounts.pool_mint.to_account_info(), accounts.sysvar_clock.to_account_info(), accounts.sysvar_stake_history.to_account_info(), @@ -95,19 +99,15 @@ pub fn deposit_stake(accounts: &crate::DepositStake) -> Result<()> { pub fn withdraw(accounts: &crate::Withdraw, pool_token_lamports: 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, - ][..]; + let seeds = &[state_address.as_ref(), VAULT_AUTHORITY, bump][..]; invoke_signed( &spl_stake_pool::instruction::withdraw_sol( &spl_stake_pool::ID, - accounts.stake_pool.key, + &accounts.stake_pool.key(), accounts.stake_pool_withdraw_authority.key, &accounts.vault_authority.key(), - &accounts.pool_tokens_vault.key(), + &accounts.pool_token_vault.key(), accounts.reserve_stake_account.key, accounts.withdrawer.key, accounts.manager_fee_account.key, @@ -120,7 +120,7 @@ pub fn withdraw(accounts: &crate::Withdraw, pool_token_lamports: u64) -> Result< accounts.stake_pool.to_account_info(), accounts.stake_pool_withdraw_authority.to_account_info(), accounts.vault_authority.to_account_info(), - accounts.pool_tokens_vault.to_account_info(), + accounts.pool_token_vault.to_account_info(), accounts.reserve_stake_account.to_account_info(), accounts.withdrawer.to_account_info(), accounts.manager_fee_account.to_account_info(), @@ -136,43 +136,122 @@ pub fn withdraw(accounts: &crate::Withdraw, pool_token_lamports: u64) -> Result< Ok(()) } -pub fn withdraw_stake(accounts: &crate::WithdrawStake, lamports: u64) -> Result<()> { +/// Accounts required by the WithdrawStake program in the Stake Pool program +pub struct ExtractStakeAccount<'info> { + pub state: Box>, + pub sunrise_state: Box>, + pub stake_pool_program: AccountInfo<'info>, + pub stake_pool: Box>, + pub validator_list_storage: AccountInfo<'info>, + pub stake_pool_withdraw: AccountInfo<'info>, + pub stake_to_split: AccountInfo<'info>, + pub stake_to_receive: AccountInfo<'info>, + pub user_stake_authority: AccountInfo<'info>, + pub user_transfer_authority: AccountInfo<'info>, + pub user_pool_token_account: AccountInfo<'info>, + pub manager_fee_account: AccountInfo<'info>, + pub pool_mint: AccountInfo<'info>, + pub token_program: AccountInfo<'info>, + pub native_stake_program: AccountInfo<'info>, + pub sysvar_clock: AccountInfo<'info>, +} + +impl<'a> From> for ExtractStakeAccount<'a> { + /// Convert the ExtractYield beam instruction accounts to the ExtractStakeAccount accounts + fn from(extract_yield: ExtractYield<'a>) -> Self { + Self { + state: extract_yield.state, + sunrise_state: extract_yield.sunrise_state, + stake_pool_program: extract_yield.spl_stake_pool_program.to_account_info(), + stake_pool: extract_yield.stake_pool, + validator_list_storage: extract_yield.validator_stake_list.to_account_info(), + stake_pool_withdraw: extract_yield + .stake_pool_withdraw_authority + .to_account_info(), + stake_to_split: extract_yield.stake_account_to_split.to_account_info(), + stake_to_receive: extract_yield.new_stake_account.to_account_info(), + user_stake_authority: extract_yield.vault_authority.to_account_info(), + user_transfer_authority: extract_yield.vault_authority.to_account_info(), + user_pool_token_account: extract_yield.pool_token_vault.to_account_info(), + manager_fee_account: extract_yield.manager_fee_account.to_account_info(), + pool_mint: extract_yield.pool_mint.to_account_info(), + token_program: extract_yield.token_program.to_account_info(), + native_stake_program: extract_yield.native_stake_program.to_account_info(), + sysvar_clock: extract_yield.sysvar_clock.to_account_info(), + } + } +} +impl<'a> From<&ExtractYield<'a>> for ExtractStakeAccount<'a> { + fn from(extract_yield: &ExtractYield<'a>) -> Self { + extract_yield.to_owned().into() + } +} +impl<'a> From> for ExtractStakeAccount<'a> { + /// Convert the WithdrawStake beam instruction accounts to the ExtractStakeAccount accounts + fn from(withdraw_stake: WithdrawStake<'a>) -> Self { + Self { + state: withdraw_stake.state, + sunrise_state: withdraw_stake.sunrise_state, + stake_pool_program: withdraw_stake.spl_stake_pool_program.to_account_info(), + stake_pool: withdraw_stake.stake_pool, + validator_list_storage: withdraw_stake.validator_stake_list.to_account_info(), + stake_pool_withdraw: withdraw_stake + .stake_pool_withdraw_authority + .to_account_info(), + stake_to_split: withdraw_stake.stake_account_to_split.to_account_info(), + stake_to_receive: withdraw_stake.new_stake_account.to_account_info(), + user_stake_authority: withdraw_stake.vault_authority.to_account_info(), + user_transfer_authority: withdraw_stake.vault_authority.to_account_info(), + user_pool_token_account: withdraw_stake.pool_token_vault.to_account_info(), + manager_fee_account: withdraw_stake.manager_fee_account.to_account_info(), + pool_mint: withdraw_stake.pool_mint.to_account_info(), + token_program: withdraw_stake.token_program.to_account_info(), + native_stake_program: withdraw_stake.native_stake_program.to_account_info(), + sysvar_clock: withdraw_stake.sysvar_clock.to_account_info(), + } + } +} +impl<'a> From<&WithdrawStake<'a>> for ExtractStakeAccount<'a> { + fn from(withdraw_stake: &WithdrawStake<'a>) -> Self { + withdraw_stake.to_owned().into() + } +} + +pub fn extract_stake(accounts: &ExtractStakeAccount, lamports: 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, - ][..]; + let seeds = &[state_address.as_ref(), VAULT_AUTHORITY, bump][..]; - let pool_tokens = crate::utils::pool_tokens_from_lamports(&accounts.stake_pool, lamports)?; + let pool = &accounts.stake_pool; + let pool_tokens = + crate::utils::pool_tokens_from_lamports(&pool.clone().into_inner(), lamports)?; invoke_signed( &spl_stake_pool::instruction::withdraw_stake( &spl_stake_pool::ID, - accounts.stake_pool.key, - accounts.validator_stake_list.key, - accounts.stake_pool_withdraw_authority.key, - accounts.stake_account_to_split.key, - accounts.new_stake_account.key, - accounts.withdrawer.key, - &accounts.vault_authority.key(), - &accounts.pool_tokens_vault.key(), + &pool.key(), + accounts.validator_list_storage.key, + accounts.stake_pool_withdraw.key, + accounts.stake_to_split.key, + accounts.stake_to_receive.key, + accounts.user_stake_authority.key, + &accounts.user_transfer_authority.key(), + &accounts.user_pool_token_account.key(), accounts.manager_fee_account.key, &accounts.pool_mint.key(), accounts.token_program.key, pool_tokens, ), &[ - accounts.spl_stake_pool_program.to_account_info(), + accounts.stake_pool_program.clone(), accounts.stake_pool.to_account_info(), - accounts.validator_stake_list.to_account_info(), - accounts.stake_pool_withdraw_authority.to_account_info(), - accounts.stake_account_to_split.to_account_info(), - accounts.new_stake_account.to_account_info(), - accounts.withdrawer.to_account_info(), - accounts.vault_authority.to_account_info(), - accounts.pool_tokens_vault.to_account_info(), + accounts.validator_list_storage.to_account_info(), + accounts.stake_pool_withdraw.to_account_info(), + accounts.stake_to_split.to_account_info(), + accounts.stake_to_receive.to_account_info(), + accounts.user_stake_authority.to_account_info(), + accounts.user_stake_authority.to_account_info(), + accounts.user_pool_token_account.to_account_info(), accounts.manager_fee_account.to_account_info(), accounts.pool_mint.to_account_info(), accounts.sysvar_clock.to_account_info(), diff --git a/programs/spl-beam/src/cpi_interface/stake_pool.rs b/programs/spl-beam/src/cpi_interface/stake_pool.rs new file mode 100644 index 0000000..bed174d --- /dev/null +++ b/programs/spl-beam/src/cpi_interface/stake_pool.rs @@ -0,0 +1,32 @@ +use crate::cpi_interface::program::SPL_STAKE_POOL_PROGRAM_ID; +use anchor_lang::prelude::borsh::BorshDeserialize; +use anchor_lang::prelude::Pubkey; +use anchor_lang::{AccountDeserialize, AccountSerialize}; +use std::ops::Deref; + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct StakePool(spl_stake_pool::state::StakePool); + +impl AccountDeserialize for StakePool { + fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result { + spl_stake_pool::state::StakePool::deserialize(buf) + .map(StakePool) + .map_err(Into::into) + } +} + +impl AccountSerialize for StakePool {} + +impl anchor_lang::Owner for StakePool { + fn owner() -> Pubkey { + SPL_STAKE_POOL_PROGRAM_ID + } +} + +impl Deref for StakePool { + type Target = spl_stake_pool::state::StakePool; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/programs/spl-beam/src/cpi_interface/sunrise.rs b/programs/spl-beam/src/cpi_interface/sunrise.rs index 3552991..2b226e0 100644 --- a/programs/spl-beam/src/cpi_interface/sunrise.rs +++ b/programs/spl-beam/src/cpi_interface/sunrise.rs @@ -1,5 +1,5 @@ +use crate::seeds::*; use anchor_lang::prelude::*; -// TODO: Use actual CPI crate. use sunrise_core as sunrise_core_cpi; use sunrise_core_cpi::cpi::{ accounts::{BurnGsol, MintGsol}, @@ -16,7 +16,7 @@ pub fn mint_gsol<'a>( ) -> Result<()> { let accounts: MintGsol<'a> = accounts.into(); let seeds = [ - crate::constants::STATE, + STATE, sunrise_key.as_ref(), stake_pool.as_ref(), &[state_bump], @@ -66,7 +66,7 @@ pub fn burn_gsol<'a>( ) -> Result<()> { let accounts: BurnGsol<'a> = accounts.into(); let seeds = [ - crate::constants::STATE, + STATE, sunrise_key.as_ref(), stake_pool.as_ref(), &[state_bump], diff --git a/programs/spl-beam/src/lib.rs b/programs/spl-beam/src/lib.rs index 3b6687f..b7a9679 100644 --- a/programs/spl-beam/src/lib.rs +++ b/programs/spl-beam/src/lib.rs @@ -5,25 +5,21 @@ use anchor_spl::associated_token::{AssociatedToken, Create}; use anchor_spl::token::{Mint, Token, TokenAccount}; use cpi_interface::spl as spl_interface; use cpi_interface::sunrise as sunrise_interface; +use seeds::*; use state::{State, StateEntry}; use std::ops::Deref; -// TODO: Use actual CPI crate. +use crate::cpi_interface::program::{NativeStakeProgram, SplStakePool}; +use cpi_interface::stake_pool::StakePool; use sunrise_core as sunrise_core_cpi; mod cpi_interface; +mod seeds; mod state; mod utils; declare_id!("EUZfY4LePXSZVMvRuiVzbxazw9yBDYU99DpGJKCthxbS"); -mod constants { - /// Seed of the PDA that can authorize spending from the vault that holds pool tokens. - pub const VAULT_AUTHORITY: &[u8] = b"vault-authority"; - /// Seed of this program's state address. - pub const STATE: &[u8] = b"sunrise-spl"; -} - #[program] pub mod spl_beam { use super::*; @@ -34,7 +30,7 @@ pub mod spl_beam { let cpi_accounts = Create { payer: ctx.accounts.payer.to_account_info(), authority: ctx.accounts.vault_authority.to_account_info(), - associated_token: ctx.accounts.pool_tokens_vault.to_account_info(), + associated_token: ctx.accounts.pool_token_vault.to_account_info(), mint: ctx.accounts.pool_mint.to_account_info(), system_program: ctx.accounts.system_program.to_account_info(), token_program: ctx.accounts.token_program.to_account_info(), @@ -59,7 +55,7 @@ pub mod spl_beam { // CPI: Mint GSOL of the same proportion as the lamports deposited to depositor. sunrise_interface::mint_gsol( ctx.accounts.deref(), - ctx.accounts.beam_program.to_account_info(), + ctx.accounts.sunrise_program.to_account_info(), ctx.accounts.sunrise_state.key(), ctx.accounts.stake_pool.key(), state_bump, @@ -85,7 +81,7 @@ pub mod spl_beam { // CPI: Mint Gsol of the same proportion as the stake amount. sunrise_interface::mint_gsol( ctx.accounts.deref(), - ctx.accounts.beam_program.to_account_info(), + ctx.accounts.sunrise_program.to_account_info(), ctx.accounts.sunrise_state.key(), ctx.accounts.stake_pool.key(), state_bump, @@ -103,8 +99,9 @@ pub mod spl_beam { pub fn withdraw(ctx: Context, lamports: u64) -> Result<()> { // Calculate the number of pool tokens needed to be burnt to withdraw `lamports` lamports. + let pool = &ctx.accounts.stake_pool; let pool_tokens_amount = - utils::pool_tokens_from_lamports(&ctx.accounts.stake_pool, lamports)?; + utils::pool_tokens_from_lamports(&pool.clone().into_inner(), lamports)?; // CPI: Withdraw SOL from SPL stake pool. spl_interface::withdraw(ctx.accounts.deref(), pool_tokens_amount)?; @@ -115,7 +112,7 @@ pub mod spl_beam { ctx.accounts.deref(), ctx.accounts.beam_program.to_account_info(), ctx.accounts.sunrise_state.key(), - ctx.accounts.stake_pool.key(), + pool.key(), state_bump, lamports, )?; @@ -132,17 +129,19 @@ pub mod spl_beam { pub fn withdraw_stake(ctx: Context, lamports: u64) -> Result<()> { // Calculate the number of pool tokens needed to be burnt to withdraw `lamports` worth of stake. + let pool = &ctx.accounts.stake_pool; let pool_tokens_amount = - utils::pool_tokens_from_lamports(&ctx.accounts.stake_pool, lamports)?; + utils::pool_tokens_from_lamports(&pool.clone().into_inner(), lamports)?; // CPI: Withdraw SOL from SPL stake pool into a stake account. - spl_interface::withdraw_stake(ctx.accounts.deref(), pool_tokens_amount)?; + let extract_stake_account_accounts = ctx.accounts.deref().into(); + spl_interface::extract_stake(&extract_stake_account_accounts, pool_tokens_amount)?; // CPI: Burn GSOL of the same proportion as the lamports withdrawn. let state_bump = ctx.bumps.state; sunrise_interface::burn_gsol( ctx.accounts.deref(), - ctx.accounts.beam_program.to_account_info(), + ctx.accounts.sunrise_program.to_account_info(), ctx.accounts.sunrise_state.key(), ctx.accounts.stake_pool.key(), state_bump, @@ -168,6 +167,20 @@ pub mod spl_beam { // spl stake pools only support immediate withdrawals. Err(SplBeamError::Unimplemented.into()) } + + pub fn extract_yield(ctx: Context) -> Result<()> { + // Calculate how much yield can be extracted from the pool. + let extractable_yield = utils::calculate_extractable_yield( + &ctx.accounts.sunrise_state, + &ctx.accounts.state, + &ctx.accounts.stake_pool, + &ctx.accounts.pool_token_vault, + )?; + + // CPI: Extract this yield into a new stake account. + let extract_stake_account_accounts = ctx.accounts.deref().into(); + spl_interface::extract_stake(&extract_stake_account_accounts, extractable_yield) + } } #[derive(Accounts)] @@ -179,7 +192,7 @@ pub struct Initialize<'info> { init, space = State::SPACE, payer = payer, - seeds = [constants::STATE, input.sunrise_state.as_ref(), input.stake_pool.as_ref()], + seeds = [STATE, input.sunrise_state.as_ref(), input.stake_pool.as_ref()], bump )] pub state: Account<'info, State>, @@ -187,12 +200,12 @@ pub struct Initialize<'info> { pub pool_mint: UncheckedAccount<'info>, #[account(mut)] /// CHECK: Initialized as token account in handler. - pub pool_tokens_vault: UncheckedAccount<'info>, + pub pool_token_vault: UncheckedAccount<'info>, /// CHECK: PDA authority of the pool tokens. #[account( seeds = [ state.key().as_ref(), - constants::VAULT_AUTHORITY + VAULT_AUTHORITY ], bump = input.vault_authority_bump )] @@ -219,16 +232,16 @@ pub struct Deposit<'info> { mut, has_one = sunrise_state, has_one = stake_pool, - seeds = [constants::STATE, sunrise_state.key().as_ref(), stake_pool.key().as_ref()], + seeds = [STATE, sunrise_state.key().as_ref(), stake_pool.key().as_ref()], bump )] pub state: Box>, + #[account(mut)] - /// CHECK: The registered SPL stake pool. - pub stake_pool: UncheckedAccount<'info>, + pub stake_pool: Box>, + #[account(mut)] - /// CHECK: The main Sunrise beam state. - pub sunrise_state: UncheckedAccount<'info>, + pub sunrise_state: Box>, #[account(mut)] pub depositor: Signer<'info>, @@ -242,11 +255,11 @@ pub struct Deposit<'info> { token::mint = pool_mint, token::authority = vault_authority )] - pub pool_tokens_vault: Box>, + pub pool_token_vault: Box>, #[account( seeds = [ state.key().as_ref(), - constants::VAULT_AUTHORITY + VAULT_AUTHORITY ], bump = state.vault_authority_bump )] @@ -271,12 +284,8 @@ pub struct Deposit<'info> { /// CHECK: Checked by CPI to Sunrise. pub instructions_sysvar: UncheckedAccount<'info>, - #[account(address = sunrise_core_cpi::ID)] - /// CHECK: The Sunrise ProgramID. - pub beam_program: UncheckedAccount<'info>, - #[account(address = spl_stake_pool::ID)] - /// CHECK: The SPL StakePool ProgramID. - pub spl_stake_pool_program: UncheckedAccount<'info>, + pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, + pub spl_stake_pool_program: Program<'info, SplStakePool>, pub system_program: Program<'info, System>, pub token_program: Program<'info, Token>, @@ -288,7 +297,7 @@ pub struct DepositStake<'info> { mut, has_one = sunrise_state, has_one = stake_pool, - seeds = [constants::STATE, sunrise_state.key().as_ref()], + seeds = [STATE, sunrise_state.key().as_ref()], bump )] pub state: Box>, @@ -314,11 +323,11 @@ pub struct DepositStake<'info> { token::mint = pool_mint, token::authority = vault_authority )] - pub pool_tokens_vault: Box>, + pub pool_token_vault: Box>, #[account( seeds = [ state.key().as_ref(), - constants::VAULT_AUTHORITY + VAULT_AUTHORITY ], bump = state.vault_authority_bump )] @@ -342,10 +351,9 @@ pub struct DepositStake<'info> { pub manager_fee_account: UncheckedAccount<'info>, /// CHECK: Checked by CPI to SPL StakePool program. pub sysvar_stake_history: UncheckedAccount<'info>, - /// CHECK: Checked by CPI to SPL StakePool program. - pub sysvar_clock: UncheckedAccount<'info>, - /// CHECK: Checked by CPI to SPL StakePool program. - pub native_stake_program: UncheckedAccount<'info>, + + pub sysvar_clock: Sysvar<'info, Clock>, + pub native_stake_program: Program<'info, NativeStakeProgram>, #[account(mut)] /// Verified in CPI to Sunrise program. @@ -355,12 +363,8 @@ pub struct DepositStake<'info> { /// CHECK: Checked by CPI to Sunrise. pub instructions_sysvar: UncheckedAccount<'info>, - #[account(address = sunrise_core_cpi::ID)] - /// CHECK: The Sunrise ProgramID. - pub beam_program: UncheckedAccount<'info>, - #[account(address = spl_stake_pool::ID)] - /// CHECK: The SPL StakePool ProgramID. - pub spl_stake_pool_program: UncheckedAccount<'info>, + pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, + pub spl_stake_pool_program: Program<'info, SplStakePool>, pub system_program: Program<'info, System>, pub token_program: Program<'info, Token>, @@ -372,16 +376,14 @@ pub struct Withdraw<'info> { mut, has_one = sunrise_state, has_one = stake_pool, - seeds = [constants::STATE, sunrise_state.key().as_ref(), stake_pool.key().as_ref()], + seeds = [STATE, sunrise_state.key().as_ref(), stake_pool.key().as_ref()], bump )] pub state: Box>, #[account(mut)] - /// CHECK: The registered Spl stake pool. - pub stake_pool: UncheckedAccount<'info>, + pub stake_pool: Box>, #[account(mut)] - /// CHECK: The main Sunrise beam state. - pub sunrise_state: UncheckedAccount<'info>, + pub sunrise_state: Box>, #[account(mut)] pub withdrawer: Signer<'info>, @@ -395,11 +397,11 @@ pub struct Withdraw<'info> { token::mint = pool_mint, token::authority = vault_authority )] - pub pool_tokens_vault: Box>, + pub pool_token_vault: Box>, #[account( seeds = [ state.key().as_ref(), - constants::VAULT_AUTHORITY + VAULT_AUTHORITY ], bump = state.vault_authority_bump )] @@ -445,16 +447,16 @@ pub struct WithdrawStake<'info> { mut, has_one = sunrise_state, has_one = stake_pool, - seeds = [constants::STATE, sunrise_state.key().as_ref()], + seeds = [STATE, sunrise_state.key().as_ref()], bump )] pub state: Box>, #[account(mut)] /// CHECK: The registered spl stake pool. - pub stake_pool: UncheckedAccount<'info>, + pub stake_pool: Box>, #[account(mut)] /// CHECK: The main Sunrise beam state. - pub sunrise_state: UncheckedAccount<'info>, + pub sunrise_state: Box>, #[account(mut)] pub withdrawer: Signer<'info>, @@ -470,11 +472,11 @@ pub struct WithdrawStake<'info> { token::mint = pool_mint, token::authority = vault_authority )] - pub pool_tokens_vault: Box>, + pub pool_token_vault: Box>, #[account( seeds = [ state.key().as_ref(), - constants::VAULT_AUTHORITY + VAULT_AUTHORITY ], bump = state.vault_authority_bump )] @@ -498,10 +500,9 @@ pub struct WithdrawStake<'info> { pub manager_fee_account: UncheckedAccount<'info>, /// CHECK: Checked by CPI to SPL StakePool Program. pub sysvar_stake_history: UncheckedAccount<'info>, - /// CHECK: Checked by CPI to SPL StakePool Program. - pub sysvar_clock: UncheckedAccount<'info>, - /// CHECK: Checked by CPI to SPL StakePool Program. - pub native_stake_program: UncheckedAccount<'info>, + + pub sysvar_clock: Sysvar<'info, Clock>, + pub native_stake_program: Program<'info, NativeStakeProgram>, #[account(mut)] /// Verified in CPI to Sunrise program. @@ -509,12 +510,85 @@ pub struct WithdrawStake<'info> { /// CHECK: Checked by CPI to Sunrise. pub instructions_sysvar: UncheckedAccount<'info>, - #[account(address = sunrise_core_cpi::ID)] - /// CHECK: The Sunrise ProgramID. - pub beam_program: UncheckedAccount<'info>, - #[account(address = spl_stake_pool::ID)] - /// CHECK: The SPL StakePool ProgramID. - pub spl_stake_pool_program: UncheckedAccount<'info>, + pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, + pub spl_stake_pool_program: Program<'info, SplStakePool>, + + pub system_program: Program<'info, System>, + pub token_program: Program<'info, Token>, +} + +#[derive(Accounts)] +pub struct ExtractYield<'info> { + #[account( + has_one = stake_pool, + has_one = sunrise_state + )] + pub state: Box>, + #[account( + has_one = yield_account + )] + pub sunrise_state: Box>, + #[account(mut)] + pub stake_pool: Box>, + + #[account(mut)] + pub payer: Signer<'info>, + + #[account(mut)] + pub pool_mint: Box>, + + #[account(mut)] + /// CHECK: Matches the yield key stored in the state. + pub yield_account: UncheckedAccount<'info>, + + #[account( + seeds = [ + state.key().as_ref(), + EXTRACT_YIELD_STAKE_ACCOUNT + ], + bump + )] + /// CHECK: The uninitialized new stake account. + pub new_stake_account: UncheckedAccount<'info>, + + #[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( + mut, + token::mint = pool_mint, + token::authority = vault_authority + )] + pub pool_token_vault: Box>, + + /// CHECK: Checked by CPI to SPL StakePool Program. + pub stake_pool_withdraw_authority: UncheckedAccount<'info>, + + /// CHECK: Checked by CPI to SPL StakePool program. + pub validator_stake_list: UncheckedAccount<'info>, + #[account(mut)] + // The SPL StakePool program checks that this is either + // the stake account of a recognized validator, or the + // pool's reserve stake account. + /// CHECK: The stake account to split from. - Checked by CPI to SPL StakePool Program. + pub stake_account_to_split: UncheckedAccount<'info>, + /// CHECK: Checked by CPI to SPL StakePool Program. + #[account(mut)] + /// CHECK: Checked by CPI to SPL StakePool Program. + pub manager_fee_account: UncheckedAccount<'info>, + + pub sysvar_clock: Sysvar<'info, Clock>, + pub native_stake_program: Program<'info, NativeStakeProgram>, + + pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, + pub spl_stake_pool_program: Program<'info, SplStakePool>, pub system_program: Program<'info, System>, pub token_program: Program<'info, Token>, diff --git a/programs/spl-beam/src/seeds.rs b/programs/spl-beam/src/seeds.rs new file mode 100644 index 0000000..496c248 --- /dev/null +++ b/programs/spl-beam/src/seeds.rs @@ -0,0 +1,6 @@ +/// Seed of the PDA that can authorize spending from the vault that holds pool tokens. +pub const VAULT_AUTHORITY: &[u8] = b"vault_authority"; +/// Seed of this program's state address. +pub const STATE: &[u8] = b"sunrise_spl"; + +pub const EXTRACT_YIELD_STAKE_ACCOUNT: &[u8] = b"extract_yield_stake_account"; diff --git a/programs/spl-beam/src/state.rs b/programs/spl-beam/src/state.rs index e1ceb01..65c98f4 100644 --- a/programs/spl-beam/src/state.rs +++ b/programs/spl-beam/src/state.rs @@ -15,9 +15,6 @@ pub struct State { /// that holds pool tokens. pub vault_authority_bump: u8, - /// This state's SOL vault. - pub treasury: Pubkey, - /// The amount of the current gsol supply this beam is responsible for. /// This field is also tracked in the matching beam-details struct in the /// sunrise program's state and is expected to match that value. @@ -32,7 +29,6 @@ impl State { 32 + /*spl_state*/ 32 + /*sunrise_state*/ 1 + /*vault_authority_bump*/ - 32 + /*treasury*/ 8; /*partial_gsol_supply*/ } @@ -45,7 +41,6 @@ pub struct StateEntry { pub stake_pool: Pubkey, pub sunrise_state: Pubkey, pub vault_authority_bump: u8, - pub treasury: Pubkey, } impl From for State { @@ -55,7 +50,6 @@ impl From for State { stake_pool: se.stake_pool, sunrise_state: se.sunrise_state, vault_authority_bump: se.vault_authority_bump, - treasury: se.treasury, partial_gsol_supply: 0, } } diff --git a/programs/spl-beam/src/utils.rs b/programs/spl-beam/src/utils.rs index 4ab3d4b..6f4dae6 100644 --- a/programs/spl-beam/src/utils.rs +++ b/programs/spl-beam/src/utils.rs @@ -1,8 +1,11 @@ +use crate::cpi_interface::stake_pool::StakePool; +use crate::state::State; use anchor_lang::prelude::*; use anchor_lang::solana_program::{ borsh0_10::try_from_slice_unchecked, stake::state::StakeStateV2, }; -use spl_stake_pool::state::StakePool; +use anchor_spl::token::TokenAccount; +use sunrise_core::BeamError; /// calculate amount*numerator/denominator /// as value = shares * share_price where share_price=total_value/total_shares @@ -16,8 +19,7 @@ pub fn proportional(amount: u64, numerator: u64, denominator: u64) -> Result Result { - let stake_pool = try_from_slice_unchecked::(&stake_pool.data.borrow())?; +pub fn pool_tokens_from_lamports(stake_pool: &StakePool, lamports: u64) -> Result { let token_supply = stake_pool.pool_token_supply; let total_lamports = stake_pool.total_lamports; @@ -33,3 +35,27 @@ pub fn get_delegated_stake_amount(stake_account: &AccountInfo) -> Result { None => Err(crate::SplBeamError::NotDelegated.into()), } } + +/// Calculates the amount of yield that can be extracted from this pool. +/// This is calculated as: +/// The value of the pool tokens minus the amount of SOL staked in the beam +pub fn calculate_extractable_yield( + sunrise_state: &Account, + beam_state: &Account, + stake_pool: &Account, + pool_token_vault: &Account, +) -> Result { + // Calculate the beam's ownership of the stake pool state + let total_lamports = stake_pool.total_lamports; + let token_supply = stake_pool.pool_token_supply; + let balance = pool_token_vault.amount; + let owned_pool_value = proportional(balance, token_supply, total_lamports)?; + + // Calculate the amount of SOL staked in the beam + let details = sunrise_state + .get_beam_details(&beam_state.key()) + .ok_or(BeamError::UnidentifiedBeam)?; + let staked_sol = details.partial_gsol_supply; + + Ok(owned_pool_value.saturating_sub(staked_sol)) +} diff --git a/programs/sunrise-core/src/state.rs b/programs/sunrise-core/src/state.rs index 1b604ec..60269a8 100644 --- a/programs/sunrise-core/src/state.rs +++ b/programs/sunrise-core/src/state.rs @@ -3,7 +3,7 @@ use anchor_lang::prelude::*; /// The state for the Sunrise beam controller program. #[account] -#[derive(Debug)] +#[derive(Debug, Default)] pub struct State { /// Update authority for this state. pub update_authority: Pubkey, @@ -266,8 +266,6 @@ mod internal_tests { fn test_contains_beam() { let mut state = State::default(); - let beam_program_id = Pubkey::new_unique(); - let key1 = Pubkey::new_unique(); let key2 = Pubkey::new_unique(); state.allocations = vec![key1, key2] @@ -282,8 +280,6 @@ mod internal_tests { fn test_beam_count() { let mut state = State::default(); - let beam_program_id = Pubkey::new_unique(); - let key1 = Pubkey::new_unique(); let key2 = Pubkey::new_unique(); @@ -339,7 +335,6 @@ mod internal_tests { #[test] fn test_remove_beam() { let mut state = State::default(); - let beam_program_id = Pubkey::new_unique(); let keys = vec![ Pubkey::new_unique(), @@ -378,7 +373,6 @@ mod internal_tests { #[test] fn test_get_beam_details() { let mut state = State::default(); - let beam_program_id = Pubkey::new_unique(); let key = Pubkey::new_unique(); state.allocations = vec![ From 585947eed75379eb206ccf5a14f0b7c4cc81e6e8 Mon Sep 17 00:00:00 2001 From: dankelleher Date: Mon, 8 Jan 2024 12:07:00 +0100 Subject: [PATCH 03/10] SPL Stake Pool extract yield tests --- Cargo.lock | 141 ++++++++++----- programs/spl-beam/Cargo.toml | 4 + .../spl-beam/src/cpi_interface/stake_pool.rs | 26 +++ programs/spl-beam/src/state.rs | 1 + programs/spl-beam/src/utils.rs | 161 +++++++++++++++++- programs/sunrise-core/Cargo.toml | 2 +- programs/sunrise-core/src/lib.rs | 3 +- programs/sunrise-core/src/state.rs | 2 + 8 files changed, 285 insertions(+), 55 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3d542da..26fa071 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -572,7 +572,7 @@ checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -862,7 +862,7 @@ checksum = "965ab7eb5f8f97d2a083c799f3a1b994fc397b2fe2da5d1da1626ce15a39f2b1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -1235,7 +1235,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.10.0", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -1257,7 +1257,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core 0.20.3", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -1375,7 +1375,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -1398,7 +1398,7 @@ checksum = "a6cbae11b3de8fce2a456e8ea3dada226b35fe791f0dc1d360c0941f0bb681f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -1492,7 +1492,7 @@ checksum = "eecf8589574ce9b895052fa12d69af7a233f99e6107f5cb8dd1044f2a17bfdcb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -1505,7 +1505,7 @@ dependencies = [ "num-traits", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -1657,7 +1657,7 @@ checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -1672,6 +1672,12 @@ version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" + [[package]] name = "futures-util" version = "0.3.29" @@ -1743,6 +1749,12 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "goblin" version = "0.5.4" @@ -2469,7 +2481,7 @@ checksum = "cfb77679af88f8b125209d354a202862602672222e7f2313fdd6dc349bad4712" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -2551,7 +2563,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -2563,7 +2575,7 @@ dependencies = [ "proc-macro-crate 2.0.0", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -2592,9 +2604,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opaque-debug" @@ -2744,7 +2756,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -2866,9 +2878,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.70" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" dependencies = [ "unicode-ident", ] @@ -2890,7 +2902,7 @@ checksum = "9e2e25ee72f5b24d773cae88422baddefff7714f97aab68d96fe2b6fc4a28fb2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -2943,9 +2955,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -3109,6 +3121,12 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "relative-path" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e898588f33fdd5b9420719948f9f2a32c922a246964576f71ba7f24f80610fbc" + [[package]] name = "reqwest" version = "0.11.22" @@ -3191,6 +3209,35 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rstest" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97eeab2f3c0a199bc4be135c36c924b6590b88c377d416494288c14f2db30199" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d428f8247852f894ee1be110b375111b586d4fa431f6c46e64ba5a0dcccbe605" +dependencies = [ + "cfg-if", + "glob", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.48", + "unicode-ident", +] + [[package]] name = "rtoolbox" version = "0.0.2" @@ -3340,7 +3387,7 @@ checksum = "1db149f81d46d2deba7cd3c50772474707729550221e69588478ebf9ada425ae" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -3408,7 +3455,7 @@ checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -3453,7 +3500,7 @@ dependencies = [ "darling 0.20.3", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -3942,7 +3989,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -4439,7 +4486,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -4761,6 +4808,8 @@ version = "0.1.0" dependencies = [ "anchor-lang", "anchor-spl", + "once_cell", + "rstest", "spl-stake-pool", "sunrise-core", ] @@ -4794,7 +4843,7 @@ checksum = "fadbefec4f3c678215ca72bd71862697bb06b41fd77c0088902dd3203354387b" dependencies = [ "quote", "spl-discriminator-syn 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -4804,7 +4853,7 @@ source = "git+https://github.com/solana-labs/solana-program-library#177a6c94d751 dependencies = [ "quote", "spl-discriminator-syn 0.1.1 (git+https://github.com/solana-labs/solana-program-library)", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -4816,7 +4865,7 @@ dependencies = [ "proc-macro2", "quote", "sha2 0.10.8", - "syn 2.0.39", + "syn 2.0.48", "thiserror", ] @@ -4828,7 +4877,7 @@ dependencies = [ "proc-macro2", "quote", "sha2 0.10.8", - "syn 2.0.39", + "syn 2.0.48", "thiserror", ] @@ -4921,7 +4970,7 @@ dependencies = [ "proc-macro2", "quote", "sha2 0.10.8", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -4932,7 +4981,7 @@ dependencies = [ "proc-macro2", "quote", "sha2 0.10.8", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -5227,9 +5276,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.39" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -5355,7 +5404,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -5366,7 +5415,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", "test-case-core", ] @@ -5387,22 +5436,22 @@ checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" [[package]] name = "thiserror" -version = "1.0.50" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.50" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -5505,7 +5554,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -5652,7 +5701,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -5907,7 +5956,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", "wasm-bindgen-shared", ] @@ -5941,7 +5990,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -6221,7 +6270,7 @@ checksum = "dd7e48ccf166952882ca8bd778a43502c64f33bf94c12ebe2a7f08e5a0f6689f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -6241,7 +6290,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] diff --git a/programs/spl-beam/Cargo.toml b/programs/spl-beam/Cargo.toml index 4f468aa..2210f9f 100644 --- a/programs/spl-beam/Cargo.toml +++ b/programs/spl-beam/Cargo.toml @@ -20,3 +20,7 @@ anchor-lang = '0.29.0' anchor-spl = '0.29.0' spl-stake-pool = { git = "https://github.com/solana-labs/solana-program-library", features = ["no-entrypoint"] } sunrise-core = { path = "../sunrise-core", features = ["cpi"] } +once_cell = "1.19.0" + +[dev-dependencies] +rstest = "0.18.2" \ No newline at end of file diff --git a/programs/spl-beam/src/cpi_interface/stake_pool.rs b/programs/spl-beam/src/cpi_interface/stake_pool.rs index bed174d..05fec22 100644 --- a/programs/spl-beam/src/cpi_interface/stake_pool.rs +++ b/programs/spl-beam/src/cpi_interface/stake_pool.rs @@ -30,3 +30,29 @@ impl Deref for StakePool { &self.0 } } + +#[cfg(test)] +mod tests { + use anchor_lang::__private::base64; + use anchor_lang::Owner; + use super::*; + + // This is a stake pool account - see packages/tests/fixtures/spl/pool.json + const BASE64_POOL_DATA: &str = "AQi2aQPmj/kyc1PszrLaqtyAYSpJobj5d6Ix+gjkmqjkCLZpA+aP+TJzU+zOstqq3IBhKkmhuPl3ojH6COSaqOR0TlK3ODVp7q8xpWvvF7+QNZz/+Qxc/JYj8YXrjzH2C/wIg0ukdM5I0b2+7xzv1QkIWnhD3KHOW51k82GiSUI9HwiQKTXm+75i8/+yT5wnONmvFKIUMPYZ6vcHRhqy8CAcCNLpcPk8ez1QGR5hGs2TqoClRrReyWXhiwWHFVaZyKy+io330TRBlc2b9EScxWN+/9wvGEkjPl5KMq8RBlm3bgbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCp1QsvlBIAAADIq5cMEgAAALoBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAECcAAAAAAAD0AQAAAAAAAAAAABAnAAAAAAAACAAAAAAAAAAQJwAAAAAAAAoAAAAAAAAAAGQAECcAAAAAAAAIAAAAAAAAAGQAECcAAAAAAAADAAAAAAAAAADIq5cMEgAAANULL5QSAAAAAwAAAAAAAAEAAAAAAAAAARAnAAAAAAAAAwAAAAAAAAAJLUF3EAAAANWF8/IQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; + // bSo13r4TkiE4KumL71LsHTPpL2euBYLFx6h9HP3piy1 + const EXPECTED_POOL_MINT: Pubkey = Pubkey::new_from_array([ + 8,210,233,112,249,60,123,61,80,25,30,97,26,205,147,170,128,165,70,180,94,201,101,225,139,5,135,21,86,153,200,172 + ]); + + #[test] + fn test_deserialize_spl_stake_pool() { + let bytes = &base64::decode(BASE64_POOL_DATA).unwrap(); + let stake_pool = StakePool::try_deserialize(&mut &bytes[..]).unwrap(); + assert_eq!(stake_pool.pool_mint, EXPECTED_POOL_MINT); + } + + #[test] + fn test_owner() { + assert_eq!(StakePool::owner(), SPL_STAKE_POOL_PROGRAM_ID); + } +} diff --git a/programs/spl-beam/src/state.rs b/programs/spl-beam/src/state.rs index 65c98f4..e17fd49 100644 --- a/programs/spl-beam/src/state.rs +++ b/programs/spl-beam/src/state.rs @@ -1,6 +1,7 @@ use anchor_lang::prelude::*; #[account] +#[derive(Debug, Default)] pub struct State { /// The update authority of the state. pub update_authority: Pubkey, diff --git a/programs/spl-beam/src/utils.rs b/programs/spl-beam/src/utils.rs index 6f4dae6..f1a5ba3 100644 --- a/programs/spl-beam/src/utils.rs +++ b/programs/spl-beam/src/utils.rs @@ -40,16 +40,18 @@ pub fn get_delegated_stake_amount(stake_account: &AccountInfo) -> Result { /// This is calculated as: /// The value of the pool tokens minus the amount of SOL staked in the beam pub fn calculate_extractable_yield( - sunrise_state: &Account, + sunrise_state: &sunrise_core::State, beam_state: &Account, - stake_pool: &Account, - pool_token_vault: &Account, + stake_pool: &StakePool, + pool_token_vault: &TokenAccount, ) -> Result { // Calculate the beam's ownership of the stake pool state - let total_lamports = stake_pool.total_lamports; - let token_supply = stake_pool.pool_token_supply; - let balance = pool_token_vault.amount; - let owned_pool_value = proportional(balance, token_supply, total_lamports)?; + let total_lamports = stake_pool.total_lamports; // the total number of lamports staked in the pool + let token_supply = stake_pool.pool_token_supply; // the total number of pool tokens in existence + let balance = pool_token_vault.amount; // how many pool tokens the beam owns + let owned_pool_value = proportional(balance, total_lamports, token_supply)?; // the value in lamports of the pool tokens owned by the beam + + msg!("owned_pool_value: {}, total_lamports: {}, token_supply: {}, balance: {}", owned_pool_value, total_lamports, token_supply, balance); // Calculate the amount of SOL staked in the beam let details = sunrise_state @@ -57,5 +59,150 @@ pub fn calculate_extractable_yield( .ok_or(BeamError::UnidentifiedBeam)?; let staked_sol = details.partial_gsol_supply; + msg!("staked_sol: {}", staked_sol); + Ok(owned_pool_value.saturating_sub(staked_sol)) } + +#[cfg(test)] +mod utils_tests { + use std::cell::RefCell; + use std::rc::Rc; + use anchor_lang::__private::base64; + use anchor_lang::solana_program::program_pack::Pack; + use anchor_spl::token::spl_token; + use anchor_spl::token::spl_token::state::AccountState; + use sunrise_core::BeamDetails; + use rstest::rstest; + use super::*; + + static mut LAMPORTS_STORAGE: u64 = 0; + + fn clone_token_account_with_amount(token_account: &TokenAccount, new_amount: u64) -> Result { + let new_spl_account = spl_token::state::Account { + mint: token_account.mint, + owner: token_account.owner, + amount: new_amount, + delegate: token_account.delegate, + state: AccountState::Initialized, + is_native: token_account.is_native, + delegated_amount: token_account.delegated_amount, + close_authority: token_account.close_authority, + }; + + let mut dst = [0u8; spl_token::state::Account::LEN]; + spl_token::state::Account::pack(new_spl_account, &mut dst).unwrap(); + TokenAccount::try_deserialize_unchecked(&mut &dst[..]) + } + + pub fn create_stake_pool() -> StakePool { + // This is a stake pool account - see packages/tests/fixtures/spl/pool.json + const BASE64_POOL_DATA: &str = "AQi2aQPmj/kyc1PszrLaqtyAYSpJobj5d6Ix+gjkmqjkCLZpA+aP+TJzU+zOstqq3IBhKkmhuPl3ojH6COSaqOR0TlK3ODVp7q8xpWvvF7+QNZz/+Qxc/JYj8YXrjzH2C/wIg0ukdM5I0b2+7xzv1QkIWnhD3KHOW51k82GiSUI9HwiQKTXm+75i8/+yT5wnONmvFKIUMPYZ6vcHRhqy8CAcCNLpcPk8ez1QGR5hGs2TqoClRrReyWXhiwWHFVaZyKy+io330TRBlc2b9EScxWN+/9wvGEkjPl5KMq8RBlm3bgbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCp1QsvlBIAAADIq5cMEgAAALoBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAECcAAAAAAAD0AQAAAAAAAAAAABAnAAAAAAAACAAAAAAAAAAQJwAAAAAAAAoAAAAAAAAAAGQAECcAAAAAAAAIAAAAAAAAAGQAECcAAAAAAAADAAAAAAAAAADIq5cMEgAAANULL5QSAAAAAwAAAAAAAAEAAAAAAAAAARAnAAAAAAAAAwAAAAAAAAAJLUF3EAAAANWF8/IQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; + let bytes = &base64::decode(BASE64_POOL_DATA).unwrap(); + StakePool::try_deserialize(&mut &bytes[..]).unwrap() + } + + pub fn create_sunrise_state() -> sunrise_core::State { + const BASE64_SUNRISE_DATA: &str = "2JJrXmhLtrHJi086QhFaVlGIukKID4tkZHDOjVITr4RH7vx5aT2Xqc2hpwNXs05dP6XFw8EiAxG+oWnkP5J1x/Y3KJwMAngLAAAAAAAAAAD//XTkcY2+Bb9ZFikxQyz/hY58xeSkY6ATmfCxiGP0DyhBDwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="; + let bytes = &base64::decode(BASE64_SUNRISE_DATA).unwrap(); + sunrise_core::State::try_deserialize(&mut &bytes[..]).unwrap() + } + + pub fn create_mock_account_info<'info, T: AccountSerialize + AccountDeserialize + Clone>(data: &'info T, owner: &'info Pubkey, key: &'info Pubkey) -> AccountInfo<'info> { + // Serialize T into a byte vector + let mut data_vec = Vec::new(); + data.try_serialize(&mut data_vec).unwrap(); + // let mut data_storage = Box::new(data_vec.clone()); + // Get a mutable reference to the data (with static lifetime so that it can be returned from this function) + // (NOTE: Only do this in test code) + let static_ref: &'static mut [u8] = Box::leak(data_vec.into_boxed_slice()); + + // Create a reference counted, mutable, interior-mutable cell to the data to match the AccountInfo `data` property + let data_ref = Rc::new(RefCell::new(static_ref)); + + // Get a mutable reference to the lamports storage (with a fixed dummy value) + let lamports = unsafe { + Rc::new(RefCell::new(&mut LAMPORTS_STORAGE)) + }; + + // Create the AccountInfo + AccountInfo { + key: &key, + is_signer: false, + is_writable: false, + lamports, + data: data_ref, + owner, + executable: false, + rent_epoch: 0, + } + } + + fn create_and_register_beam_state(sunrise_state: &mut sunrise_core::State, gsol_supply: u64 ) -> Result<(State, Pubkey)> { + let beam_key = Pubkey::new_unique(); + + // add the beam to the core state + let beam_details = BeamDetails { + key: beam_key, + partial_gsol_supply: gsol_supply, + ..Default::default() + }; + sunrise_state + .allocations + .extend(std::iter::repeat(BeamDetails::default()).take(1)); + sunrise_state.add_beam(beam_details)?; + + + let beam_state = State::default(); + + Ok((beam_state, beam_key)) + } + + #[test] + fn test_proportional() { + assert_eq!(proportional(100, 1, 1).unwrap(), 100); + assert_eq!(proportional(100, 1, 2).unwrap(), 50); + assert_eq!(proportional(100, 2, 1).unwrap(), 200); + assert_eq!(proportional(100, 2, 2).unwrap(), 100); + assert_eq!(proportional(100, 0, 1).unwrap(), 0); + assert_eq!(proportional(100, 1, 0).unwrap(), 100); + assert_eq!(proportional(100, 0, 0).unwrap(), 100); + } + + #[test] + fn test_pool_tokens_from_lamports() { + let stake_pool = create_stake_pool(); + assert_eq!(pool_tokens_from_lamports(&stake_pool, 100).unwrap(), 97); + assert_eq!(pool_tokens_from_lamports(&stake_pool, 0).unwrap(), 0); + assert_eq!(pool_tokens_from_lamports(&stake_pool, 1000).unwrap(), 971); + } + + #[rstest] + // total supply is 77520677832, total lamports is 79795522517, + // so the value of one pool token is 79795522517 / 77520677832 = 1.029345 + // 0 lamports are in the beam, so there is no extractable yield + #[case::all_zeroes(0, 0, 0)] + // 48 pool tokens are worth 48 * 1.029345 = 49.4 lamports + // 50 lamports are in the beam, so the extractable yield is 0. + #[case::no_accrued_value(48, 50, 0)] + // 60 pool tokens are worth 60 * 1.029345 = 61.7607 lamports + // 50 lamports are in the beam, so the extractable yield is 61.7607 - 50 = 11.7607, rounded down to 11. + #[case::accrued_value(60, 50, 11)] + fn test_calculate_extractable_yield(#[case] pool_value: u64, #[case] issued_gsol: u64, #[case] expected_extractable_yield: u64) -> Result<()> { + let mut sunrise_state = create_sunrise_state(); + let stake_pool = create_stake_pool(); + + // create a beam and register it against the sunrise state with the given issued_gsol (the amount of sol staked in the beam) + let (beam_state, beam_key) = create_and_register_beam_state(&mut sunrise_state, issued_gsol)?; + let beam_state_account_info = create_mock_account_info(&beam_state, &crate::ID, &beam_key); + let beam_state_account = Account::try_from(&beam_state_account_info)?; + + // create a token account for the stake pool token vault with the given pool_value (the amount of pool tokens owned by the beam) + let pool_token_vault = clone_token_account_with_amount(&TokenAccount::default(), pool_value)?; + + let extractable_yield = calculate_extractable_yield(&sunrise_state, &beam_state_account, &stake_pool, &pool_token_vault).unwrap(); + assert_eq!(extractable_yield, expected_extractable_yield); + + Ok(()) + } +} \ No newline at end of file diff --git a/programs/sunrise-core/Cargo.toml b/programs/sunrise-core/Cargo.toml index 6ed4ebb..a550764 100644 --- a/programs/sunrise-core/Cargo.toml +++ b/programs/sunrise-core/Cargo.toml @@ -25,7 +25,7 @@ anchor-spl = '0.29.0' solana-program-test = { git = "https://github.com/dankelleher/solana.git", branch = "program-test-hack" } #solana-program-test = "1.17.12" solana-sdk = "1.17.12" -thiserror = "1.0.43" +thiserror = "1.0.56" [[test]] name = "sunrise-beam-integration" diff --git a/programs/sunrise-core/src/lib.rs b/programs/sunrise-core/src/lib.rs index bc3e6a6..cdbc165 100644 --- a/programs/sunrise-core/src/lib.rs +++ b/programs/sunrise-core/src/lib.rs @@ -12,7 +12,8 @@ use anchor_lang::solana_program::sysvar; use anchor_spl::token::{Mint, Token, TokenAccount}; use instructions::*; use seeds::*; -pub use state::{AllocationUpdate, EpochReport, RegisterStateInput, State, UpdateStateInput}; + +pub use state::{AllocationUpdate, EpochReport, RegisterStateInput, State, UpdateStateInput, BeamDetails}; declare_id!("suncPB4RR39bMwnRhCym6ZLKqMfnFG83vjzVVuXNhCq"); diff --git a/programs/sunrise-core/src/state.rs b/programs/sunrise-core/src/state.rs index 60269a8..2ce2f83 100644 --- a/programs/sunrise-core/src/state.rs +++ b/programs/sunrise-core/src/state.rs @@ -174,6 +174,8 @@ impl State { /// Get a shared reference to a [BeamDetails] given its key. pub fn get_beam_details(&self, key: &Pubkey) -> Option<&BeamDetails> { + msg!("get_beam_details"); + msg!("{:?}", self.allocations); self.allocations.iter().find(|x| x.key == *key) } From 376d00361e5e0db0e0be869fbd76d44f26c6a6ba Mon Sep 17 00:00:00 2001 From: dankelleher Date: Thu, 18 Jan 2024 11:08:59 +0100 Subject: [PATCH 04/10] SPL Stake Pool extract yield WIP --- README.md | 14 +- packages/sdks/common/src/BeamInterface.ts | 1 + .../sdks/common/src/types/marinade_beam.ts | 200 +++++++++++++++--- .../sdks/common/src/types/marinade_lp_beam.ts | 154 ++++++++++++-- packages/sdks/common/src/types/spl_beam.ts | 160 ++++++++++++-- packages/sdks/marinade-lp/src/index.ts | 64 +++++- packages/sdks/marinade-sp/src/index.ts | 38 +++- packages/sdks/spl/src/index.ts | 70 +++++- packages/sdks/spl/src/utils.ts | 11 + packages/tests/src/analyseReport.ts | 2 +- .../src/functional/beams/marinade-lp.test.ts | 28 +++ .../functional/beams/spl-stake-pool.test.ts | 32 ++- .../src/cpi_interface/sunrise.rs | 14 ++ programs/marinade-beam/src/lib.rs | 139 +++++++----- programs/marinade-beam/src/state.rs | 11 +- programs/marinade-beam/src/system/accounts.rs | 5 +- programs/marinade-beam/src/system/utils.rs | 12 +- .../src/cpi_interface/sunrise.rs | 14 ++ programs/marinade-lp-beam/src/lib.rs | 78 ++++--- programs/marinade-lp-beam/src/state.rs | 11 +- programs/marinade-lp-beam/src/system/utils.rs | 12 +- .../spl-beam/src/cpi_interface/sunrise.rs | 14 ++ programs/spl-beam/src/lib.rs | 88 +++++--- programs/spl-beam/src/state.rs | 11 +- .../src/instructions/burn_gsol.rs | 1 + programs/sunrise-core/src/state.rs | 2 - 26 files changed, 930 insertions(+), 256 deletions(-) diff --git a/README.md b/README.md index 81e7c3f..256a3f0 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,15 @@ cargo update -p indexmap --precise 1.9.3 cargo update -p toml_edit --precise 0.19.8 cargo update -p winnow --precise 0.4.1 cargo update -p toml_datetime --precise 0.6.1 -cargo update -p solana-zk-token-sdk --precise 1.14.17 -cargo update -p solana-program --precise 1.14.17 -cargo update -p solana-sdk --precise 1.14.17 -cargo update -p solana-program-test --precise 1.14.17 - ``` See here for more details: -- https://solana.stackexchange.com/a/6535 \ No newline at end of file +- https://solana.stackexchange.com/a/6535 + +### How Beams Work + +TODO + +#### Burning gSOL + diff --git a/packages/sdks/common/src/BeamInterface.ts b/packages/sdks/common/src/BeamInterface.ts index 6bb7ca9..c1befe8 100644 --- a/packages/sdks/common/src/BeamInterface.ts +++ b/packages/sdks/common/src/BeamInterface.ts @@ -38,6 +38,7 @@ export abstract class BeamInterface< proxyTicket: Keypair; }>; abstract redeemTicket(sunriseTicket: PublicKey): Promise; + abstract burnGSol(lamports: BN): Promise; public supportsSolDeposit(): boolean { return this.caps.find((cap) => canDepositSol(cap)) !== undefined; diff --git a/packages/sdks/common/src/types/marinade_beam.ts b/packages/sdks/common/src/types/marinade_beam.ts index 4bd8c31..3b41d32 100644 --- a/packages/sdks/common/src/types/marinade_beam.ts +++ b/packages/sdks/common/src/types/marinade_beam.ts @@ -162,7 +162,7 @@ export type MarinadeBeam = { "isSigner": false }, { - "name": "beamProgram", + "name": "sunriseProgram", "isMut": false, "isSigner": false }, @@ -281,7 +281,7 @@ export type MarinadeBeam = { "isSigner": false }, { - "name": "beamProgram", + "name": "sunriseProgram", "isMut": false, "isSigner": false }, @@ -390,7 +390,7 @@ export type MarinadeBeam = { "isSigner": false }, { - "name": "beamProgram", + "name": "sunriseProgram", "isMut": false, "isSigner": false }, @@ -484,7 +484,7 @@ export type MarinadeBeam = { "isSigner": true }, { - "name": "beamProgram", + "name": "sunriseProgram", "isMut": false, "isSigner": false }, @@ -521,6 +521,75 @@ export type MarinadeBeam = { } ] }, + { + "name": "burn", + "docs": [ + "Burning is withdrawing without redeeming the pool tokens. The result is a beam that is \"worth more\"", + "than the SOL that has been staked into it, i.e. the pool tokens are more valuable than the SOL.", + "This allows yield extraction and can be seen as a form of \"donation\"." + ], + "accounts": [ + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "sunriseState", + "isMut": true, + "isSigner": false + }, + { + "name": "burner", + "isMut": true, + "isSigner": true + }, + { + "name": "gsolTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "vaultAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "gsolMint", + "isMut": true, + "isSigner": false, + "docs": [ + "Verified in CPI to Sunrise program." + ] + }, + { + "name": "instructionsSysvar", + "isMut": false, + "isSigner": false + }, + { + "name": "sunriseProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "lamports", + "type": "u64" + } + ] + }, { "name": "redeemTicket", "accounts": [ @@ -585,6 +654,11 @@ export type MarinadeBeam = { "isMut": false, "isSigner": false }, + { + "name": "sunriseState", + "isMut": false, + "isSigner": false + }, { "name": "marinadeState", "isMut": false, @@ -646,6 +720,11 @@ export type MarinadeBeam = { "isMut": false, "isSigner": false }, + { + "name": "sunriseState", + "isMut": false, + "isSigner": false + }, { "name": "marinadeState", "isMut": false, @@ -775,15 +854,6 @@ export type MarinadeBeam = { "that holds pool tokens (msol in this case)." ], "type": "u8" - }, - { - "name": "partialGsolSupply", - "docs": [ - "The amount of the current gsol supply this beam is responsible for.", - "This field is also tracked in the matching beam-details struct in the", - "sunrise program's state and is expected to match that value." - ], - "type": "u64" } ] } @@ -840,10 +910,6 @@ export type MarinadeBeam = { "name": "extractedYield", "type": "u64" }, - { - "name": "partialGsolSupply", - "type": "u64" - }, { "name": "bump", "type": "u8" @@ -1076,7 +1142,7 @@ export const IDL: MarinadeBeam = { "isSigner": false }, { - "name": "beamProgram", + "name": "sunriseProgram", "isMut": false, "isSigner": false }, @@ -1195,7 +1261,7 @@ export const IDL: MarinadeBeam = { "isSigner": false }, { - "name": "beamProgram", + "name": "sunriseProgram", "isMut": false, "isSigner": false }, @@ -1304,7 +1370,7 @@ export const IDL: MarinadeBeam = { "isSigner": false }, { - "name": "beamProgram", + "name": "sunriseProgram", "isMut": false, "isSigner": false }, @@ -1398,7 +1464,7 @@ export const IDL: MarinadeBeam = { "isSigner": true }, { - "name": "beamProgram", + "name": "sunriseProgram", "isMut": false, "isSigner": false }, @@ -1435,6 +1501,75 @@ export const IDL: MarinadeBeam = { } ] }, + { + "name": "burn", + "docs": [ + "Burning is withdrawing without redeeming the pool tokens. The result is a beam that is \"worth more\"", + "than the SOL that has been staked into it, i.e. the pool tokens are more valuable than the SOL.", + "This allows yield extraction and can be seen as a form of \"donation\"." + ], + "accounts": [ + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "sunriseState", + "isMut": true, + "isSigner": false + }, + { + "name": "burner", + "isMut": true, + "isSigner": true + }, + { + "name": "gsolTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "vaultAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "gsolMint", + "isMut": true, + "isSigner": false, + "docs": [ + "Verified in CPI to Sunrise program." + ] + }, + { + "name": "instructionsSysvar", + "isMut": false, + "isSigner": false + }, + { + "name": "sunriseProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "lamports", + "type": "u64" + } + ] + }, { "name": "redeemTicket", "accounts": [ @@ -1499,6 +1634,11 @@ export const IDL: MarinadeBeam = { "isMut": false, "isSigner": false }, + { + "name": "sunriseState", + "isMut": false, + "isSigner": false + }, { "name": "marinadeState", "isMut": false, @@ -1560,6 +1700,11 @@ export const IDL: MarinadeBeam = { "isMut": false, "isSigner": false }, + { + "name": "sunriseState", + "isMut": false, + "isSigner": false + }, { "name": "marinadeState", "isMut": false, @@ -1689,15 +1834,6 @@ export const IDL: MarinadeBeam = { "that holds pool tokens (msol in this case)." ], "type": "u8" - }, - { - "name": "partialGsolSupply", - "docs": [ - "The amount of the current gsol supply this beam is responsible for.", - "This field is also tracked in the matching beam-details struct in the", - "sunrise program's state and is expected to match that value." - ], - "type": "u64" } ] } @@ -1754,10 +1890,6 @@ export const IDL: MarinadeBeam = { "name": "extractedYield", "type": "u64" }, - { - "name": "partialGsolSupply", - "type": "u64" - }, { "name": "bump", "type": "u8" diff --git a/packages/sdks/common/src/types/marinade_lp_beam.ts b/packages/sdks/common/src/types/marinade_lp_beam.ts index 4737145..e1459f6 100644 --- a/packages/sdks/common/src/types/marinade_lp_beam.ts +++ b/packages/sdks/common/src/types/marinade_lp_beam.ts @@ -173,7 +173,7 @@ export type MarinadeLpBeam = { "isSigner": false }, { - "name": "beamProgram", + "name": "sunriseProgram", "isMut": false, "isSigner": false }, @@ -282,7 +282,7 @@ export type MarinadeLpBeam = { "isSigner": false }, { - "name": "beamProgram", + "name": "sunriseProgram", "isMut": false, "isSigner": false }, @@ -299,6 +299,70 @@ export type MarinadeLpBeam = { } ] }, + { + "name": "burn", + "docs": [ + "Burning is withdrawing without redeeming the pool tokens. The result is a beam that is \"worth more\"", + "than the SOL that has been staked into it, i.e. the pool tokens are more valuable than the SOL.", + "This allows yield extraction and can be seen as a form of \"donation\"." + ], + "accounts": [ + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "sunriseState", + "isMut": true, + "isSigner": false + }, + { + "name": "burner", + "isMut": true, + "isSigner": true + }, + { + "name": "gsolTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "gsolMint", + "isMut": true, + "isSigner": false, + "docs": [ + "Verified in CPI to Sunrise program." + ] + }, + { + "name": "instructionsSysvar", + "isMut": false, + "isSigner": false + }, + { + "name": "sunriseProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "lamports", + "type": "u64" + } + ] + }, { "name": "orderWithdrawal", "accounts": [], @@ -358,15 +422,6 @@ export type MarinadeLpBeam = { "The token-account that receives msol when withdrawing liquidity." ], "type": "publicKey" - }, - { - "name": "partialGsolSupply", - "docs": [ - "The amount of the current gsol supply this beam is responsible for.", - "This field is also tracked in the matching beam-details struct in the", - "sunrise program's state and is expected to match that value." - ], - "type": "u64" } ] } @@ -595,7 +650,7 @@ export const IDL: MarinadeLpBeam = { "isSigner": false }, { - "name": "beamProgram", + "name": "sunriseProgram", "isMut": false, "isSigner": false }, @@ -704,7 +759,7 @@ export const IDL: MarinadeLpBeam = { "isSigner": false }, { - "name": "beamProgram", + "name": "sunriseProgram", "isMut": false, "isSigner": false }, @@ -721,6 +776,70 @@ export const IDL: MarinadeLpBeam = { } ] }, + { + "name": "burn", + "docs": [ + "Burning is withdrawing without redeeming the pool tokens. The result is a beam that is \"worth more\"", + "than the SOL that has been staked into it, i.e. the pool tokens are more valuable than the SOL.", + "This allows yield extraction and can be seen as a form of \"donation\"." + ], + "accounts": [ + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "sunriseState", + "isMut": true, + "isSigner": false + }, + { + "name": "burner", + "isMut": true, + "isSigner": true + }, + { + "name": "gsolTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "gsolMint", + "isMut": true, + "isSigner": false, + "docs": [ + "Verified in CPI to Sunrise program." + ] + }, + { + "name": "instructionsSysvar", + "isMut": false, + "isSigner": false + }, + { + "name": "sunriseProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "lamports", + "type": "u64" + } + ] + }, { "name": "orderWithdrawal", "accounts": [], @@ -780,15 +899,6 @@ export const IDL: MarinadeLpBeam = { "The token-account that receives msol when withdrawing liquidity." ], "type": "publicKey" - }, - { - "name": "partialGsolSupply", - "docs": [ - "The amount of the current gsol supply this beam is responsible for.", - "This field is also tracked in the matching beam-details struct in the", - "sunrise program's state and is expected to match that value." - ], - "type": "u64" } ] } diff --git a/packages/sdks/common/src/types/spl_beam.ts b/packages/sdks/common/src/types/spl_beam.ts index 24e55c8..2c0eb87 100644 --- a/packages/sdks/common/src/types/spl_beam.ts +++ b/packages/sdks/common/src/types/spl_beam.ts @@ -403,7 +403,7 @@ export type SplBeam = { "isSigner": false }, { - "name": "beamProgram", + "name": "sunriseProgram", "isMut": false, "isSigner": false }, @@ -430,6 +430,75 @@ export type SplBeam = { } ] }, + { + "name": "burn", + "docs": [ + "Burning is withdrawing without redeeming the pool tokens. The result is a beam that is \"worth more\"", + "than the SOL that has been staked into it, i.e. the pool tokens are more valuable than the SOL.", + "This allows yield extraction and can be seen as a form of \"donation\"." + ], + "accounts": [ + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "sunriseState", + "isMut": true, + "isSigner": false + }, + { + "name": "stakePool", + "isMut": false, + "isSigner": false + }, + { + "name": "burner", + "isMut": true, + "isSigner": true + }, + { + "name": "gsolTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "gsolMint", + "isMut": true, + "isSigner": false, + "docs": [ + "Verified in CPI to Sunrise program." + ] + }, + { + "name": "instructionsSysvar", + "isMut": false, + "isSigner": false + }, + { + "name": "sunriseProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "lamports", + "type": "u64" + } + ] + }, { "name": "withdrawStake", "accounts": [ @@ -700,15 +769,6 @@ export type SplBeam = { "that holds pool tokens." ], "type": "u8" - }, - { - "name": "partialGsolSupply", - "docs": [ - "The amount of the current gsol supply this beam is responsible for.", - "This field is also tracked in the matching beam-details struct in the", - "sunrise program's state and is expected to match that value." - ], - "type": "u64" } ] } @@ -1164,7 +1224,7 @@ export const IDL: SplBeam = { "isSigner": false }, { - "name": "beamProgram", + "name": "sunriseProgram", "isMut": false, "isSigner": false }, @@ -1191,6 +1251,75 @@ export const IDL: SplBeam = { } ] }, + { + "name": "burn", + "docs": [ + "Burning is withdrawing without redeeming the pool tokens. The result is a beam that is \"worth more\"", + "than the SOL that has been staked into it, i.e. the pool tokens are more valuable than the SOL.", + "This allows yield extraction and can be seen as a form of \"donation\"." + ], + "accounts": [ + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "sunriseState", + "isMut": true, + "isSigner": false + }, + { + "name": "stakePool", + "isMut": false, + "isSigner": false + }, + { + "name": "burner", + "isMut": true, + "isSigner": true + }, + { + "name": "gsolTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "gsolMint", + "isMut": true, + "isSigner": false, + "docs": [ + "Verified in CPI to Sunrise program." + ] + }, + { + "name": "instructionsSysvar", + "isMut": false, + "isSigner": false + }, + { + "name": "sunriseProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "lamports", + "type": "u64" + } + ] + }, { "name": "withdrawStake", "accounts": [ @@ -1461,15 +1590,6 @@ export const IDL: SplBeam = { "that holds pool tokens." ], "type": "u8" - }, - { - "name": "partialGsolSupply", - "docs": [ - "The amount of the current gsol supply this beam is responsible for.", - "This field is also tracked in the matching beam-details struct in the", - "sunrise program's state and is expected to match that value." - ], - "type": "u64" } ] } diff --git a/packages/sdks/marinade-lp/src/index.ts b/packages/sdks/marinade-lp/src/index.ts index 92459e1..68e955e 100644 --- a/packages/sdks/marinade-lp/src/index.ts +++ b/packages/sdks/marinade-lp/src/index.ts @@ -174,7 +174,7 @@ export class MarinadeLpClient extends BeamInterface< /** Return a transaction to deposit to a marinade liquidity pool. */ public async deposit( - amount: BN, + lamports: BN, recipient?: PublicKey, ): Promise { const depositor = this.provider.publicKey; @@ -190,7 +190,7 @@ export class MarinadeLpClient extends BeamInterface< } const instruction = await this.program.methods - .deposit(amount) + .deposit(lamports) .accounts({ state: this.stateAddress, marinadeState: this.state.proxyState, @@ -210,7 +210,7 @@ export class MarinadeLpClient extends BeamInterface< liqPoolMintAuthority: await this.marinadeLp.marinade.lpMintAuthority(), systemProgram: SystemProgram.programId, tokenProgram: TOKEN_PROGRAM_ID, - beamProgram: this.sunrise.program.programId, + sunriseProgram: this.sunrise.program.programId, marinadeProgram: MARINADE_FINANCE_PROGRAM_ID, }) .instruction(); @@ -226,12 +226,9 @@ export class MarinadeLpClient extends BeamInterface< /** Return a transaction to withdraw from a marinade liquidity-pool. */ public async withdraw( - amount: BN, + lamports: BN, gsolTokenAccount?: PublicKey, ): Promise { - if (!this.sunrise || !this.marinadeLp) { - await this.refresh(); - } const withdrawer = this.provider.publicKey; const { gsolMint, instructionsSysvar, burnGsolFrom } = this.sunrise.burnGsolAccounts( @@ -241,7 +238,7 @@ export class MarinadeLpClient extends BeamInterface< ); const instruction = await this.program.methods - .withdraw(amount) + .withdraw(lamports) .accounts({ state: this.stateAddress, marinadeState: this.state.proxyState, @@ -260,7 +257,7 @@ export class MarinadeLpClient extends BeamInterface< tokenProgram: TOKEN_PROGRAM_ID, gsolMint, instructionsSysvar, - beamProgram: this.sunrise.program.programId, + sunriseProgram: this.sunrise.program.programId, marinadeProgram: MARINADE_FINANCE_PROGRAM_ID, }) .instruction(); @@ -282,6 +279,55 @@ export class MarinadeLpClient extends BeamInterface< ); } + /** + * Return a transaction to burn gsol. This is essentially a "donation" to the sunrise instance + */ + public async burnGSol( + lamports: BN, + gsolTokenAccount?: PublicKey, + ): Promise { + const burner = this.provider.publicKey; + const { gsolMint, instructionsSysvar, burnGsolFrom } = + this.sunrise.burnGsolAccounts( + this.stateAddress, + burner, + gsolTokenAccount, + ); + + const instruction = await this.program.methods + .burn(lamports) + .accounts({ + state: this.stateAddress, + sunriseState: this.state.sunriseState, + burner, + gsolTokenAccount: burnGsolFrom, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + gsolMint, + instructionsSysvar, + sunriseProgram: this.sunrise.program.programId, + }) + .instruction(); + + 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); + // } + /** * Return a transaction to redeem a ticket received from ordering a withdrawal from a marinade-lp. * NOTE: This is not a supported feature for Marinade Lps and will throw an error. diff --git a/packages/sdks/marinade-sp/src/index.ts b/packages/sdks/marinade-sp/src/index.ts index 6906b95..ed7e3ae 100644 --- a/packages/sdks/marinade-sp/src/index.ts +++ b/packages/sdks/marinade-sp/src/index.ts @@ -213,7 +213,7 @@ export class MarinadeClient extends BeamInterface< liqPoolMsolLegAuthority: await this.marinade.state.mSolLegAuthority(), msolMintAuthority: await this.marinade.state.mSolMintAuthority(), reservePda: await this.marinade.state.reserveAddress(), - beamProgram: this.sunrise.program.programId, + sunriseProgram: this.sunrise.program.programId, marinadeProgram: MARINADE_FINANCE_PROGRAM_ID, systemProgram: SystemProgram.programId, tokenProgram: TOKEN_PROGRAM_ID, @@ -252,7 +252,7 @@ export class MarinadeClient extends BeamInterface< liqPoolMsolLeg: this.marinade.state.mSolLeg, treasuryMsolAccount: this.marinade.state.treasuryMsolAccount, instructionsSysvar, - beamProgram: this.sunrise.program.programId, + sunriseProgram: this.sunrise.program.programId, marinadeProgram: MARINADE_FINANCE_PROGRAM_ID, systemProgram: SystemProgram.programId, tokenProgram: TOKEN_PROGRAM_ID, @@ -314,7 +314,7 @@ export class MarinadeClient extends BeamInterface< instructionsSysvar, newTicketAccount: marinadeTicket.publicKey, proxyTicketAccount: sunriseTicket.publicKey, - beamProgram: this.sunrise.program.programId, + sunriseProgram: this.sunrise.program.programId, marinadeProgram: MARINADE_FINANCE_PROGRAM_ID, clock: SYSVAR_CLOCK_PUBKEY, rent: SYSVAR_RENT_PUBKEY, @@ -331,6 +331,36 @@ export class MarinadeClient extends BeamInterface< }; } + public async burnGSol( + lamports: BN, + gsolTokenAccount?: PublicKey, + ): Promise { + const burner = this.provider.publicKey; + const { gsolMint, instructionsSysvar, burnGsolFrom } = + this.sunrise.burnGsolAccounts( + this.stateAddress, + burner, + gsolTokenAccount, + ); + + const instruction = await this.program.methods + .burn(lamports) + .accounts({ + state: this.stateAddress, + sunriseState: this.state.sunriseState, + burner, + gsolTokenAccount: burnGsolFrom, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + gsolMint, + instructionsSysvar, + sunriseProgram: this.sunrise.program.programId, + }) + .instruction(); + + return new Transaction().add(instruction); + } + /** * Return a transaction to redeem a ticket gotten from ordering a withdrawal. */ @@ -413,7 +443,7 @@ export class MarinadeClient extends BeamInterface< await this.marinade.state.validatorDuplicationFlag(voterAddress), msolMintAuthority: await this.marinade.state.mSolMintAuthority(), stakeProgram: StakeProgram.programId, - beamProgram: this.sunrise.program.programId, + sunriseProgram: this.sunrise.program.programId, marinadeProgram: MARINADE_FINANCE_PROGRAM_ID, systemProgram: SystemProgram.programId, tokenProgram: TOKEN_PROGRAM_ID, diff --git a/packages/sdks/spl/src/index.ts b/packages/sdks/spl/src/index.ts index cd4254f..b3a609d 100644 --- a/packages/sdks/spl/src/index.ts +++ b/packages/sdks/spl/src/index.ts @@ -262,7 +262,7 @@ export class SplClient extends BeamInterface { nativeStakeProgram: StakeProgram.programId, gsolMint, instructionsSysvar, - beamProgram: this.sunrise.program.programId, + sunriseProgram: this.sunrise.program.programId, splStakePoolProgram: SPL_STAKE_POOL_PROGRAM_ID, systemProgram: SystemProgram.programId, tokenProgram: TOKEN_PROGRAM_ID, @@ -395,6 +395,74 @@ export class SplClient extends BeamInterface { ); // TODO } + public async burnGSol( + lamports: BN, + gsolTokenAccount?: PublicKey, + ): Promise { + const burner = this.provider.publicKey; + const { gsolMint, instructionsSysvar, burnGsolFrom } = + this.sunrise.burnGsolAccounts( + this.stateAddress, + burner, + gsolTokenAccount, + ); + + const instruction = await this.program.methods + .burn(lamports) + .accounts({ + state: this.stateAddress, + sunriseState: this.state.sunriseState, + stakePool: this.spl.stakePoolAddress, + burner, + gsolTokenAccount: burnGsolFrom, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + gsolMint, + instructionsSysvar, + sunriseProgram: this.sunrise.program.programId, + }) + .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 [newStakeAccount] = Utils.deriveExtractYieldStakeAccount( + this.program.programId, + this.stateAddress, + ); + + const instruction = await this.program.methods + .extractYield() + .accounts({ + state: this.stateAddress, + stakePool: this.spl.stakePoolAddress, + sunriseState: this.state.sunriseState, + poolMint: this.spl.stakePoolState.poolMint, + yieldAccount: this.sunrise.state.yieldAccount, + newStakeAccount, + vaultAuthority: this.vaultAuthority[0], + poolTokenVault: this.spl.beamVault, + stakePoolWithdrawAuthority: this.spl.withdrawAuthority, + validatorStakeList: this.spl.stakePoolState.validatorList, + stakeAccountToSplit: this.spl.stakePoolState.reserveStake, + managerFeeAccount: this.spl.stakePoolState.managerFeeAccount, + sysvarClock: SYSVAR_CLOCK_PUBKEY, + nativeStakeProgram: StakeProgram.programId, + // instructionsSysvar, + sunriseProgram: this.sunrise.program.programId, + splStakePoolProgram: SPL_STAKE_POOL_PROGRAM_ID, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + }) + .instruction(); + + return new Transaction().add(instruction); + } + /** * Return a transaction to redeem a ticket received from ordering a withdrawal. * This is not a supported feature for SPL beams and will throw an error. diff --git a/packages/sdks/spl/src/utils.ts b/packages/sdks/spl/src/utils.ts index 125460b..4ccdb5e 100644 --- a/packages/sdks/spl/src/utils.ts +++ b/packages/sdks/spl/src/utils.ts @@ -23,6 +23,7 @@ export type SplClientParams = { const enum Seeds { STATE = "sunrise_spl", VAULT_AUTHORITY = "vault_authority", + EXTRACT_YIELD_STAKE_ACCOUNT = "extract_yield_stake_account", } /** @@ -52,6 +53,16 @@ export class Utils { ); } + public static deriveExtractYieldStakeAccount( + pid: PublicKey, + state: PublicKey, + ): [PublicKey, number] { + return PublicKey.findProgramAddressSync( + [state.toBuffer(), Buffer.from(Seeds.EXTRACT_YIELD_STAKE_ACCOUNT)], + pid, + ); + } + public static getSplClientParams = async ( provider: AnchorProvider, beamProgramId: PublicKey, diff --git a/packages/tests/src/analyseReport.ts b/packages/tests/src/analyseReport.ts index e5e92c1..4c1f783 100644 --- a/packages/tests/src/analyseReport.ts +++ b/packages/tests/src/analyseReport.ts @@ -10,7 +10,7 @@ let passedTests = 0; let failedTests = 0; report.results.forEach( - (suite: { suites: { tests: { state: string }[] }[] }) => { + (suite: { suites: { tests: { state: string | null }[] }[] }) => { suite.suites.forEach((testSuite) => { testSuite.tests.forEach((test) => { totalTests++; diff --git a/packages/tests/src/functional/beams/marinade-lp.test.ts b/packages/tests/src/functional/beams/marinade-lp.test.ts index a78950f..220a179 100644 --- a/packages/tests/src/functional/beams/marinade-lp.test.ts +++ b/packages/tests/src/functional/beams/marinade-lp.test.ts @@ -221,4 +221,32 @@ describe("Marinade liquidity pool beam", () => { expectedLPTokens, ); }); + + it("can burn gsol", async () => { + // burn some gsol to simulate the creation of yield + const burnAmount = new BN(1 * LAMPORTS_PER_SOL); + await sendAndConfirmTransaction( + stakerIdentity, + await beamClient.burnGSol(burnAmount), + ); + + const expectedGsol = stakerGsolBalance.sub(burnAmount); + + await expectTokenBalance( + beamClient.provider, + beamClient.sunrise.gsolAssociatedTokenAccount(), + expectedGsol, + ); + }); + + // 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(), + // ) + // }); }); 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 c5c12d4..8a35a66 100644 --- a/packages/tests/src/functional/beams/spl-stake-pool.test.ts +++ b/packages/tests/src/functional/beams/spl-stake-pool.test.ts @@ -28,7 +28,7 @@ describe("SPL stake pool beam", () => { const depositAmount = 10 * LAMPORTS_PER_SOL; const failedDepositAmount = 5 * LAMPORTS_PER_SOL; - const withdrawalAmount = 10 * LAMPORTS_PER_SOL; + const withdrawalAmount = 5 * LAMPORTS_PER_SOL; before("Set up the sunrise state", async () => { coreClient = await registerSunriseState(); @@ -182,5 +182,35 @@ describe("SPL stake pool beam", () => { ); stakerGsolBalance = expectedGsol; vaultStakePoolSolBalance = expectedBsol; + + console.log("Remaining gSOL: " + stakerGsolBalance.toString()); + }); + + it("can burn gsol", async () => { + // burn some gsol to simulate the creation of yield + const burnAmount = new BN(1 * LAMPORTS_PER_SOL); + await sendAndConfirmTransaction( + stakerIdentity, + await beamClient.burnGSol(burnAmount), + ); + + const expectedGsol = stakerGsolBalance.sub(burnAmount); + + await expectTokenBalance( + beamClient.provider, + beamClient.sunrise.gsolAssociatedTokenAccount(), + expectedGsol, + ); + }); + + 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(), + ); }); }); diff --git a/programs/marinade-beam/src/cpi_interface/sunrise.rs b/programs/marinade-beam/src/cpi_interface/sunrise.rs index 116a753..4035346 100644 --- a/programs/marinade-beam/src/cpi_interface/sunrise.rs +++ b/programs/marinade-beam/src/cpi_interface/sunrise.rs @@ -95,3 +95,17 @@ impl<'a> From<&crate::OrderWithdrawal<'a>> for BurnGsol<'a> { } } } + +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.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(), + } + } +} \ No newline at end of file diff --git a/programs/marinade-beam/src/lib.rs b/programs/marinade-beam/src/lib.rs index 08a11ff..d430027 100644 --- a/programs/marinade-beam/src/lib.rs +++ b/programs/marinade-beam/src/lib.rs @@ -57,10 +57,7 @@ pub mod marinade_beam { } pub fn update(ctx: Context, update_input: StateEntry) -> Result<()> { - let mut updated_state: State = update_input.into(); - // Make sure the partial gsol supply remains consistent. - updated_state.partial_gsol_supply = ctx.accounts.state.partial_gsol_supply; - ctx.accounts.state.set_inner(updated_state); + ctx.accounts.state.set_inner(update_input.into()); Ok(()) } @@ -73,19 +70,12 @@ pub mod marinade_beam { // CPI: Mint GSOL of the same proportion as the lamports deposited to the depositor. sunrise_interface::mint_gsol( ctx.accounts.deref(), - ctx.accounts.beam_program.to_account_info(), + ctx.accounts.sunrise_program.to_account_info(), ctx.accounts.sunrise_state.key(), bump, lamports, )?; - // Update the partial gsol supply for this beam. - let state_account = &mut ctx.accounts.state; - state_account.partial_gsol_supply = state_account - .partial_gsol_supply - .checked_add(lamports) - .unwrap(); - Ok(()) } @@ -100,19 +90,12 @@ pub mod marinade_beam { // CPI: Mint GSOL of the same proportion as the stake amount to the depositor. sunrise_interface::mint_gsol( ctx.accounts.deref(), - ctx.accounts.beam_program.to_account_info(), + ctx.accounts.sunrise_program.to_account_info(), ctx.accounts.sunrise_state.key(), bump, lamports, )?; - // Update the partial gsol supply for this beam. - let state_account = &mut ctx.accounts.state; - state_account.partial_gsol_supply = state_account - .partial_gsol_supply - .checked_add(lamports) - .unwrap(); - Ok(()) } @@ -128,19 +111,12 @@ pub mod marinade_beam { // CPI: Burn GSOL of the same proportion as the number of lamports withdrawn. sunrise_interface::burn_gsol( ctx.accounts.deref(), - ctx.accounts.beam_program.to_account_info(), + ctx.accounts.sunrise_program.to_account_info(), ctx.accounts.sunrise_state.key(), bump, lamports, )?; - // Update the partial gsol supply for this beam. - let state_account = &mut ctx.accounts.state; - state_account.partial_gsol_supply = state_account - .partial_gsol_supply - .checked_sub(lamports) - .unwrap(); - Ok(()) } @@ -161,18 +137,27 @@ pub mod marinade_beam { // CPI: Burn GSOL of the same proportion as the number of lamports withdrawn. sunrise_interface::burn_gsol( ctx.accounts.deref(), - ctx.accounts.beam_program.to_account_info(), + ctx.accounts.sunrise_program.to_account_info(), ctx.accounts.sunrise_state.key(), bump, lamports, )?; - // Update the partial gsol supply for this beam. - let state_account = &mut ctx.accounts.state; - state_account.partial_gsol_supply = state_account - .partial_gsol_supply - .checked_sub(lamports) - .unwrap(); + Ok(()) + } + + /// Burning is withdrawing without redeeming the pool tokens. The result is a beam that is "worth more" + /// than the SOL that has been staked into it, i.e. the pool tokens are more valuable than the SOL. + /// This allows yield extraction and can be seen as a form of "donation". + pub fn burn(ctx: Context, lamports: u64) -> Result<()> { + let state_bump = ctx.bumps.state; + sunrise_interface::burn_gsol( + ctx.accounts.deref(), + ctx.accounts.sunrise_program.to_account_info(), + ctx.accounts.sunrise_state.key(), + state_bump, + lamports, + )?; Ok(()) } @@ -205,7 +190,8 @@ pub mod marinade_beam { } pub fn init_epoch_report(ctx: Context, extracted_yield: u64) -> Result<()> { - let extractable_yield = utils::extractable_yield( + let extractable_yield = utils::calculate_extractable_yield( + &ctx.accounts.sunrise_state, &ctx.accounts.state, &ctx.accounts.marinade_state, &ctx.accounts.msol_vault, @@ -217,7 +203,6 @@ pub mod marinade_beam { total_ordered_lamports: 0, extractable_yield, extracted_yield: 0, // modified below with remarks - partial_gsol_supply: ctx.accounts.state.partial_gsol_supply, bump: ctx.bumps.epoch_report_account, }; // we have to trust that the extracted amount is accurate, @@ -246,10 +231,9 @@ pub mod marinade_beam { ); ctx.accounts.epoch_report_account.epoch = ctx.accounts.clock.epoch; - ctx.accounts.epoch_report_account.partial_gsol_supply = - ctx.accounts.state.partial_gsol_supply; - let extractable_yield = utils::extractable_yield( + let extractable_yield = utils::calculate_extractable_yield( + &ctx.accounts.sunrise_state, &ctx.accounts.state, &ctx.accounts.marinade_state, &ctx.accounts.msol_vault, @@ -260,7 +244,8 @@ pub mod marinade_beam { } pub fn extract_yield(ctx: Context) -> Result<()> { - let yield_lamports = utils::extractable_yield( + let yield_lamports = utils::calculate_extractable_yield( + &ctx.accounts.sunrise_state, &ctx.accounts.state, &ctx.accounts.marinade_state, &ctx.accounts.msol_vault, @@ -385,9 +370,7 @@ pub struct Deposit<'info> { /// CHECK: Checked by Marinade CPI. pub reserve_pda: UncheckedAccount<'info>, - #[account(address = sunrise_core::ID)] - /// CHECK: The Sunrise program ID. - pub beam_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>, @@ -462,9 +445,7 @@ pub struct DepositStake<'info> { /// CHECK: Checked by Marinade CPI. pub stake_program: UncheckedAccount<'info>, - #[account(address = sunrise_core_cpi::ID)] - /// CHECK: The Sunrise program ID. - pub beam_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>, @@ -531,9 +512,7 @@ pub struct Withdraw<'info> { /// CHECK: Checked by Sunrise CPI. pub instructions_sysvar: UncheckedAccount<'info>, - #[account(address = sunrise_core_cpi::ID)] - /// CHECK: The Sunrise Program ID. - pub beam_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>, @@ -598,9 +577,7 @@ pub struct OrderWithdrawal<'info> { )] pub proxy_ticket_account: Box>, - #[account(address = sunrise_core_cpi::ID)] - /// CHECK: The Sunrise Program ID. - pub beam_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>, @@ -652,6 +629,48 @@ pub struct RedeemTicket<'info> { pub system_program: Program<'info, System>, } +#[derive(Accounts)] +pub struct Burn<'info> { + #[account( + mut, + has_one = sunrise_state, + seeds = [constants::STATE, sunrise_state.key().as_ref()], + bump + )] + pub state: Box>, + #[account(mut)] + /// CHECK: The main Sunrise beam state. + pub sunrise_state: UncheckedAccount<'info>, + + #[account(mut)] + pub burner: Signer<'info>, + #[account(mut, token::mint = gsol_mint)] + pub gsol_token_account: 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>, + + /// CHECK: Checked by Marinade CPI. + pub system_program: UncheckedAccount<'info>, + /// CHECK: Checked by Marinade CPI. + pub token_program: UncheckedAccount<'info>, + + #[account(mut)] + /// Verified in CPI to Sunrise program. + pub gsol_mint: Box>, + /// CHECK: Checked by Sunrise CPI. + pub instructions_sysvar: UncheckedAccount<'info>, + + pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, +} + #[derive(Accounts, Clone)] pub struct ExtractYield<'info> { #[account( @@ -708,8 +727,13 @@ pub struct ExtractYield<'info> { #[derive(Accounts, Clone)] pub struct InitEpochReport<'info> { - #[account(has_one = marinade_state, has_one = update_authority)] + #[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>, @@ -750,8 +774,12 @@ pub struct InitEpochReport<'info> { #[derive(Accounts)] pub struct UpdateEpochReport<'info> { - #[account(has_one = marinade_state)] + #[account( + has_one = marinade_state, + has_one = sunrise_state + )] pub state: Box>, + pub sunrise_state: Box>, #[account(has_one = msol_mint)] pub marinade_state: Box>, @@ -786,6 +814,7 @@ pub struct UpdateEpochReport<'info> { pub clock: Sysvar<'info, Clock>, } + #[error_code] pub enum MarinadeBeamError { #[msg("No delegation for stake account deposit")] diff --git a/programs/marinade-beam/src/state.rs b/programs/marinade-beam/src/state.rs index f9b774e..91cb12f 100644 --- a/programs/marinade-beam/src/state.rs +++ b/programs/marinade-beam/src/state.rs @@ -14,13 +14,6 @@ pub struct State { /// The bump of the PDA that can authorize spending from the vault /// that holds pool tokens (msol in this case). pub vault_authority_bump: u8, - - /// The amount of the current gsol supply this beam is responsible for. - /// This field is also tracked in the matching beam-details struct in the - /// sunrise program's state and is expected to match that value. - // TODO: Consider removing this and always use the value from the sunrise - // state instead. - pub partial_gsol_supply: u64, } // Anchor-ts only supports deserialization(in instruction arguments) for types @@ -41,7 +34,6 @@ impl From for State { marinade_state: se.marinade_state, sunrise_state: se.sunrise_state, vault_authority_bump: se.vault_authority_bump, - partial_gsol_supply: 0, } } } @@ -51,6 +43,5 @@ impl State { 32 + /*update_authority*/ 32 + /*marinade_state*/ 32 + /*sunrise_state*/ - 1 + /*vault_authority_bump*/ - 8; /*partial_gsol_supply*/ + 1; /*vault_authority_bump*/ } diff --git a/programs/marinade-beam/src/system/accounts.rs b/programs/marinade-beam/src/system/accounts.rs index 15525ac..af64a8f 100644 --- a/programs/marinade-beam/src/system/accounts.rs +++ b/programs/marinade-beam/src/system/accounts.rs @@ -20,12 +20,11 @@ pub struct EpochReport { pub total_ordered_lamports: u64, pub extractable_yield: u64, pub extracted_yield: u64, - pub partial_gsol_supply: u64, // TODO: Remove? Already tracked by state account. pub bump: u8, } impl EpochReport { - pub const SPACE: usize = 32 + 8 + 8 + 8 + 8 + 8 + 8 + 1 + 8 /* DISCRIMINATOR */ ; + pub const SPACE: usize = 32 + 8 + 8 + 8 + 8 + 8 + 1 + 8 /* DISCRIMINATOR */ ; pub fn all_extractable_yield(&self) -> u64 { self.extractable_yield @@ -53,11 +52,9 @@ impl EpochReport { pub fn update_report( &mut self, - current_gsol_supply: u64, extractable_yield: u64, add_extracted_yield: u64, ) { - self.partial_gsol_supply = current_gsol_supply; 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 fd69328..5f7caab 100644 --- a/programs/marinade-beam/src/system/utils.rs +++ b/programs/marinade-beam/src/system/utils.rs @@ -5,16 +5,22 @@ use anchor_lang::solana_program::{ }; use anchor_spl::token::TokenAccount; use marinade_cpi::state::State as MarinadeState; +use sunrise_core::BeamError; /// Calculates the amount that can be extracted as yield, in lamports. -pub fn extractable_yield( - state: &State, +pub fn calculate_extractable_yield( + sunrise_state: &sunrise_core::State, + beam_state: &Account, marinade_state: &MarinadeState, msol_vault: &TokenAccount, ) -> Result { let staked_value = super::utils::calc_lamports_from_msol_amount(marinade_state, msol_vault.amount)?; - Ok(staked_value.saturating_sub(state.partial_gsol_supply)) + 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)) } /// calculate amount*numerator/denominator diff --git a/programs/marinade-lp-beam/src/cpi_interface/sunrise.rs b/programs/marinade-lp-beam/src/cpi_interface/sunrise.rs index ff60cc5..8f67e97 100644 --- a/programs/marinade-lp-beam/src/cpi_interface/sunrise.rs +++ b/programs/marinade-lp-beam/src/cpi_interface/sunrise.rs @@ -67,3 +67,17 @@ impl<'a> From<&crate::Withdraw<'a>> for BurnGsol<'a> { } } } + +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.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(), + } + } +} \ No newline at end of file diff --git a/programs/marinade-lp-beam/src/lib.rs b/programs/marinade-lp-beam/src/lib.rs index 9fdced8..e24e3a9 100644 --- a/programs/marinade-lp-beam/src/lib.rs +++ b/programs/marinade-lp-beam/src/lib.rs @@ -49,10 +49,7 @@ pub mod marinade_lp_beam { } pub fn update(ctx: Context, update_input: StateEntry) -> Result<()> { - let mut updated_state: State = update_input.into(); - // Make sure the partial gsol supply remains consistent. - updated_state.partial_gsol_supply = ctx.accounts.state.partial_gsol_supply; - ctx.accounts.state.set_inner(updated_state); + ctx.accounts.state.set_inner(update_input.into()); Ok(()) } @@ -65,19 +62,12 @@ pub mod marinade_lp_beam { // CPI: Mint GSOL of the same proportion as the lamports deposited to the depositor. sunrise_interface::mint_gsol( ctx.accounts.deref(), - ctx.accounts.beam_program.to_account_info(), + ctx.accounts.sunrise_program.to_account_info(), ctx.accounts.sunrise_state.key(), state_bump, lamports, )?; - // Update the partial gsol supply for this beam. - let state_account = &mut ctx.accounts.state; - state_account.partial_gsol_supply = state_account - .partial_gsol_supply - .checked_add(lamports) - .unwrap(); - Ok(()) } @@ -97,22 +87,30 @@ pub mod marinade_lp_beam { // CPI: Burn GSOL of the same proportion as the lamports withdrawn from the depositor. sunrise_interface::burn_gsol( ctx.accounts.deref(), - ctx.accounts.beam_program.to_account_info(), + ctx.accounts.sunrise_program.to_account_info(), ctx.accounts.sunrise_state.key(), state_bump, lamports, )?; - // Update the partial gsol supply for this beam. - let state_account = &mut ctx.accounts.state; - state_account.partial_gsol_supply = state_account - .partial_gsol_supply - .checked_sub(lamports) - .unwrap(); Ok(()) } - // pub fn extract_yield() {} + /// Burning is withdrawing without redeeming the pool tokens. The result is a beam that is "worth more" + /// than the SOL that has been staked into it, i.e. the pool tokens are more valuable than the SOL. + /// This allows yield extraction and can be seen as a form of "donation". + pub fn burn(ctx: Context, lamports: u64) -> Result<()> { + let state_bump = ctx.bumps.state; + sunrise_interface::burn_gsol( + ctx.accounts.deref(), + ctx.accounts.sunrise_program.to_account_info(), + ctx.accounts.sunrise_state.key(), + state_bump, + lamports, + )?; + + Ok(()) + } pub fn order_withdrawal(_ctx: Context) -> Result<()> { // Marinade liq_pool only supports immediate withdrawals. @@ -232,9 +230,7 @@ pub struct Deposit<'info> { /// CHECK: Checked by Marinade CPI. pub token_program: UncheckedAccount<'info>, - #[account(address = sunrise_core_cpi::ID)] - /// CHECK: The Sunrise ProgramID. - pub beam_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>, @@ -304,14 +300,44 @@ pub struct Withdraw<'info> { /// CHECK: Checked by Sunrise CPI. pub instructions_sysvar: UncheckedAccount<'info>, - #[account(address = sunrise_core_cpi::ID)] - /// CHECK: The Sunrise program ID. - pub beam_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>, } +#[derive(Accounts)] +pub struct Burn<'info> { + #[account( + mut, + has_one = sunrise_state, + seeds = [constants::STATE, sunrise_state.key().as_ref()], + bump + )] + pub state: Box>, + #[account(mut)] + /// CHECK: The main Sunrise beam state. + pub sunrise_state: UncheckedAccount<'info>, + + #[account(mut)] + pub burner: Signer<'info>, + #[account(mut, token::mint = gsol_mint)] + pub gsol_token_account: Box>, + + /// CHECK: Checked by Marinade CPI. + pub system_program: UncheckedAccount<'info>, + /// CHECK: Checked by Marinade CPI. + pub token_program: UncheckedAccount<'info>, + + #[account(mut)] + /// Verified in CPI to Sunrise program. + pub gsol_mint: Box>, + /// CHECK: Checked by Sunrise CPI. + pub instructions_sysvar: UncheckedAccount<'info>, + + pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, +} + #[derive(Accounts)] pub struct Noop {} diff --git a/programs/marinade-lp-beam/src/state.rs b/programs/marinade-lp-beam/src/state.rs index 0f4905e..cbb4507 100644 --- a/programs/marinade-lp-beam/src/state.rs +++ b/programs/marinade-lp-beam/src/state.rs @@ -20,13 +20,6 @@ pub struct State { /// The token-account that receives msol when withdrawing liquidity. pub msol_token_account: Pubkey, - - /// The amount of the current gsol supply this beam is responsible for. - /// This field is also tracked in the matching beam-details struct in the - /// sunrise program's state and is expected to match that value. - // TODO: Consider removing this and always use the value from the sunrise - // state instead. - pub partial_gsol_supply: u64, } impl State { @@ -36,8 +29,7 @@ impl State { 32 + /*sunrise_state*/ 1 + /*vault_authority_bump*/ 32 + /*treasury*/ - 32 + /*msol_token_account*/ - 8; /*partial_gsol_supply*/ + 32; /*msol_token_account*/ } // Anchor-ts only supports deserialization(in instruction arguments) for types @@ -62,7 +54,6 @@ impl From for State { vault_authority_bump: se.vault_authority_bump, treasury: se.treasury, msol_token_account: se.msol_token_account, - partial_gsol_supply: 0, } } } diff --git a/programs/marinade-lp-beam/src/system/utils.rs b/programs/marinade-lp-beam/src/system/utils.rs index be78f6e..cd20cd9 100644 --- a/programs/marinade-lp-beam/src/system/utils.rs +++ b/programs/marinade-lp-beam/src/system/utils.rs @@ -3,6 +3,7 @@ use crate::state::State; use anchor_lang::prelude::*; use anchor_spl::token::{Mint, TokenAccount}; 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 @@ -17,8 +18,9 @@ pub(super) fn proportional(amount: u64, numerator: u64, denominator: u64) -> Res } /// Calculates the amount that can be extracted as yield, in lamports. -pub fn extractable_yield( - state: &State, +pub fn calculate_extractable_yield( + sunrise_state: &sunrise_core::State, + beam_state: &Account, marinade_state: &MarinadeState, liq_pool_mint: &Mint, liq_pool_token_account: &TokenAccount, @@ -33,7 +35,11 @@ pub fn extractable_yield( liq_pool_msol_leg, )? .sol_value(marinade_state); - Ok(staked_value.saturating_sub(state.partial_gsol_supply)) + 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)) } // Prevent the compiler from enlarging the stack and potentially triggering an Access violation diff --git a/programs/spl-beam/src/cpi_interface/sunrise.rs b/programs/spl-beam/src/cpi_interface/sunrise.rs index 2b226e0..2df5c4e 100644 --- a/programs/spl-beam/src/cpi_interface/sunrise.rs +++ b/programs/spl-beam/src/cpi_interface/sunrise.rs @@ -106,3 +106,17 @@ impl<'a> From<&crate::WithdrawStake<'a>> for BurnGsol<'a> { } } } + +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.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(), + } + } +} \ No newline at end of file diff --git a/programs/spl-beam/src/lib.rs b/programs/spl-beam/src/lib.rs index b7a9679..c19826b 100644 --- a/programs/spl-beam/src/lib.rs +++ b/programs/spl-beam/src/lib.rs @@ -40,9 +40,7 @@ pub mod spl_beam { } pub fn update(ctx: Context, update_input: StateEntry) -> Result<()> { - let mut updated_state: State = update_input.into(); - // Make sure the partial gsol supply remains consistent. - updated_state.partial_gsol_supply = ctx.accounts.state.partial_gsol_supply; + let updated_state: State = update_input.into(); ctx.accounts.state.set_inner(updated_state); Ok(()) } @@ -62,12 +60,6 @@ pub mod spl_beam { lamports, )?; - // Update the partial gsol supply for this beam. - let state_account = &mut ctx.accounts.state; - state_account.partial_gsol_supply = state_account - .partial_gsol_supply - .checked_add(lamports) - .unwrap(); Ok(()) } @@ -88,12 +80,6 @@ pub mod spl_beam { lamports, )?; - // Update the partial gsol supply for this beam. - let state_account = &mut ctx.accounts.state; - state_account.partial_gsol_supply = state_account - .partial_gsol_supply - .checked_add(lamports) - .unwrap(); Ok(()) } @@ -110,19 +96,31 @@ pub mod spl_beam { let state_bump = ctx.bumps.state; sunrise_interface::burn_gsol( ctx.accounts.deref(), - ctx.accounts.beam_program.to_account_info(), + ctx.accounts.sunrise_program.to_account_info(), ctx.accounts.sunrise_state.key(), pool.key(), state_bump, lamports, )?; - // Update the partial gsol supply for this beam. - let state_account = &mut ctx.accounts.state; - state_account.partial_gsol_supply = state_account - .partial_gsol_supply - .checked_sub(lamports) - .unwrap(); + Ok(()) + } + + /// Burning is withdrawing without redeeming the pool tokens. The result is a beam that is "worth more" + /// than the SOL that has been staked into it, i.e. the pool tokens are more valuable than the SOL. + /// This allows yield extraction and can be seen as a form of "donation". + pub fn burn(ctx: Context, lamports: u64) -> Result<()> { + let pool = &ctx.accounts.stake_pool; + + let state_bump = ctx.bumps.state; + sunrise_interface::burn_gsol( + ctx.accounts.deref(), + ctx.accounts.sunrise_program.to_account_info(), + ctx.accounts.sunrise_state.key(), + pool.key(), + state_bump, + lamports, + )?; Ok(()) } @@ -148,13 +146,6 @@ pub mod spl_beam { lamports, )?; - // Update the partial gsol supply for this beam. - let state_account = &mut ctx.accounts.state; - state_account.partial_gsol_supply = state_account - .partial_gsol_supply - .checked_sub(lamports) - .unwrap(); - Ok(()) } @@ -430,12 +421,8 @@ pub struct Withdraw<'info> { /// CHECK: Checked by CPI to Sunrise. pub instructions_sysvar: UncheckedAccount<'info>, - #[account(address = sunrise_core_cpi::ID)] - /// CHECK: The Sunrise program ID. - pub beam_program: UncheckedAccount<'info>, - #[account(address = spl_stake_pool::ID)] - /// CHECK: The SPL StakePool ProgramID. - pub spl_stake_pool_program: UncheckedAccount<'info>, + pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, + pub spl_stake_pool_program: Program<'info, SplStakePool>, pub system_program: Program<'info, System>, pub token_program: Program<'info, Token>, @@ -594,6 +581,37 @@ pub struct ExtractYield<'info> { pub token_program: Program<'info, Token>, } +#[derive(Accounts)] +pub struct Burn<'info> { + #[account( + mut, + has_one = sunrise_state, + has_one = stake_pool, + seeds = [STATE, sunrise_state.key().as_ref(), stake_pool.key().as_ref()], + bump + )] + pub state: Box>, + #[account(mut)] + pub sunrise_state: Box>, + pub stake_pool: Box>, + + #[account(mut)] + pub burner: Signer<'info>, + #[account(mut, token::mint = gsol_mint)] + pub gsol_token_account: Box>, + + #[account(mut)] + /// Verified in CPI to Sunrise program. + pub gsol_mint: Account<'info, Mint>, + /// CHECK: Checked by CPI to Sunrise. + pub instructions_sysvar: UncheckedAccount<'info>, + + pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, + + pub system_program: Program<'info, System>, + pub token_program: Program<'info, Token>, +} + #[derive(Accounts)] pub struct Noop {} diff --git a/programs/spl-beam/src/state.rs b/programs/spl-beam/src/state.rs index e17fd49..203e8f4 100644 --- a/programs/spl-beam/src/state.rs +++ b/programs/spl-beam/src/state.rs @@ -15,13 +15,6 @@ pub struct State { /// The bump of the PDA that can authorize spending from the vault /// that holds pool tokens. pub vault_authority_bump: u8, - - /// The amount of the current gsol supply this beam is responsible for. - /// This field is also tracked in the matching beam-details struct in the - /// sunrise program's state and is expected to match that value. - // TODO: Consider removing this and always use the value from the sunrise - // state instead. - pub partial_gsol_supply: u64, } impl State { @@ -29,8 +22,7 @@ impl State { 32 + /*update_authority*/ 32 + /*spl_state*/ 32 + /*sunrise_state*/ - 1 + /*vault_authority_bump*/ - 8; /*partial_gsol_supply*/ + 1; /*vault_authority_bump*/ } // Anchor-ts only supports deserialization(in instruction arguments) for types @@ -51,7 +43,6 @@ impl From for State { stake_pool: se.stake_pool, sunrise_state: se.sunrise_state, vault_authority_bump: se.vault_authority_bump, - partial_gsol_supply: 0, } } } diff --git a/programs/sunrise-core/src/instructions/burn_gsol.rs b/programs/sunrise-core/src/instructions/burn_gsol.rs index 0c4ddb9..deedf24 100644 --- a/programs/sunrise-core/src/instructions/burn_gsol.rs +++ b/programs/sunrise-core/src/instructions/burn_gsol.rs @@ -16,6 +16,7 @@ pub fn handler(ctx: Context, amount_in_lamports: u64) -> Result<()> { // Can't burn more gsol than this beam is responsible for. if details.partial_gsol_supply < amount { + msg!("Beam supply {}, requested burn {}", details.partial_gsol_supply, amount); return Err(BeamError::BurnWindowExceeded.into()); } diff --git a/programs/sunrise-core/src/state.rs b/programs/sunrise-core/src/state.rs index 2ce2f83..60269a8 100644 --- a/programs/sunrise-core/src/state.rs +++ b/programs/sunrise-core/src/state.rs @@ -174,8 +174,6 @@ impl State { /// Get a shared reference to a [BeamDetails] given its key. pub fn get_beam_details(&self, key: &Pubkey) -> Option<&BeamDetails> { - msg!("get_beam_details"); - msg!("{:?}", self.allocations); self.allocations.iter().find(|x| x.key == *key) } From ff18070da198c88c75cea0794d54c5ad08c0320d Mon Sep 17 00:00:00 2001 From: dankelleher Date: Thu, 18 Jan 2024 13:58:08 +0100 Subject: [PATCH 05/10] SPL Stake Pool extract yield fix loop in CPI call --- packages/sdks/common/src/types/spl_beam.ts | 8 ++-- packages/sdks/common/src/utils.ts | 21 +++++++++ packages/sdks/spl/src/index.ts | 46 ++++++++++--------- .../functional/beams/spl-stake-pool.test.ts | 9 +++- .../spl-beam/src/cpi_interface/program.rs | 2 + programs/spl-beam/src/lib.rs | 6 ++- 6 files changed, 63 insertions(+), 29 deletions(-) diff --git a/packages/sdks/common/src/types/spl_beam.ts b/packages/sdks/common/src/types/spl_beam.ts index 2c0eb87..7f26264 100644 --- a/packages/sdks/common/src/types/spl_beam.ts +++ b/packages/sdks/common/src/types/spl_beam.ts @@ -668,7 +668,7 @@ export type SplBeam = { }, { "name": "newStakeAccount", - "isMut": false, + "isMut": true, "isSigner": false }, { @@ -688,7 +688,7 @@ export type SplBeam = { }, { "name": "validatorStakeList", - "isMut": false, + "isMut": true, "isSigner": false }, { @@ -1489,7 +1489,7 @@ export const IDL: SplBeam = { }, { "name": "newStakeAccount", - "isMut": false, + "isMut": true, "isSigner": false }, { @@ -1509,7 +1509,7 @@ export const IDL: SplBeam = { }, { "name": "validatorStakeList", - "isMut": false, + "isMut": true, "isSigner": false }, { diff --git a/packages/sdks/common/src/utils.ts b/packages/sdks/common/src/utils.ts index 3368820..2f7e400 100644 --- a/packages/sdks/common/src/utils.ts +++ b/packages/sdks/common/src/utils.ts @@ -1,11 +1,13 @@ import { AnchorProvider } from "@coral-xyz/anchor"; import BN from "bn.js"; import { + ComputeBudgetProgram, type ConfirmOptions, Connection, PublicKey, type Signer, Transaction, + TransactionInstruction, } from "@solana/web3.js"; import { STAKE_PROGRAM_ID } from "./constants.js"; @@ -135,3 +137,22 @@ export const sendAndConfirmChecked = async ( throw err; } }; + +/** + * If a transaction requires additional compute units, use this function to create an instruction that requests them. + * @param cusRequested + */ +export const requestIncreasedCUsIx = ( + cusRequested: number, +): TransactionInstruction => { + return ComputeBudgetProgram.setComputeUnitLimit({ + units: cusRequested, + }); +}; + +export const logAccounts = (accounts: Record): void => { + console.log("Accounts:"); + for (const [name, address] of Object.entries(accounts)) { + console.log(` ${name}: ${address.toBase58()}`); + } +}; diff --git a/packages/sdks/spl/src/index.ts b/packages/sdks/spl/src/index.ts index b3a609d..bac585a 100644 --- a/packages/sdks/spl/src/index.ts +++ b/packages/sdks/spl/src/index.ts @@ -18,6 +18,7 @@ import { import { BeamInterface, getParsedStakeAccountInfo, + logAccounts, sendAndConfirmChecked, SPL_STAKE_POOL_PROGRAM_ID, SplBeam, @@ -434,30 +435,31 @@ export class SplClient extends BeamInterface { this.program.programId, this.stateAddress, ); - + const accounts = { + state: this.stateAddress, + stakePool: this.spl.stakePoolAddress, + sunriseState: this.state.sunriseState, + poolMint: this.spl.stakePoolState.poolMint, + yieldAccount: this.sunrise.state.yieldAccount, + newStakeAccount, + vaultAuthority: this.vaultAuthority[0], + poolTokenVault: this.spl.beamVault, + stakePoolWithdrawAuthority: this.spl.withdrawAuthority, + validatorStakeList: this.spl.stakePoolState.validatorList, + stakeAccountToSplit: this.spl.stakePoolState.reserveStake, + managerFeeAccount: this.spl.stakePoolState.managerFeeAccount, + sysvarClock: SYSVAR_CLOCK_PUBKEY, + nativeStakeProgram: StakeProgram.programId, + // instructionsSysvar, + sunriseProgram: this.sunrise.program.programId, + splStakePoolProgram: SPL_STAKE_POOL_PROGRAM_ID, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + }; + logAccounts(accounts); const instruction = await this.program.methods .extractYield() - .accounts({ - state: this.stateAddress, - stakePool: this.spl.stakePoolAddress, - sunriseState: this.state.sunriseState, - poolMint: this.spl.stakePoolState.poolMint, - yieldAccount: this.sunrise.state.yieldAccount, - newStakeAccount, - vaultAuthority: this.vaultAuthority[0], - poolTokenVault: this.spl.beamVault, - stakePoolWithdrawAuthority: this.spl.withdrawAuthority, - validatorStakeList: this.spl.stakePoolState.validatorList, - stakeAccountToSplit: this.spl.stakePoolState.reserveStake, - managerFeeAccount: this.spl.stakePoolState.managerFeeAccount, - sysvarClock: SYSVAR_CLOCK_PUBKEY, - nativeStakeProgram: StakeProgram.programId, - // instructionsSysvar, - sunriseProgram: this.sunrise.program.programId, - splStakePoolProgram: SPL_STAKE_POOL_PROGRAM_ID, - systemProgram: SystemProgram.programId, - tokenProgram: TOKEN_PROGRAM_ID, - }) + .accounts(accounts) .instruction(); return new Transaction().add(instruction); 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 8a35a66..2d4bf04 100644 --- a/packages/tests/src/functional/beams/spl-stake-pool.test.ts +++ b/packages/tests/src/functional/beams/spl-stake-pool.test.ts @@ -29,6 +29,7 @@ describe("SPL stake 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(); @@ -188,7 +189,6 @@ describe("SPL stake pool beam", () => { it("can burn gsol", async () => { // burn some gsol to simulate the creation of yield - const burnAmount = new BN(1 * LAMPORTS_PER_SOL); await sendAndConfirmTransaction( stakerIdentity, await beamClient.burnGSol(burnAmount), @@ -212,5 +212,12 @@ describe("SPL stake pool beam", () => { stakerIdentity, await beamClient.extractYield(), ); + + // we burned `burnAmount` gsol, so we should have `burnAmount` gsol in the yield account + await expectTokenBalance( + beamClient.provider, + beamClient.sunrise.state.yieldAccount, + burnAmount, + ); }); }); diff --git a/programs/spl-beam/src/cpi_interface/program.rs b/programs/spl-beam/src/cpi_interface/program.rs index 454fb63..2e57954 100644 --- a/programs/spl-beam/src/cpi_interface/program.rs +++ b/programs/spl-beam/src/cpi_interface/program.rs @@ -8,6 +8,7 @@ pub const SPL_STAKE_POOL_PROGRAM_ID: Pubkey = Pubkey::new_from_array([ 29, 68, 183, 34, 147, 246, 219, 219, 0, 22, 80, ]); +#[derive(Clone)] pub struct SplStakePool; impl Id for SplStakePool { @@ -16,6 +17,7 @@ impl Id for SplStakePool { } } +#[derive(Clone)] pub struct NativeStakeProgram; impl Id for NativeStakeProgram { diff --git a/programs/spl-beam/src/lib.rs b/programs/spl-beam/src/lib.rs index c19826b..0f68214 100644 --- a/programs/spl-beam/src/lib.rs +++ b/programs/spl-beam/src/lib.rs @@ -504,7 +504,7 @@ pub struct WithdrawStake<'info> { pub token_program: Program<'info, Token>, } -#[derive(Accounts)] +#[derive(Accounts, Clone)] pub struct ExtractYield<'info> { #[account( has_one = stake_pool, @@ -529,13 +529,14 @@ pub struct ExtractYield<'info> { pub yield_account: UncheckedAccount<'info>, #[account( + mut, seeds = [ state.key().as_ref(), EXTRACT_YIELD_STAKE_ACCOUNT ], bump )] - /// CHECK: The uninitialized new stake account. + /// CHECK: The uninitialized new stake account. Will be initialised by CPI to the SPL StakePool program. pub new_stake_account: UncheckedAccount<'info>, #[account( @@ -558,6 +559,7 @@ pub struct ExtractYield<'info> { /// CHECK: Checked by CPI to SPL StakePool Program. pub stake_pool_withdraw_authority: UncheckedAccount<'info>, + #[account(mut)] /// CHECK: Checked by CPI to SPL StakePool program. pub validator_stake_list: UncheckedAccount<'info>, #[account(mut)] From bcd65c63491f49a546ff814512ec93ddf85d9396 Mon Sep 17 00:00:00 2001 From: dankelleher Date: Sun, 21 Jan 2024 13:00:47 +0100 Subject: [PATCH 06/10] SPL Stake Pool extract yield to stake account --- Cargo.lock | 1 + packages/sdks/spl/src/index.ts | 16 +++-- .../functional/beams/spl-stake-pool.test.ts | 37 +++++++++-- packages/tests/src/utils.ts | 19 ++++++ .../src/cpi_interface/sunrise.rs | 2 +- programs/marinade-beam/src/lib.rs | 1 - programs/marinade-beam/src/system/accounts.rs | 6 +- .../src/cpi_interface/sunrise.rs | 2 +- programs/spl-beam/Cargo.toml | 5 +- programs/spl-beam/src/constants.rs | 2 + programs/spl-beam/src/cpi_interface/mod.rs | 1 + .../src/cpi_interface/stake_account.rs | 36 +++++++++++ .../spl-beam/src/cpi_interface/stake_pool.rs | 7 ++- .../spl-beam/src/cpi_interface/sunrise.rs | 2 +- programs/spl-beam/src/lib.rs | 25 +++++--- programs/spl-beam/src/state.rs | 2 +- programs/spl-beam/src/utils.rs | 63 +++++++++++++------ .../src/instructions/burn_gsol.rs | 6 +- programs/sunrise-core/src/lib.rs | 4 +- 19 files changed, 184 insertions(+), 53 deletions(-) create mode 100644 programs/spl-beam/src/constants.rs create mode 100644 programs/spl-beam/src/cpi_interface/stake_account.rs diff --git a/Cargo.lock b/Cargo.lock index 26fa071..c395242 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4808,6 +4808,7 @@ version = "0.1.0" dependencies = [ "anchor-lang", "anchor-spl", + "borsh 0.10.3", "once_cell", "rstest", "spl-stake-pool", diff --git a/packages/sdks/spl/src/index.ts b/packages/sdks/spl/src/index.ts index bac585a..419b072 100644 --- a/packages/sdks/spl/src/index.ts +++ b/packages/sdks/spl/src/index.ts @@ -18,7 +18,6 @@ import { import { BeamInterface, getParsedStakeAccountInfo, - logAccounts, sendAndConfirmChecked, SPL_STAKE_POOL_PROGRAM_ID, SplBeam, @@ -450,13 +449,11 @@ export class SplClient extends BeamInterface { managerFeeAccount: this.spl.stakePoolState.managerFeeAccount, sysvarClock: SYSVAR_CLOCK_PUBKEY, nativeStakeProgram: StakeProgram.programId, - // instructionsSysvar, sunriseProgram: this.sunrise.program.programId, splStakePoolProgram: SPL_STAKE_POOL_PROGRAM_ID, systemProgram: SystemProgram.programId, tokenProgram: TOKEN_PROGRAM_ID, }; - logAccounts(accounts); const instruction = await this.program.methods .extractYield() .accounts(accounts) @@ -499,14 +496,21 @@ export class SplClient extends BeamInterface { } /** Utility method to derive the SPL-beam address from the sunrise state, the stake pool and program ID. */ - public static deriveStateAddress = ( + public static deriveStateAddress( sunriseState: PublicKey, stakePool: PublicKey, programId?: PublicKey, - ): [PublicKey, number] => { + ): [PublicKey, number] { const PID = programId ?? SPL_BEAM_PROGRAM_ID; return Utils.deriveStateAddress(PID, sunriseState, stakePool); - }; + } + + public get yieldStakeAccount() { + return Utils.deriveExtractYieldStakeAccount( + this.program.programId, + this.stateAddress, + )[0]; + } public async details() { const balance = await this.provider.connection.getTokenAccountBalance( 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 2d4bf04..0242720 100644 --- a/packages/tests/src/functional/beams/spl-stake-pool.test.ts +++ b/packages/tests/src/functional/beams/spl-stake-pool.test.ts @@ -6,11 +6,13 @@ import { SplClient } from "@sunrisestake/beams-spl"; import { Keypair, LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js"; import BN from "bn.js"; import { + expectStakeAccountBalance, expectTokenBalance, fund, registerSunriseState, sendAndConfirmTransaction, tokenAccountBalance, + waitForNextEpoch, } from "../../utils.js"; import { provider, staker, stakerIdentity } from "../setup.js"; import { expect } from "chai"; @@ -183,8 +185,6 @@ describe("SPL stake pool beam", () => { ); stakerGsolBalance = expectedGsol; vaultStakePoolSolBalance = expectedBsol; - - console.log("Remaining gSOL: " + stakerGsolBalance.toString()); }); it("can burn gsol", async () => { @@ -203,8 +203,9 @@ describe("SPL stake pool beam", () => { ); }); - it("can extract yield", async () => { + 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 @@ -213,7 +214,35 @@ describe("SPL stake pool beam", () => { await beamClient.extractYield(), ); - // we burned `burnAmount` gsol, so we should have `burnAmount` gsol in the yield account + // we burned `burnAmount` gsol, so we should have `burnAmount` - fee in the stake account + const expectedFee = burnAmount + .mul(beamClient.spl.stakePoolState.stakeWithdrawalFee.numerator) + .div(beamClient.spl.stakePoolState.stakeWithdrawalFee.denominator); + const expectedStakeAmount = burnAmount.sub(expectedFee); + + // there is no yield yet, but we have created a stake account for the yield + await expectStakeAccountBalance( + beamClient.provider, + beamClient.yieldStakeAccount, + expectedStakeAmount, + 1, + ); + }); + + it.skip("can claim stake account into yield account after cooldown", async () => { + // wait an epoch for the stake account to cool down + await waitForNextEpoch(beamClient.provider); + + // TODO enable + // 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, + // // TODO move this and extract yield into "management" object to make it clear that these + // // do not need to be executed by end-users + // await beamClient.claimExtractedYieldStakeAcccount(), + // ); + await expectTokenBalance( beamClient.provider, beamClient.sunrise.state.yieldAccount, diff --git a/packages/tests/src/utils.ts b/packages/tests/src/utils.ts index ef2a4ca..28998c3 100644 --- a/packages/tests/src/utils.ts +++ b/packages/tests/src/utils.ts @@ -23,12 +23,14 @@ import chaiAsPromised from "chai-as-promised"; import { Idl } from "@coral-xyz/anchor"; import { provider } from "./functional/setup.js"; import { SunriseClient } from "@sunrisestake/beams-core"; +import { getParsedStakeAccountInfo } from "@sunrisestake/beams-common"; chai.use(chaiAsPromised); const { expect } = chai; // Set in anchor.toml const SLOTS_IN_EPOCH = 32; +const STAKE_ACCOUNT_RENT_EXEMPT_AMOUNT = 2282880; const LOG_LEVELS = ["error", "warn", "info", "debug", "trace"] as const; type LOG_LEVEL = (typeof LOG_LEVELS)[number]; @@ -84,6 +86,23 @@ export const expectTokenBalance = async ( expectAmount(actualAmount, expectedAmount, tolerance); }; +export const expectStakeAccountBalance = async ( + provider: AnchorProvider, + stakeAccountAddress: PublicKey, + expectedAmount: number | BN, + tolerance = 0, +) => { + const stakeAccount = await getParsedStakeAccountInfo( + provider, + stakeAccountAddress, + ); + + const actualAmount = (stakeAccount.balanceLamports ?? new BN(0)).subn( + STAKE_ACCOUNT_RENT_EXEMPT_AMOUNT, + ); + expectAmount(actualAmount, expectedAmount, tolerance); +}; + // These functions use string equality to allow large numbers. // BN(number) throws assertion errors if the number is large export const expectStakerSolBalance = async ( diff --git a/programs/marinade-beam/src/cpi_interface/sunrise.rs b/programs/marinade-beam/src/cpi_interface/sunrise.rs index 4035346..2f495cb 100644 --- a/programs/marinade-beam/src/cpi_interface/sunrise.rs +++ b/programs/marinade-beam/src/cpi_interface/sunrise.rs @@ -108,4 +108,4 @@ impl<'a> From<&crate::Burn<'a>> for BurnGsol<'a> { token_program: accounts.token_program.to_account_info(), } } -} \ No newline at end of file +} diff --git a/programs/marinade-beam/src/lib.rs b/programs/marinade-beam/src/lib.rs index d430027..4def027 100644 --- a/programs/marinade-beam/src/lib.rs +++ b/programs/marinade-beam/src/lib.rs @@ -814,7 +814,6 @@ pub struct UpdateEpochReport<'info> { pub clock: Sysvar<'info, Clock>, } - #[error_code] pub enum MarinadeBeamError { #[msg("No delegation for stake account deposit")] diff --git a/programs/marinade-beam/src/system/accounts.rs b/programs/marinade-beam/src/system/accounts.rs index af64a8f..1b057d8 100644 --- a/programs/marinade-beam/src/system/accounts.rs +++ b/programs/marinade-beam/src/system/accounts.rs @@ -50,11 +50,7 @@ impl EpochReport { self.extracted_yield = self.extracted_yield.checked_add(extracted_yield).unwrap(); } - pub fn update_report( - &mut self, - extractable_yield: u64, - add_extracted_yield: u64, - ) { + 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-lp-beam/src/cpi_interface/sunrise.rs b/programs/marinade-lp-beam/src/cpi_interface/sunrise.rs index 8f67e97..11ca030 100644 --- a/programs/marinade-lp-beam/src/cpi_interface/sunrise.rs +++ b/programs/marinade-lp-beam/src/cpi_interface/sunrise.rs @@ -80,4 +80,4 @@ impl<'a> From<&crate::Burn<'a>> for BurnGsol<'a> { token_program: accounts.token_program.to_account_info(), } } -} \ No newline at end of file +} diff --git a/programs/spl-beam/Cargo.toml b/programs/spl-beam/Cargo.toml index 2210f9f..5740b13 100644 --- a/programs/spl-beam/Cargo.toml +++ b/programs/spl-beam/Cargo.toml @@ -16,8 +16,11 @@ cpi = ["no-entrypoint"] default = [] [dependencies] -anchor-lang = '0.29.0' +anchor-lang = { version = '0.29.0', features = ["init-if-needed"] } +# the stake feature does not compile with solana-program 1.17 +#anchor-spl = { version = '0.29.0', features = ["stake"] } anchor-spl = '0.29.0' +borsh = "0.10.3" spl-stake-pool = { git = "https://github.com/solana-labs/solana-program-library", features = ["no-entrypoint"] } sunrise-core = { path = "../sunrise-core", features = ["cpi"] } once_cell = "1.19.0" diff --git a/programs/spl-beam/src/constants.rs b/programs/spl-beam/src/constants.rs new file mode 100644 index 0000000..8f1aae8 --- /dev/null +++ b/programs/spl-beam/src/constants.rs @@ -0,0 +1,2 @@ +/// The size of a stake account in bytes. +pub const STAKE_ACCOUNT_SIZE: usize = 200; diff --git a/programs/spl-beam/src/cpi_interface/mod.rs b/programs/spl-beam/src/cpi_interface/mod.rs index a51f9bd..b660053 100644 --- a/programs/spl-beam/src/cpi_interface/mod.rs +++ b/programs/spl-beam/src/cpi_interface/mod.rs @@ -1,4 +1,5 @@ pub mod program; pub mod spl; +pub mod stake_account; pub mod stake_pool; pub mod sunrise; diff --git a/programs/spl-beam/src/cpi_interface/stake_account.rs b/programs/spl-beam/src/cpi_interface/stake_account.rs new file mode 100644 index 0000000..ba77fcd --- /dev/null +++ b/programs/spl-beam/src/cpi_interface/stake_account.rs @@ -0,0 +1,36 @@ +use anchor_lang::prelude::Pubkey; +use anchor_lang::solana_program::stake::program::ID; +use borsh::BorshDeserialize; +use spl_stake_pool::solana_program::stake::state::StakeStateV2; +use std::ops::Deref; + +/// A redefined StakeAccount that wraps spl StakeStateV2 +/// This is needed until anchor-spl updates to solana-program 1.17 +#[derive(Clone)] +pub struct StakeAccount(StakeStateV2); + +impl anchor_lang::AccountDeserialize for StakeAccount { + fn try_deserialize(buf: &mut &[u8]) -> anchor_lang::Result { + Self::try_deserialize_unchecked(buf) + } + + fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result { + StakeStateV2::deserialize(buf).map(Self).map_err(Into::into) + } +} + +impl anchor_lang::AccountSerialize for StakeAccount {} + +impl anchor_lang::Owner for StakeAccount { + fn owner() -> Pubkey { + ID + } +} + +impl Deref for StakeAccount { + type Target = StakeStateV2; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/programs/spl-beam/src/cpi_interface/stake_pool.rs b/programs/spl-beam/src/cpi_interface/stake_pool.rs index 05fec22..68766c7 100644 --- a/programs/spl-beam/src/cpi_interface/stake_pool.rs +++ b/programs/spl-beam/src/cpi_interface/stake_pool.rs @@ -33,15 +33,16 @@ impl Deref for StakePool { #[cfg(test)] mod tests { - use anchor_lang::__private::base64; - use anchor_lang::Owner; use super::*; + use anchor_lang::Owner; + use anchor_lang::__private::base64; // This is a stake pool account - see packages/tests/fixtures/spl/pool.json const BASE64_POOL_DATA: &str = "AQi2aQPmj/kyc1PszrLaqtyAYSpJobj5d6Ix+gjkmqjkCLZpA+aP+TJzU+zOstqq3IBhKkmhuPl3ojH6COSaqOR0TlK3ODVp7q8xpWvvF7+QNZz/+Qxc/JYj8YXrjzH2C/wIg0ukdM5I0b2+7xzv1QkIWnhD3KHOW51k82GiSUI9HwiQKTXm+75i8/+yT5wnONmvFKIUMPYZ6vcHRhqy8CAcCNLpcPk8ez1QGR5hGs2TqoClRrReyWXhiwWHFVaZyKy+io330TRBlc2b9EScxWN+/9wvGEkjPl5KMq8RBlm3bgbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCp1QsvlBIAAADIq5cMEgAAALoBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAECcAAAAAAAD0AQAAAAAAAAAAABAnAAAAAAAACAAAAAAAAAAQJwAAAAAAAAoAAAAAAAAAAGQAECcAAAAAAAAIAAAAAAAAAGQAECcAAAAAAAADAAAAAAAAAADIq5cMEgAAANULL5QSAAAAAwAAAAAAAAEAAAAAAAAAARAnAAAAAAAAAwAAAAAAAAAJLUF3EAAAANWF8/IQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; // bSo13r4TkiE4KumL71LsHTPpL2euBYLFx6h9HP3piy1 const EXPECTED_POOL_MINT: Pubkey = Pubkey::new_from_array([ - 8,210,233,112,249,60,123,61,80,25,30,97,26,205,147,170,128,165,70,180,94,201,101,225,139,5,135,21,86,153,200,172 + 8, 210, 233, 112, 249, 60, 123, 61, 80, 25, 30, 97, 26, 205, 147, 170, 128, 165, 70, 180, + 94, 201, 101, 225, 139, 5, 135, 21, 86, 153, 200, 172, ]); #[test] diff --git a/programs/spl-beam/src/cpi_interface/sunrise.rs b/programs/spl-beam/src/cpi_interface/sunrise.rs index 2df5c4e..c742a81 100644 --- a/programs/spl-beam/src/cpi_interface/sunrise.rs +++ b/programs/spl-beam/src/cpi_interface/sunrise.rs @@ -119,4 +119,4 @@ impl<'a> From<&crate::Burn<'a>> for BurnGsol<'a> { token_program: accounts.token_program.to_account_info(), } } -} \ No newline at end of file +} diff --git a/programs/spl-beam/src/lib.rs b/programs/spl-beam/src/lib.rs index 0f68214..dc516b8 100644 --- a/programs/spl-beam/src/lib.rs +++ b/programs/spl-beam/src/lib.rs @@ -1,18 +1,24 @@ #![allow(clippy::result_large_err)] use anchor_lang::prelude::*; +use anchor_lang::AnchorDeserialize; use anchor_spl::associated_token::{AssociatedToken, Create}; use anchor_spl::token::{Mint, Token, TokenAccount}; -use cpi_interface::spl as spl_interface; -use cpi_interface::sunrise as sunrise_interface; +use constants::STAKE_ACCOUNT_SIZE; +use cpi_interface::{ + program::{NativeStakeProgram, SplStakePool}, + spl as spl_interface, + stake_pool::StakePool, + sunrise as sunrise_interface, +}; use seeds::*; use state::{State, StateEntry}; use std::ops::Deref; -use crate::cpi_interface::program::{NativeStakeProgram, SplStakePool}; -use cpi_interface::stake_pool::StakePool; +use crate::cpi_interface::stake_account::StakeAccount; use sunrise_core as sunrise_core_cpi; +mod constants; mod cpi_interface; mod seeds; mod state; @@ -105,10 +111,10 @@ pub mod spl_beam { Ok(()) } - + /// Burning is withdrawing without redeeming the pool tokens. The result is a beam that is "worth more" /// than the SOL that has been staked into it, i.e. the pool tokens are more valuable than the SOL. - /// This allows yield extraction and can be seen as a form of "donation". + /// This allows yield extraction and can be seen as a form of "donation". pub fn burn(ctx: Context, lamports: u64) -> Result<()> { let pool = &ctx.accounts.stake_pool; @@ -529,7 +535,10 @@ pub struct ExtractYield<'info> { pub yield_account: UncheckedAccount<'info>, #[account( - mut, + init_if_needed, + space = STAKE_ACCOUNT_SIZE, + payer = payer, + owner = anchor_lang::solana_program::stake::program::ID, seeds = [ state.key().as_ref(), EXTRACT_YIELD_STAKE_ACCOUNT @@ -537,7 +546,7 @@ pub struct ExtractYield<'info> { bump )] /// CHECK: The uninitialized new stake account. Will be initialised by CPI to the SPL StakePool program. - pub new_stake_account: UncheckedAccount<'info>, + pub new_stake_account: Account<'info, StakeAccount>, #[account( seeds = [ diff --git a/programs/spl-beam/src/state.rs b/programs/spl-beam/src/state.rs index 203e8f4..00870fe 100644 --- a/programs/spl-beam/src/state.rs +++ b/programs/spl-beam/src/state.rs @@ -22,7 +22,7 @@ impl State { 32 + /*update_authority*/ 32 + /*spl_state*/ 32 + /*sunrise_state*/ - 1; /*vault_authority_bump*/ + 1; /*vault_authority_bump*/ } // Anchor-ts only supports deserialization(in instruction arguments) for types diff --git a/programs/spl-beam/src/utils.rs b/programs/spl-beam/src/utils.rs index f1a5ba3..fe8c73b 100644 --- a/programs/spl-beam/src/utils.rs +++ b/programs/spl-beam/src/utils.rs @@ -48,10 +48,16 @@ pub fn calculate_extractable_yield( // Calculate the beam's ownership of the stake pool state let total_lamports = stake_pool.total_lamports; // the total number of lamports staked in the pool let token_supply = stake_pool.pool_token_supply; // the total number of pool tokens in existence - let balance = pool_token_vault.amount; // how many pool tokens the beam owns + let balance = pool_token_vault.amount; // how many pool tokens the beam owns let owned_pool_value = proportional(balance, total_lamports, token_supply)?; // the value in lamports of the pool tokens owned by the beam - msg!("owned_pool_value: {}, total_lamports: {}, token_supply: {}, balance: {}", owned_pool_value, total_lamports, token_supply, balance); + msg!( + "owned_pool_value: {}, total_lamports: {}, token_supply: {}, balance: {}", + owned_pool_value, + total_lamports, + token_supply, + balance + ); // Calculate the amount of SOL staked in the beam let details = sunrise_state @@ -66,19 +72,22 @@ pub fn calculate_extractable_yield( #[cfg(test)] mod utils_tests { - use std::cell::RefCell; - use std::rc::Rc; + use super::*; use anchor_lang::__private::base64; use anchor_lang::solana_program::program_pack::Pack; use anchor_spl::token::spl_token; use anchor_spl::token::spl_token::state::AccountState; - use sunrise_core::BeamDetails; use rstest::rstest; - use super::*; + use std::cell::RefCell; + use std::rc::Rc; + use sunrise_core::BeamDetails; static mut LAMPORTS_STORAGE: u64 = 0; - fn clone_token_account_with_amount(token_account: &TokenAccount, new_amount: u64) -> Result { + fn clone_token_account_with_amount( + token_account: &TokenAccount, + new_amount: u64, + ) -> Result { let new_spl_account = spl_token::state::Account { mint: token_account.mint, owner: token_account.owner, @@ -108,7 +117,11 @@ mod utils_tests { sunrise_core::State::try_deserialize(&mut &bytes[..]).unwrap() } - pub fn create_mock_account_info<'info, T: AccountSerialize + AccountDeserialize + Clone>(data: &'info T, owner: &'info Pubkey, key: &'info Pubkey) -> AccountInfo<'info> { + pub fn create_mock_account_info<'info, T: AccountSerialize + AccountDeserialize + Clone>( + data: &'info T, + owner: &'info Pubkey, + key: &'info Pubkey, + ) -> AccountInfo<'info> { // Serialize T into a byte vector let mut data_vec = Vec::new(); data.try_serialize(&mut data_vec).unwrap(); @@ -121,9 +134,7 @@ mod utils_tests { let data_ref = Rc::new(RefCell::new(static_ref)); // Get a mutable reference to the lamports storage (with a fixed dummy value) - let lamports = unsafe { - Rc::new(RefCell::new(&mut LAMPORTS_STORAGE)) - }; + let lamports = unsafe { Rc::new(RefCell::new(&mut LAMPORTS_STORAGE)) }; // Create the AccountInfo AccountInfo { @@ -138,7 +149,10 @@ mod utils_tests { } } - fn create_and_register_beam_state(sunrise_state: &mut sunrise_core::State, gsol_supply: u64 ) -> Result<(State, Pubkey)> { + fn create_and_register_beam_state( + sunrise_state: &mut sunrise_core::State, + gsol_supply: u64, + ) -> Result<(State, Pubkey)> { let beam_key = Pubkey::new_unique(); // add the beam to the core state @@ -152,7 +166,6 @@ mod utils_tests { .extend(std::iter::repeat(BeamDetails::default()).take(1)); sunrise_state.add_beam(beam_details)?; - let beam_state = State::default(); Ok((beam_state, beam_key)) @@ -188,21 +201,33 @@ mod utils_tests { // 60 pool tokens are worth 60 * 1.029345 = 61.7607 lamports // 50 lamports are in the beam, so the extractable yield is 61.7607 - 50 = 11.7607, rounded down to 11. #[case::accrued_value(60, 50, 11)] - fn test_calculate_extractable_yield(#[case] pool_value: u64, #[case] issued_gsol: u64, #[case] expected_extractable_yield: u64) -> Result<()> { + fn test_calculate_extractable_yield( + #[case] pool_value: u64, + #[case] issued_gsol: u64, + #[case] expected_extractable_yield: u64, + ) -> Result<()> { let mut sunrise_state = create_sunrise_state(); let stake_pool = create_stake_pool(); // create a beam and register it against the sunrise state with the given issued_gsol (the amount of sol staked in the beam) - let (beam_state, beam_key) = create_and_register_beam_state(&mut sunrise_state, issued_gsol)?; + let (beam_state, beam_key) = + create_and_register_beam_state(&mut sunrise_state, issued_gsol)?; let beam_state_account_info = create_mock_account_info(&beam_state, &crate::ID, &beam_key); let beam_state_account = Account::try_from(&beam_state_account_info)?; // create a token account for the stake pool token vault with the given pool_value (the amount of pool tokens owned by the beam) - let pool_token_vault = clone_token_account_with_amount(&TokenAccount::default(), pool_value)?; - - let extractable_yield = calculate_extractable_yield(&sunrise_state, &beam_state_account, &stake_pool, &pool_token_vault).unwrap(); + let pool_token_vault = + clone_token_account_with_amount(&TokenAccount::default(), pool_value)?; + + let extractable_yield = calculate_extractable_yield( + &sunrise_state, + &beam_state_account, + &stake_pool, + &pool_token_vault, + ) + .unwrap(); assert_eq!(extractable_yield, expected_extractable_yield); Ok(()) } -} \ No newline at end of file +} diff --git a/programs/sunrise-core/src/instructions/burn_gsol.rs b/programs/sunrise-core/src/instructions/burn_gsol.rs index deedf24..beaae65 100644 --- a/programs/sunrise-core/src/instructions/burn_gsol.rs +++ b/programs/sunrise-core/src/instructions/burn_gsol.rs @@ -16,7 +16,11 @@ pub fn handler(ctx: Context, amount_in_lamports: u64) -> Result<()> { // Can't burn more gsol than this beam is responsible for. if details.partial_gsol_supply < amount { - msg!("Beam supply {}, requested burn {}", details.partial_gsol_supply, amount); + msg!( + "Beam supply {}, requested burn {}", + details.partial_gsol_supply, + amount + ); return Err(BeamError::BurnWindowExceeded.into()); } diff --git a/programs/sunrise-core/src/lib.rs b/programs/sunrise-core/src/lib.rs index cdbc165..4a62f25 100644 --- a/programs/sunrise-core/src/lib.rs +++ b/programs/sunrise-core/src/lib.rs @@ -13,7 +13,9 @@ use anchor_spl::token::{Mint, Token, TokenAccount}; use instructions::*; use seeds::*; -pub use state::{AllocationUpdate, EpochReport, RegisterStateInput, State, UpdateStateInput, BeamDetails}; +pub use state::{ + AllocationUpdate, BeamDetails, EpochReport, RegisterStateInput, State, UpdateStateInput, +}; declare_id!("suncPB4RR39bMwnRhCym6ZLKqMfnFG83vjzVVuXNhCq"); From caa212d6d014c5ee25e9d3d5ddafd3731fab35b5 Mon Sep 17 00:00:00 2001 From: dankelleher Date: Tue, 23 Jan 2024 17:02:51 +0100 Subject: [PATCH 07/10] SPL Stake Pool extract yield to stake account. Test complete --- packages/sdks/common-marinade/package.json | 2 +- packages/sdks/common/package.json | 2 +- packages/sdks/common/src/types/spl_beam.ts | 40 ++++++++- packages/sdks/core/package.json | 2 +- packages/sdks/marinade-lp/package.json | 2 +- packages/sdks/marinade-sp/package.json | 2 +- packages/sdks/spl/package.json | 2 +- packages/sdks/spl/src/index.ts | 1 + .../sdks/sunrise-stake-client/package.json | 2 +- .../functional/beams/spl-stake-pool.test.ts | 39 ++------- packages/tests/src/utils.ts | 13 ++- programs/spl-beam/src/cpi_interface/spl.rs | 20 ++++- .../src/cpi_interface/stake_account.rs | 86 +++++++++++++++++-- programs/spl-beam/src/lib.rs | 22 ++++- programs/spl-beam/src/utils.rs | 6 +- 15 files changed, 186 insertions(+), 55 deletions(-) diff --git a/packages/sdks/common-marinade/package.json b/packages/sdks/common-marinade/package.json index 5a0e675..8fdae63 100644 --- a/packages/sdks/common-marinade/package.json +++ b/packages/sdks/common-marinade/package.json @@ -16,7 +16,7 @@ "dist" ], "scripts": { - "build": "rm -r dist; tsc --outDir dist/esm; tsc --module commonjs --moduleResolution node --outDir dist/cjs && yarn typedoc", + "build": "rm -rf dist; tsc --outDir dist/esm; tsc --module commonjs --moduleResolution node --outDir dist/cjs && yarn typedoc", "lint": "tsc --noEmit && eslint -c ../../../.eslintrc.yaml --ext .ts,.tsx src" }, "dependencies": { diff --git a/packages/sdks/common/package.json b/packages/sdks/common/package.json index 8c5fd06..0aaf4a9 100644 --- a/packages/sdks/common/package.json +++ b/packages/sdks/common/package.json @@ -16,7 +16,7 @@ "dist" ], "scripts": { - "build": "rm -r dist; tsc --outDir dist/esm; tsc --module commonjs --moduleResolution node --outDir dist/cjs && yarn typedoc", + "build": "rm -rf dist; tsc --outDir dist/esm; tsc --module commonjs --moduleResolution node --outDir dist/cjs && yarn typedoc", "lint": "tsc --noEmit && eslint -c ../../../.eslintrc.yaml --ext .ts,.tsx src" }, "dependencies": { diff --git a/packages/sdks/common/src/types/spl_beam.ts b/packages/sdks/common/src/types/spl_beam.ts index 7f26264..b7d3474 100644 --- a/packages/sdks/common/src/types/spl_beam.ts +++ b/packages/sdks/common/src/types/spl_beam.ts @@ -669,7 +669,10 @@ export type SplBeam = { { "name": "newStakeAccount", "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "The uninitialized new stake account. Will be initialised by CPI to the SPL StakePool program." + ] }, { "name": "vaultAuthority", @@ -711,6 +714,11 @@ export type SplBeam = { "isMut": false, "isSigner": false }, + { + "name": "sysvarStakeHistory", + "isMut": false, + "isSigner": false + }, { "name": "sunriseProgram", "isMut": false, @@ -815,6 +823,16 @@ export type SplBeam = { "code": 6002, "name": "Unimplemented", "msg": "This feature is unimplemented for this beam" + }, + { + "code": 6003, + "name": "YieldStakeAccountNotCooledDown", + "msg": "The yield stake account cannot yet be claimed" + }, + { + "code": 6004, + "name": "InsufficientYieldToExtract", + "msg": "The yield being extracted is insufficient to cover the rent of the stake account" } ] }; @@ -1490,7 +1508,10 @@ export const IDL: SplBeam = { { "name": "newStakeAccount", "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "The uninitialized new stake account. Will be initialised by CPI to the SPL StakePool program." + ] }, { "name": "vaultAuthority", @@ -1532,6 +1553,11 @@ export const IDL: SplBeam = { "isMut": false, "isSigner": false }, + { + "name": "sysvarStakeHistory", + "isMut": false, + "isSigner": false + }, { "name": "sunriseProgram", "isMut": false, @@ -1636,6 +1662,16 @@ export const IDL: SplBeam = { "code": 6002, "name": "Unimplemented", "msg": "This feature is unimplemented for this beam" + }, + { + "code": 6003, + "name": "YieldStakeAccountNotCooledDown", + "msg": "The yield stake account cannot yet be claimed" + }, + { + "code": 6004, + "name": "InsufficientYieldToExtract", + "msg": "The yield being extracted is insufficient to cover the rent of the stake account" } ] }; diff --git a/packages/sdks/core/package.json b/packages/sdks/core/package.json index 230c528..ff72de1 100644 --- a/packages/sdks/core/package.json +++ b/packages/sdks/core/package.json @@ -16,7 +16,7 @@ "dist" ], "scripts": { - "build": "rm -r dist; tsc --outDir dist/esm; tsc --module commonjs --moduleResolution node --outDir dist/cjs && yarn typedoc", + "build": "rm -rf dist; tsc --outDir dist/esm; tsc --module commonjs --moduleResolution node --outDir dist/cjs && yarn typedoc", "lint": "tsc --noEmit && eslint -c ../../../.eslintrc.yaml --ext .ts,.tsx src" }, "dependencies": { diff --git a/packages/sdks/marinade-lp/package.json b/packages/sdks/marinade-lp/package.json index 7db2e39..300578d 100644 --- a/packages/sdks/marinade-lp/package.json +++ b/packages/sdks/marinade-lp/package.json @@ -16,7 +16,7 @@ "dist" ], "scripts": { - "build": "rm -r dist; tsc --outDir dist/esm; tsc --module commonjs --moduleResolution node --outDir dist/cjs && yarn typedoc", + "build": "rm -rf dist; tsc --outDir dist/esm; tsc --module commonjs --moduleResolution node --outDir dist/cjs && yarn typedoc", "lint": "tsc --noEmit && eslint -c ../../../.eslintrc.yaml --ext .ts,.tsx src" }, "dependencies": { diff --git a/packages/sdks/marinade-sp/package.json b/packages/sdks/marinade-sp/package.json index 28b136d..9f2c8b0 100644 --- a/packages/sdks/marinade-sp/package.json +++ b/packages/sdks/marinade-sp/package.json @@ -16,7 +16,7 @@ "dist" ], "scripts": { - "build": "rm -r dist; tsc --outDir dist/esm; tsc --module commonjs --moduleResolution node --outDir dist/cjs && yarn typedoc", + "build": "rm -rf dist; tsc --outDir dist/esm; tsc --module commonjs --moduleResolution node --outDir dist/cjs && yarn typedoc", "lint": "tsc --noEmit && eslint -c ../../../.eslintrc.yaml --ext .ts,.tsx src" }, "dependencies": { diff --git a/packages/sdks/spl/package.json b/packages/sdks/spl/package.json index 7a531c4..6be76af 100644 --- a/packages/sdks/spl/package.json +++ b/packages/sdks/spl/package.json @@ -16,7 +16,7 @@ "dist" ], "scripts": { - "build": "rm -r dist; tsc --outDir dist/esm; tsc --module commonjs --moduleResolution node --outDir dist/cjs && yarn typedoc", + "build": "rm -rf dist; tsc --outDir dist/esm; tsc --module commonjs --moduleResolution node --outDir dist/cjs && yarn typedoc", "lint": "tsc --noEmit && eslint -c ../../../.eslintrc.yaml --ext .ts,.tsx src" }, "dependencies": { diff --git a/packages/sdks/spl/src/index.ts b/packages/sdks/spl/src/index.ts index 419b072..4c91db5 100644 --- a/packages/sdks/spl/src/index.ts +++ b/packages/sdks/spl/src/index.ts @@ -449,6 +449,7 @@ export class SplClient extends BeamInterface { managerFeeAccount: this.spl.stakePoolState.managerFeeAccount, sysvarClock: SYSVAR_CLOCK_PUBKEY, nativeStakeProgram: StakeProgram.programId, + sysvarStakeHistory: SYSVAR_STAKE_HISTORY_PUBKEY, sunriseProgram: this.sunrise.program.programId, splStakePoolProgram: SPL_STAKE_POOL_PROGRAM_ID, systemProgram: SystemProgram.programId, diff --git a/packages/sdks/sunrise-stake-client/package.json b/packages/sdks/sunrise-stake-client/package.json index ab64ba5..0ae84c5 100644 --- a/packages/sdks/sunrise-stake-client/package.json +++ b/packages/sdks/sunrise-stake-client/package.json @@ -16,7 +16,7 @@ "dist" ], "scripts": { - "build": "rm -r dist; tsc --outDir dist/esm; tsc --module commonjs --moduleResolution node --outDir dist/cjs && yarn typedoc", + "build": "rm -rf dist; tsc --outDir dist/esm; tsc --module commonjs --moduleResolution node --outDir dist/cjs && yarn typedoc", "lint": "tsc --noEmit && eslint -c ../../../.eslintrc.yaml --ext .ts,.tsx src" }, "dependencies": { 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 0242720..b7d00e0 100644 --- a/packages/tests/src/functional/beams/spl-stake-pool.test.ts +++ b/packages/tests/src/functional/beams/spl-stake-pool.test.ts @@ -6,13 +6,12 @@ import { SplClient } from "@sunrisestake/beams-spl"; import { Keypair, LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js"; import BN from "bn.js"; import { - expectStakeAccountBalance, + expectSolBalance, expectTokenBalance, fund, registerSunriseState, sendAndConfirmTransaction, tokenAccountBalance, - waitForNextEpoch, } from "../../utils.js"; import { provider, staker, stakerIdentity } from "../setup.js"; import { expect } from "chai"; @@ -205,8 +204,6 @@ describe("SPL stake pool beam", () => { 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 @@ -214,39 +211,19 @@ describe("SPL stake pool beam", () => { await beamClient.extractYield(), ); - // we burned `burnAmount` gsol, so we should have `burnAmount` - fee in the stake account + // 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 expectedStakeAmount = burnAmount.sub(expectedFee); - - // there is no yield yet, but we have created a stake account for the yield - await expectStakeAccountBalance( - beamClient.provider, - beamClient.yieldStakeAccount, - expectedStakeAmount, - 1, - ); - }); - - it.skip("can claim stake account into yield account after cooldown", async () => { - // wait an epoch for the stake account to cool down - await waitForNextEpoch(beamClient.provider); + const expectedExtractedYield = burnAmount.sub(expectedFee); - // TODO enable - // 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, - // // TODO move this and extract yield into "management" object to make it clear that these - // // do not need to be executed by end-users - // await beamClient.claimExtractedYieldStakeAcccount(), - // ); - - await expectTokenBalance( + await expectSolBalance( beamClient.provider, beamClient.sunrise.state.yieldAccount, - burnAmount, + 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, ); }); }); diff --git a/packages/tests/src/utils.ts b/packages/tests/src/utils.ts index 28998c3..c8c1976 100644 --- a/packages/tests/src/utils.ts +++ b/packages/tests/src/utils.ts @@ -109,8 +109,15 @@ export const expectStakerSolBalance = async ( provider: AnchorProvider, expectedAmount: number | BN, tolerance = 0, // Allow for a tolerance as the balance depends on the fees which are unstable at the beginning of a test validator +) => expectSolBalance(provider, provider.publicKey, expectedAmount, tolerance); + +export const expectSolBalance = async ( + provider: AnchorProvider, + address = provider.publicKey, + expectedAmount: number | BN, + tolerance = 0, // Allow for a tolerance as the balance depends on the fees which are unstable at the beginning of a test validator ) => { - const actualAmount = await solBalance(provider); + const actualAmount = await solBalance(provider, address); expectAmount(actualAmount, expectedAmount, tolerance); }; @@ -140,10 +147,14 @@ export const waitForNextEpoch = async (provider: AnchorProvider) => { const startSlot = startingEpoch.slotIndex; let subscriptionId = 0; + log("Waiting for epoch", nextEpoch); + await new Promise((resolve) => { subscriptionId = provider.connection.onSlotChange((slotInfo) => { + log("slot", slotInfo.slot, "startSlot", startSlot); if (slotInfo.slot % SLOTS_IN_EPOCH === 1 && slotInfo.slot > startSlot) { void provider.connection.getEpochInfo().then((currentEpoch) => { + log("currentEpoch", currentEpoch); if (currentEpoch.epoch === nextEpoch) { resolve(slotInfo.slot); } diff --git a/programs/spl-beam/src/cpi_interface/spl.rs b/programs/spl-beam/src/cpi_interface/spl.rs index d384bd9..7ef4439 100644 --- a/programs/spl-beam/src/cpi_interface/spl.rs +++ b/programs/spl-beam/src/cpi_interface/spl.rs @@ -1,7 +1,8 @@ +use crate::constants::STAKE_ACCOUNT_SIZE; use crate::cpi_interface::stake_pool::StakePool; use crate::seeds::*; use crate::state::State; -use crate::{ExtractYield, WithdrawStake}; +use crate::{ExtractYield, SplBeamError, WithdrawStake}; use anchor_lang::{ prelude::*, solana_program::program::{invoke, invoke_signed}, @@ -222,9 +223,22 @@ pub fn extract_stake(accounts: &ExtractStakeAccount, lamports: u64) -> Result<() let state_address = accounts.state.key(); let seeds = &[state_address.as_ref(), VAULT_AUTHORITY, bump][..]; + let stake_account_rent = Rent::get()?.minimum_balance(STAKE_ACCOUNT_SIZE); + let mut total_extractable_lamports = lamports.saturating_sub(stake_account_rent); + + if total_extractable_lamports > accounts.stake_to_split.lamports() { + total_extractable_lamports = accounts.stake_to_split.lamports(); + msg!("Limiting the extraction to the amount of lamports in the reserve stake account of the pool: {}", total_extractable_lamports); + } + if total_extractable_lamports == 0 { + return Err(SplBeamError::InsufficientYieldToExtract.into()); + } + let pool = &accounts.stake_pool; - let pool_tokens = - crate::utils::pool_tokens_from_lamports(&pool.clone().into_inner(), lamports)?; + let pool_tokens = crate::utils::pool_tokens_from_lamports( + &pool.clone().into_inner(), + total_extractable_lamports, + )?; invoke_signed( &spl_stake_pool::instruction::withdraw_stake( diff --git a/programs/spl-beam/src/cpi_interface/stake_account.rs b/programs/spl-beam/src/cpi_interface/stake_account.rs index ba77fcd..762d476 100644 --- a/programs/spl-beam/src/cpi_interface/stake_account.rs +++ b/programs/spl-beam/src/cpi_interface/stake_account.rs @@ -1,5 +1,12 @@ -use anchor_lang::prelude::Pubkey; +use crate::seeds::VAULT_AUTHORITY; +use crate::state::State; +use crate::ExtractYield; +use anchor_lang::prelude::*; +use anchor_lang::solana_program::clock::Epoch; +use anchor_lang::solana_program::program::invoke_signed; use anchor_lang::solana_program::stake::program::ID; +use anchor_lang::Key; +use anchor_spl::token::spl_token::solana_program; use borsh::BorshDeserialize; use spl_stake_pool::solana_program::stake::state::StakeStateV2; use std::ops::Deref; @@ -9,19 +16,19 @@ use std::ops::Deref; #[derive(Clone)] pub struct StakeAccount(StakeStateV2); -impl anchor_lang::AccountDeserialize for StakeAccount { - fn try_deserialize(buf: &mut &[u8]) -> anchor_lang::Result { +impl AccountDeserialize for StakeAccount { + fn try_deserialize(buf: &mut &[u8]) -> Result { Self::try_deserialize_unchecked(buf) } - fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result { + fn try_deserialize_unchecked(buf: &mut &[u8]) -> Result { StakeStateV2::deserialize(buf).map(Self).map_err(Into::into) } } -impl anchor_lang::AccountSerialize for StakeAccount {} +impl AccountSerialize for StakeAccount {} -impl anchor_lang::Owner for StakeAccount { +impl Owner for StakeAccount { fn owner() -> Pubkey { ID } @@ -34,3 +41,70 @@ impl Deref for StakeAccount { &self.0 } } + +impl StakeAccount { + pub fn can_be_withdrawn(&self, current_epoch: &Epoch) -> bool { + match self.0 { + StakeStateV2::Stake(_, stake, _) => { + stake.delegation.deactivation_epoch <= *current_epoch + } + StakeStateV2::Initialized(_) => true, + _ => false, + } + } +} + +pub struct ClaimStakeAccount<'info> { + pub state: Box>, + pub stake_account: Account<'info, StakeAccount>, + pub withdrawer: AccountInfo<'info>, + pub to: AccountInfo<'info>, + pub native_stake_program: AccountInfo<'info>, + pub sysvar_clock: AccountInfo<'info>, + pub sysvar_stake_history: AccountInfo<'info>, +} +impl<'a> From> for ClaimStakeAccount<'a> { + /// Convert the ExtractYield beam instruction accounts to the ClaimStakeAccount accounts + fn from(extract_yield: ExtractYield<'a>) -> Self { + Self { + state: extract_yield.state, + stake_account: extract_yield.new_stake_account, + withdrawer: extract_yield.vault_authority.to_account_info(), + to: extract_yield.yield_account.to_account_info(), + native_stake_program: extract_yield.native_stake_program.to_account_info(), + sysvar_clock: extract_yield.sysvar_clock.to_account_info(), + sysvar_stake_history: extract_yield.sysvar_stake_history.to_account_info(), + } + } +} +impl<'a> From<&ExtractYield<'a>> for ClaimStakeAccount<'a> { + fn from(extract_yield: &ExtractYield<'a>) -> Self { + extract_yield.to_owned().into() + } +} + +pub fn claim_stake_account(accounts: &ClaimStakeAccount, lamports: u64) -> Result<()> { + let bump = &[accounts.state.vault_authority_bump][..]; + let state_address = accounts.state.key(); + let seeds = &[state_address.as_ref(), VAULT_AUTHORITY, bump][..]; + + invoke_signed( + &solana_program::stake::instruction::withdraw( + &accounts.stake_account.key(), + accounts.withdrawer.key, + accounts.to.key, + lamports, + None, + ), + &[ + accounts.stake_account.to_account_info(), + accounts.to.clone(), + accounts.sysvar_clock.clone(), + accounts.sysvar_stake_history.clone(), + accounts.withdrawer.clone(), + ], + &[seeds], + )?; + + Ok(()) +} diff --git a/programs/spl-beam/src/lib.rs b/programs/spl-beam/src/lib.rs index dc516b8..c65482d 100644 --- a/programs/spl-beam/src/lib.rs +++ b/programs/spl-beam/src/lib.rs @@ -29,6 +29,7 @@ declare_id!("EUZfY4LePXSZVMvRuiVzbxazw9yBDYU99DpGJKCthxbS"); #[program] pub mod spl_beam { use super::*; + use crate::cpi_interface::stake_account::claim_stake_account; pub fn initialize(ctx: Context, input: StateEntry) -> Result<()> { ctx.accounts.state.set_inner(input.into()); @@ -176,7 +177,18 @@ pub mod spl_beam { // CPI: Extract this yield into a new stake account. let extract_stake_account_accounts = ctx.accounts.deref().into(); - spl_interface::extract_stake(&extract_stake_account_accounts, extractable_yield) + spl_interface::extract_stake(&extract_stake_account_accounts, extractable_yield)?; + + // get the staked lamports amount + let stake_account = &mut ctx.accounts.new_stake_account; + stake_account.reload()?; + let lamports = stake_account.to_account_info().lamports(); + + // CPI: Withdraw the lamports from the stake account. + let claim_stake_account_accounts = ctx.accounts.deref().into(); + claim_stake_account(&claim_stake_account_accounts, lamports)?; + + Ok(()) } } @@ -545,7 +557,7 @@ pub struct ExtractYield<'info> { ], bump )] - /// CHECK: The uninitialized new stake account. Will be initialised by CPI to the SPL StakePool program. + /// The uninitialized new stake account. Will be initialised by CPI to the SPL StakePool program. pub new_stake_account: Account<'info, StakeAccount>, #[account( @@ -584,6 +596,8 @@ pub struct ExtractYield<'info> { pub sysvar_clock: Sysvar<'info, Clock>, pub native_stake_program: Program<'info, NativeStakeProgram>, + /// CHECK: Checked by CPI to SPL Stake program. + pub sysvar_stake_history: UncheckedAccount<'info>, pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, pub spl_stake_pool_program: Program<'info, SplStakePool>, @@ -634,4 +648,8 @@ pub enum SplBeamError { CalculationFailure, #[msg("This feature is unimplemented for this beam")] Unimplemented, + #[msg("The yield stake account cannot yet be claimed")] + YieldStakeAccountNotCooledDown, + #[msg("The yield being extracted is insufficient to cover the rent of the stake account")] + InsufficientYieldToExtract, } diff --git a/programs/spl-beam/src/utils.rs b/programs/spl-beam/src/utils.rs index fe8c73b..39beb21 100644 --- a/programs/spl-beam/src/utils.rs +++ b/programs/spl-beam/src/utils.rs @@ -1,8 +1,8 @@ use crate::cpi_interface::stake_pool::StakePool; use crate::state::State; -use anchor_lang::prelude::*; -use anchor_lang::solana_program::{ - borsh0_10::try_from_slice_unchecked, stake::state::StakeStateV2, +use anchor_lang::{ + prelude::*, solana_program::borsh0_10::try_from_slice_unchecked, + solana_program::stake::state::StakeStateV2, }; use anchor_spl::token::TokenAccount; use sunrise_core::BeamError; From 04dc9bbe458826d3bb005bd62161c63fc3ba09fa Mon Sep 17 00:00:00 2001 From: dankelleher Date: Tue, 23 Jan 2024 18:48:31 +0100 Subject: [PATCH 08/10] SPL Stake Pool extract yield to stake account. Upload test artifacts in CI --- .github/actions/cache-solana/action.yml | 2 +- .github/workflows/tests.yml | 66 +++++++++++++++---------- packages/sdks/spl/src/index.ts | 2 + packages/tests/src/utils.ts | 6 +-- 4 files changed, 47 insertions(+), 29 deletions(-) diff --git a/.github/actions/cache-solana/action.yml b/.github/actions/cache-solana/action.yml index efe9711..d8afb06 100644 --- a/.github/actions/cache-solana/action.yml +++ b/.github/actions/cache-solana/action.yml @@ -8,7 +8,7 @@ description: install and Cache Solana binaries runs: using: composite steps: - - uses: actions/cache@v3 + - uses: actions/cache@v4 id: cache-solana with: path: | diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 64ce060..91b3a74 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,16 +7,16 @@ jobs: install: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ./.github/actions/cache-solana - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: '18' - name: Cache node dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: '**/node_modules' key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} @@ -41,7 +41,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Cache rust uses: Swatinem/rust-cache@v2 - name: Run fmt @@ -53,11 +53,11 @@ jobs: needs: install runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Cache rust uses: Swatinem/rust-cache@v2 - uses: ./.github/actions/cache-solana - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: path: | ~/.cargo/registry @@ -69,8 +69,8 @@ jobs: - name: Upload build artifacts uses: actions/upload-artifact@v3 with: - name: target - path: target + name: target_deploy + path: target/deploy if-no-files-found: error retention-days: 1 @@ -79,11 +79,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Cache rust uses: Swatinem/rust-cache@v2 - uses: ./.github/actions/cache-solana - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: path: | ~/.cargo/registry @@ -99,14 +99,14 @@ jobs: needs: install runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node ${{ matrix.node }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: '18' - name: Cache node dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: '**/node_modules' key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} @@ -126,14 +126,14 @@ jobs: needs: yarn-build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node ${{ matrix.node }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: '18' - name: Cache node dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: '**/node_modules' key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} @@ -150,19 +150,19 @@ jobs: yarn lint functional-test: - needs: [clippy-lint, yarn-lint, cargo-build] + needs: [yarn-build, cargo-build] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node ${{ matrix.node }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: '18' - name: Cache node dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: '**/node_modules' key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} @@ -179,11 +179,17 @@ jobs: - name: Download Rust build artifacts uses: actions/download-artifact@v3 with: - name: target - path: . + name: target_deploy + path: ./target + + - name: Download TS build artifacts + uses: actions/download-artifact@v3 + with: + name: dists + path: packages/sdks - uses: ./.github/actions/cache-solana - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: path: | ~/.cargo/registry @@ -193,11 +199,21 @@ jobs: - name: run tests run: | ls target/deploy + cat packages/sdks/common/dist/cjs/types/spl_beam.js + cat node_modules/@sunrisestake/beams-common/dist/cjs/types/spl_beam.js #yarn anchor test --skip-build packages/tests/src/functional/beams/core.test.ts yarn test - - uses: actions/upload-artifact@v3 + - name: upload program logs + uses: actions/upload-artifact@v3 if: always() with: name: program-logs - path: .anchor/program-logs/* \ No newline at end of file + path: .anchor/program-logs/* + + - name: upload test report + uses: actions/upload-artifact@v3 + if: always() + with: + name: test-report + path: mochawesome-report/* \ No newline at end of file diff --git a/packages/sdks/spl/src/index.ts b/packages/sdks/spl/src/index.ts index 4c91db5..f5d7243 100644 --- a/packages/sdks/spl/src/index.ts +++ b/packages/sdks/spl/src/index.ts @@ -455,6 +455,8 @@ 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/utils.ts b/packages/tests/src/utils.ts index c8c1976..1699337 100644 --- a/packages/tests/src/utils.ts +++ b/packages/tests/src/utils.ts @@ -147,14 +147,14 @@ export const waitForNextEpoch = async (provider: AnchorProvider) => { const startSlot = startingEpoch.slotIndex; let subscriptionId = 0; - log("Waiting for epoch", nextEpoch); + logAtLevel("info")("Waiting for epoch", nextEpoch); await new Promise((resolve) => { subscriptionId = provider.connection.onSlotChange((slotInfo) => { - log("slot", slotInfo.slot, "startSlot", startSlot); + logAtLevel("trace")("slot", slotInfo.slot, "startSlot", startSlot); if (slotInfo.slot % SLOTS_IN_EPOCH === 1 && slotInfo.slot > startSlot) { void provider.connection.getEpochInfo().then((currentEpoch) => { - log("currentEpoch", currentEpoch); + logAtLevel("trace")("currentEpoch", currentEpoch); if (currentEpoch.epoch === nextEpoch) { resolve(slotInfo.slot); } From 919cd0b5e62e8f2b83ebb0d9d1dd19da9f8c70ee Mon Sep 17 00:00:00 2001 From: dankelleher Date: Thu, 25 Jan 2024 13:32:13 +0100 Subject: [PATCH 09/10] SPL Stake Pool extract yield to stake account. Add support to update the epoch report on yield extraction --- .github/workflows/tests.yml | 7 +-- packages/sdks/common/src/types/spl_beam.ts | 48 +++++++++++++++---- .../sdks/common/src/types/sunrise_core.ts | 8 ++-- packages/sdks/core/src/index.ts | 1 + packages/sdks/spl/src/index.ts | 4 ++ .../src/functional/beams/marinade-sp.test.ts | 43 +++++++++++++++++ .../spl-beam/src/cpi_interface/sunrise.rs | 39 ++++++++++++++- programs/spl-beam/src/lib.rs | 28 ++++++++--- .../src/instructions/extract_yield.rs | 4 +- programs/sunrise-core/src/lib.rs | 4 +- 10 files changed, 154 insertions(+), 32 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 91b3a74..1bf70b6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -197,12 +197,7 @@ jobs: target key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('rust-toolchain.toml') }} - name: run tests - run: | - ls target/deploy - cat packages/sdks/common/dist/cjs/types/spl_beam.js - cat node_modules/@sunrisestake/beams-common/dist/cjs/types/spl_beam.js - #yarn anchor test --skip-build packages/tests/src/functional/beams/core.test.ts - yarn test + run: yarn test - name: upload program logs uses: actions/upload-artifact@v3 diff --git a/packages/sdks/common/src/types/spl_beam.ts b/packages/sdks/common/src/types/spl_beam.ts index b7d3474..8237497 100644 --- a/packages/sdks/common/src/types/spl_beam.ts +++ b/packages/sdks/common/src/types/spl_beam.ts @@ -83,7 +83,7 @@ export type SplBeam = { "accounts": [ { "name": "state", - "isMut": true, + "isMut": false, "isSigner": false }, { @@ -187,7 +187,7 @@ export type SplBeam = { "accounts": [ { "name": "state", - "isMut": true, + "isMut": false, "isSigner": false }, { @@ -321,7 +321,7 @@ export type SplBeam = { "accounts": [ { "name": "state", - "isMut": true, + "isMut": false, "isSigner": false }, { @@ -440,7 +440,7 @@ export type SplBeam = { "accounts": [ { "name": "state", - "isMut": true, + "isMut": false, "isSigner": false }, { @@ -504,7 +504,7 @@ export type SplBeam = { "accounts": [ { "name": "state", - "isMut": true, + "isMut": false, "isSigner": false }, { @@ -704,6 +704,15 @@ 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, @@ -719,6 +728,11 @@ export type SplBeam = { "isMut": false, "isSigner": false }, + { + "name": "sysvarInstructions", + "isMut": false, + "isSigner": false + }, { "name": "sunriseProgram", "isMut": false, @@ -922,7 +936,7 @@ export const IDL: SplBeam = { "accounts": [ { "name": "state", - "isMut": true, + "isMut": false, "isSigner": false }, { @@ -1026,7 +1040,7 @@ export const IDL: SplBeam = { "accounts": [ { "name": "state", - "isMut": true, + "isMut": false, "isSigner": false }, { @@ -1160,7 +1174,7 @@ export const IDL: SplBeam = { "accounts": [ { "name": "state", - "isMut": true, + "isMut": false, "isSigner": false }, { @@ -1279,7 +1293,7 @@ export const IDL: SplBeam = { "accounts": [ { "name": "state", - "isMut": true, + "isMut": false, "isSigner": false }, { @@ -1343,7 +1357,7 @@ export const IDL: SplBeam = { "accounts": [ { "name": "state", - "isMut": true, + "isMut": false, "isSigner": false }, { @@ -1543,6 +1557,15 @@ 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, @@ -1558,6 +1581,11 @@ export const IDL: SplBeam = { "isMut": false, "isSigner": false }, + { + "name": "sysvarInstructions", + "isMut": false, + "isSigner": false + }, { "name": "sunriseProgram", "isMut": false, diff --git a/packages/sdks/common/src/types/sunrise_core.ts b/packages/sdks/common/src/types/sunrise_core.ts index 62c2a13..af05d75 100644 --- a/packages/sdks/common/src/types/sunrise_core.ts +++ b/packages/sdks/common/src/types/sunrise_core.ts @@ -400,12 +400,12 @@ export type SunriseCore = { ] }, { - "name": "clock", + "name": "sysvarClock", "isMut": false, "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false } @@ -1136,12 +1136,12 @@ export const IDL: SunriseCore = { ] }, { - "name": "clock", + "name": "sysvarClock", "isMut": false, "isSigner": false }, { - "name": "instructionsSysvar", + "name": "sysvarInstructions", "isMut": false, "isSigner": false } diff --git a/packages/sdks/core/src/index.ts b/packages/sdks/core/src/index.ts index ef0d95e..ca2f632 100644 --- a/packages/sdks/core/src/index.ts +++ b/packages/sdks/core/src/index.ts @@ -257,6 +257,7 @@ 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, diff --git a/packages/sdks/spl/src/index.ts b/packages/sdks/spl/src/index.ts index f5d7243..6315f45 100644 --- a/packages/sdks/spl/src/index.ts +++ b/packages/sdks/spl/src/index.ts @@ -5,6 +5,7 @@ import { StakeProgram, SystemProgram, SYSVAR_CLOCK_PUBKEY, + SYSVAR_INSTRUCTIONS_PUBKEY, SYSVAR_STAKE_HISTORY_PUBKEY, Transaction, type TransactionInstruction, @@ -434,6 +435,7 @@ export class SplClient extends BeamInterface { this.program.programId, this.stateAddress, ); + const accounts = { state: this.stateAddress, stakePool: this.spl.stakePoolAddress, @@ -447,9 +449,11 @@ 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, + sysvarInstructions: SYSVAR_INSTRUCTIONS_PUBKEY, sunriseProgram: this.sunrise.program.programId, splStakePoolProgram: SPL_STAKE_POOL_PROGRAM_ID, systemProgram: SystemProgram.programId, diff --git a/packages/tests/src/functional/beams/marinade-sp.test.ts b/packages/tests/src/functional/beams/marinade-sp.test.ts index 6605fd7..457fd48 100644 --- a/packages/tests/src/functional/beams/marinade-sp.test.ts +++ b/packages/tests/src/functional/beams/marinade-sp.test.ts @@ -41,6 +41,7 @@ describe("Marinade stake pool beam", () => { 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); before("Set up the sunrise state", async () => { coreClient = await registerSunriseState(); @@ -296,4 +297,46 @@ describe("Marinade stake pool beam", () => { 100, ); }); + + // + // 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, + // ); + // }); }); diff --git a/programs/spl-beam/src/cpi_interface/sunrise.rs b/programs/spl-beam/src/cpi_interface/sunrise.rs index c742a81..29d32c2 100644 --- a/programs/spl-beam/src/cpi_interface/sunrise.rs +++ b/programs/spl-beam/src/cpi_interface/sunrise.rs @@ -2,8 +2,8 @@ use crate::seeds::*; use anchor_lang::prelude::*; 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}, + burn_gsol as cpi_burn_gsol, extract_yield as cpi_extract_yield, mint_gsol as cpi_mint_gsol, }; pub fn mint_gsol<'a>( @@ -120,3 +120,38 @@ impl<'a> From<&crate::Burn<'a>> for BurnGsol<'a> { } } } + +pub fn extract_yield<'a>( + accounts: impl Into>, + cpi_program: AccountInfo<'a>, + sunrise_key: Pubkey, + stake_pool: Pubkey, + state_bump: u8, + lamports: u64, +) -> Result<()> { + let accounts: ExtractYield<'a> = accounts.into(); + let seeds = [ + STATE, + sunrise_key.as_ref(), + stake_pool.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(), + epoch_report: accounts.epoch_report.to_account_info(), + sysvar_clock: accounts.sysvar_clock.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 c65482d..ec07df5 100644 --- a/programs/spl-beam/src/lib.rs +++ b/programs/spl-beam/src/lib.rs @@ -188,6 +188,17 @@ pub mod spl_beam { let claim_stake_account_accounts = ctx.accounts.deref().into(); claim_stake_account(&claim_stake_account_accounts, 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(), + ctx.accounts.stake_pool.key(), + state_bump, + lamports, + )?; + Ok(()) } } @@ -238,7 +249,6 @@ pub struct Update<'info> { #[derive(Accounts)] pub struct Deposit<'info> { #[account( - mut, has_one = sunrise_state, has_one = stake_pool, seeds = [STATE, sunrise_state.key().as_ref(), stake_pool.key().as_ref()], @@ -303,7 +313,6 @@ pub struct Deposit<'info> { #[derive(Accounts)] pub struct DepositStake<'info> { #[account( - mut, has_one = sunrise_state, has_one = stake_pool, seeds = [STATE, sunrise_state.key().as_ref()], @@ -382,7 +391,6 @@ pub struct DepositStake<'info> { #[derive(Accounts)] pub struct Withdraw<'info> { #[account( - mut, has_one = sunrise_state, has_one = stake_pool, seeds = [STATE, sunrise_state.key().as_ref(), stake_pool.key().as_ref()], @@ -449,7 +457,6 @@ pub struct Withdraw<'info> { #[derive(Accounts)] pub struct WithdrawStake<'info> { #[account( - mut, has_one = sunrise_state, has_one = stake_pool, seeds = [STATE, sunrise_state.key().as_ref()], @@ -526,7 +533,9 @@ pub struct WithdrawStake<'info> { pub struct ExtractYield<'info> { #[account( has_one = stake_pool, - has_one = sunrise_state + has_one = sunrise_state, + seeds = [STATE, sunrise_state.key().as_ref(), stake_pool.key().as_ref()], + bump )] pub state: Box>, #[account( @@ -594,10 +603,18 @@ 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. pub sysvar_stake_history: UncheckedAccount<'info>, + /// CHECK: Checked by CPI to Sunrise. + pub sysvar_instructions: UncheckedAccount<'info>, pub sunrise_program: Program<'info, sunrise_core_cpi::program::SunriseCore>, pub spl_stake_pool_program: Program<'info, SplStakePool>, @@ -609,7 +626,6 @@ pub struct ExtractYield<'info> { #[derive(Accounts)] pub struct Burn<'info> { #[account( - mut, has_one = sunrise_state, has_one = stake_pool, seeds = [STATE, sunrise_state.key().as_ref(), stake_pool.key().as_ref()], diff --git a/programs/sunrise-core/src/instructions/extract_yield.rs b/programs/sunrise-core/src/instructions/extract_yield.rs index 37da4bb..e07670e 100644 --- a/programs/sunrise-core/src/instructions/extract_yield.rs +++ b/programs/sunrise-core/src/instructions/extract_yield.rs @@ -9,11 +9,11 @@ use crate::{system, utils, BeamError, ExtractYield}; 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.clock.epoch; + let current_epoch = ctx.accounts.sysvar_clock.epoch; // Check that the executing program is valid. let cpi_program = - utils::get_cpi_program_id(&ctx.accounts.instructions_sysvar.to_account_info())?; + utils::get_cpi_program_id(&ctx.accounts.sysvar_instructions.to_account_info())?; system::check_beam_validity(state, &ctx.accounts.beam, &cpi_program)?; // The epoch report must be already updated for this epoch diff --git a/programs/sunrise-core/src/lib.rs b/programs/sunrise-core/src/lib.rs index 4a62f25..e685bcd 100644 --- a/programs/sunrise-core/src/lib.rs +++ b/programs/sunrise-core/src/lib.rs @@ -333,11 +333,11 @@ pub struct ExtractYield<'info> { )] pub epoch_report: Account<'info, EpochReport>, - pub clock: Sysvar<'info, Clock>, + pub sysvar_clock: Sysvar<'info, Clock>, /// CHECK: Verified Instructions Sysvar. #[account(address = sysvar::instructions::ID)] - pub instructions_sysvar: UncheckedAccount<'info>, + pub sysvar_instructions: UncheckedAccount<'info>, } #[error_code] From 63414379b281670a8e8f6f6ceec758873c77e469 Mon Sep 17 00:00:00 2001 From: dankelleher Date: Thu, 25 Jan 2024 15:45:21 +0100 Subject: [PATCH 10/10] Check hashes of deployed programs in CI --- .github/workflows/tests.yml | 44 ++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1bf70b6..916d115 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -52,6 +52,8 @@ jobs: cargo-build: needs: install runs-on: ubuntu-latest + outputs: + target_deploy_artifact: ${{ steps.build-artifact-upload-step.outputs.artifact-id }} steps: - uses: actions/checkout@v4 - name: Cache rust @@ -64,10 +66,13 @@ jobs: ~/.cargo/git target key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('rust-toolchain.toml') }} + - name: Build all programs run: cargo build-sbf + - name: Upload build artifacts - uses: actions/upload-artifact@v3 + id: build-artifact-upload-step + uses: actions/upload-artifact@v4 with: name: target_deploy path: target/deploy @@ -115,7 +120,7 @@ jobs: run: yarn build - name: Upload build artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: dists path: packages/sdks/*/dist @@ -139,7 +144,7 @@ jobs: key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} - name: Download build artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: dists path: packages/sdks @@ -161,6 +166,15 @@ jobs: with: node-version: '18' + - uses: ./.github/actions/cache-solana + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('rust-toolchain.toml') }} + - name: Cache node dependencies uses: actions/cache@v4 with: @@ -171,43 +185,29 @@ jobs: run: yarn --frozen-lockfile - name: Download JS build artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: dists path: packages/sdks - name: Download Rust build artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: target_deploy - path: ./target + path: ./target/deploy - - name: Download TS build artifacts - uses: actions/download-artifact@v3 - with: - name: dists - path: packages/sdks - - - uses: ./.github/actions/cache-solana - - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('rust-toolchain.toml') }} - name: run tests run: yarn test - name: upload program logs - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: always() with: name: program-logs path: .anchor/program-logs/* - name: upload test report - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: always() with: name: test-report