Skip to content

Commit

Permalink
Marinade LP: Extract yield
Browse files Browse the repository at this point in the history
  • Loading branch information
dankelleher committed Feb 29, 2024
1 parent 0e657b2 commit 0da9b70
Show file tree
Hide file tree
Showing 20 changed files with 244 additions and 190 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion lib/marinade-common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ edition = "2021"

[dependencies]
anchor-lang = '0.29.0'
marinade-cpi = { git = "https://github.com/sunrise-stake/anchor-gen", branch = "update/anchor-v0.29" }
marinade-cpi = { git = "https://github.com/sunrise-stake/anchor-gen", branch = "update/anchor-v0.29" }
num-traits = "0.2.17"
72 changes: 47 additions & 25 deletions lib/marinade-common/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,46 +1,68 @@
pub mod vault_authority_seed;

use anchor_lang::prelude::*;
use marinade_cpi::state::State as MarinadeState;
use std::num::TryFromIntError;
use num_traits::{NumCast, PrimInt};
use std::fmt::Debug;
use std::ops::{Div, Mul};

/// calculate amount*numerator/denominator
/// as value = shares * share_price where share_price=total_value/total_shares
/// or shares = amount_value / share_price where share_price=total_value/total_shares
/// => shares = amount_value * 1/share_price where 1/share_price=total_shares/total_value
pub fn proportional(
amount: u64,
numerator: u64,
denominator: u64,
) -> std::result::Result<u64, TryFromIntError> {
if denominator == 0 {
return Ok(amount);
pub fn proportional<T>(amount: T, numerator: T, denominator: T) -> T
where
T: PrimInt + Mul<Output = T> + Div<Output = T> + TryInto<i128>,
<T as TryInto<i128>>::Error: Debug,
{
proportional_with_rounding(amount, numerator, denominator, RoundingMode::Down)
}

pub enum RoundingMode {
Up,
Down,
}

pub fn proportional_with_rounding<T>(
amount: T,
numerator: T,
denominator: T,
rounding_mode: RoundingMode,
) -> T
where
T: PrimInt + Mul<Output = T> + Div<Output = T> + TryInto<i128>,
<T as TryInto<i128>>::Error: Debug,
{
if denominator == T::zero() {
return amount;
}

let amount_i128: i128 = amount.try_into().unwrap();
let numerator_i128: i128 = numerator.try_into().unwrap();
let denominator_i128: i128 = denominator.try_into().unwrap();

match rounding_mode {
RoundingMode::Up => {
// Round up by adding (denominator - 1) before dividing
let result = (amount_i128 * numerator_i128 + (denominator_i128 - 1)) / denominator_i128;
<T as NumCast>::from(result).unwrap()
}
RoundingMode::Down => {
// Default behavior (round down)
let result = amount_i128 * numerator_i128 / denominator_i128;
<T as NumCast>::from(result).unwrap()
}
}
u64::try_from((amount as u128) * (numerator as u128) / (denominator as u128))
}

// All lifted from https://github.com/marinade-finance/liquid-staking-program/blob/447f9607a8c755cac7ad63223febf047142c6c8f/programs/marinade-finance/src/state.rs#L227
pub fn calc_msol_from_lamports(
marinade_state: &MarinadeState,
stake_lamports: u64,
) -> std::result::Result<u64, TryFromIntError> {
msg!("calc_msol_from_lamports");
msg!("stake_lamports: {}", stake_lamports);
msg!("marinade_state.msol_supply: {}", marinade_state.msol_supply);
msg!(
"total_virtual_staked_lamports: {}",
total_virtual_staked_lamports(marinade_state)
);
pub fn calc_msol_from_lamports(marinade_state: &MarinadeState, stake_lamports: u64) -> u64 {
proportional(
stake_lamports,
marinade_state.msol_supply,
total_virtual_staked_lamports(marinade_state),
)
}
pub fn calc_lamports_from_msol_amount(
marinade_state: &MarinadeState,
msol_amount: u64,
) -> std::result::Result<u64, TryFromIntError> {
pub fn calc_lamports_from_msol_amount(marinade_state: &MarinadeState, msol_amount: u64) -> u64 {
proportional(
msol_amount,
total_virtual_staked_lamports(marinade_state),
Expand Down
22 changes: 6 additions & 16 deletions packages/sdks/common/src/types/marinade_beam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -884,26 +884,21 @@ export type MarinadeBeam = {
},
{
"code": 6001,
"name": "CalculationFailure",
"msg": "An error occurred during calculation"
},
{
"code": 6002,
"name": "InvalidEpochReportAccount",
"msg": "The epoch report account has not been updated to the current epoch yet"
},
{
"code": 6003,
"code": 6002,
"name": "RemainingUnclaimableTicketAmount",
"msg": "The total ordered ticket amount exceeds the amount in all found tickets"
},
{
"code": 6004,
"code": 6003,
"name": "DelayedUnstakeTicketsNotYetClaimable",
"msg": "Delayed unstake tickets for the current epoch can not yet be claimed"
},
{
"code": 6005,
"code": 6004,
"name": "TooManyTicketsClaimed",
"msg": "The amount of delayed unstake tickets requested to be recovered exceeds the amount in the report"
}
Expand Down Expand Up @@ -1796,26 +1791,21 @@ export const IDL: MarinadeBeam = {
},
{
"code": 6001,
"name": "CalculationFailure",
"msg": "An error occurred during calculation"
},
{
"code": 6002,
"name": "InvalidEpochReportAccount",
"msg": "The epoch report account has not been updated to the current epoch yet"
},
{
"code": 6003,
"code": 6002,
"name": "RemainingUnclaimableTicketAmount",
"msg": "The total ordered ticket amount exceeds the amount in all found tickets"
},
{
"code": 6004,
"code": 6003,
"name": "DelayedUnstakeTicketsNotYetClaimable",
"msg": "Delayed unstake tickets for the current epoch can not yet be claimed"
},
{
"code": 6005,
"code": 6004,
"name": "TooManyTicketsClaimed",
"msg": "The amount of delayed unstake tickets requested to be recovered exceeds the amount in the report"
}
Expand Down
10 changes: 10 additions & 0 deletions packages/sdks/common/src/types/marinade_lp_beam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,11 @@ export type MarinadeLpBeam = {
"code": 6001,
"name": "Unimplemented",
"msg": "This feature is unimplemented for this beam"
},
{
"code": 6002,
"name": "InsufficientYieldBalance",
"msg": "The yield balance is insufficient to extract yield"
}
]
};
Expand Down Expand Up @@ -1282,6 +1287,11 @@ export const IDL: MarinadeLpBeam = {
"code": 6001,
"name": "Unimplemented",
"msg": "This feature is unimplemented for this beam"
},
{
"code": 6002,
"name": "InsufficientYieldBalance",
"msg": "The yield balance is insufficient to extract yield"
}
]
};
25 changes: 8 additions & 17 deletions packages/sdks/marinade-lp/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
Keypair,
PublicKey,
SystemProgram,
SYSVAR_CLOCK_PUBKEY,
SYSVAR_INSTRUCTIONS_PUBKEY,
Transaction,
type TransactionInstruction,
Expand All @@ -18,6 +17,7 @@ import {
BeamInterface,
deriveAuthorityAddress,
MarinadeLpBeam,
requestIncreasedCUsIx,
sendAndConfirmChecked,
} from "@sunrisestake/beams-common";
import { StateAccount } from "./state.js";
Expand Down Expand Up @@ -264,7 +264,7 @@ export class MarinadeLpClient extends BeamInterface<
})
.instruction();

return new Transaction().add(instruction);
return new Transaction().add(requestIncreasedCUsIx(400_000), instruction);
}

/**
Expand Down Expand Up @@ -350,15 +350,16 @@ export class MarinadeLpClient extends BeamInterface<
state: this.stateAddress,
marinadeState: this.state.proxyState,
sunriseState: this.state.sunriseState,
msolMint: this.marinadeLp.marinade.mSolMint.address,
msolVault: this.state.msolTokenAccount,
yieldAccount: this.sunrise.state.yieldAccount,
liqPoolMint: this.marinadeLp.marinade.lpMint.address,
liqPoolTokenVault: this.marinadeLp.beamVault,
vaultAuthority: this.vaultAuthority[0],
transferMsolTo: this.state.msolTokenAccount,
liqPoolSolLegPda: await this.marinadeLp.marinade.solLeg(),
liqPoolMsolLeg: this.marinadeLp.marinade.mSolLeg,
treasuryMsolAccount: this.marinadeLp.marinade.treasuryMsolAccount,
yieldAccount: this.sunrise.state.yieldAccount,
liqPoolMsolLegAuthority:
await this.marinadeLp.marinade.mSolLegAuthority(),
sysvarInstructions: SYSVAR_INSTRUCTIONS_PUBKEY,
sysvarClock: SYSVAR_CLOCK_PUBKEY,
sunriseProgram: this.sunrise.program.programId,
marinadeProgram: MARINADE_FINANCE_PROGRAM_ID,
systemProgram: SystemProgram.programId,
Expand Down Expand Up @@ -464,16 +465,6 @@ export class MarinadeLpClient extends BeamInterface<
const solValue = solBalance * proportionOfPool;
const msolValue = msolLegBalance.toNumber() * proportionOfPool;

console.log("calculateBalanceFromLpTokens", {
lpTokens: lpTokens.toNumber(),
totalSupply: totalSupply.toNumber(),
proportionOfPool,
solBalance,
msolLegBalance: msolLegBalance.toNumber(),
solValue,
msolValue,
});

return {
lamports: new BN(solValue),
msolLamports: new BN(msolValue),
Expand Down
49 changes: 28 additions & 21 deletions packages/tests/src/functional/beams/marinade-lp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Keypair, LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js";
import BN from "bn.js";
import { provider, staker, stakerIdentity } from "../setup.js";
import {
expectAmount,
expectSolBalance,
expectTokenBalance,
fund,
Expand All @@ -18,6 +19,7 @@ import {
} from "../../utils.js";
import { expect } from "chai";
import { MarinadeClient } from "@sunrisestake/beams-marinade-sp";
import { MARINADE_LP_WITHDRAWAL_FEE_PERCENTAGE, ZERO } from "../consts.js";

describe("Marinade liquidity pool beam", () => {
let coreClient: SunriseClient;
Expand Down Expand Up @@ -68,6 +70,11 @@ describe("Marinade liquidity pool beam", () => {
provider.publicKey,
sunriseStateAddress,
);
coreClient = await coreClient.refresh();
await sendAndConfirmTransaction(
provider,
await coreClient.registerBeam(marinadeSpBeamClient.stateAddress),
);

await SunriseClient.get(provider, sunriseStateAddress);

Expand All @@ -76,7 +83,7 @@ describe("Marinade liquidity pool beam", () => {
provider.publicKey,
sunriseStateAddress,
marinadeSpBeamClient.stateAddress,
marinadeSpBeamClient.marinade.beamMsolVault,
marinadeSpBeamClient.marinade.beamMsolVault, // this will be the target of any extracted msol
);

const info = beamClient.state.pretty();
Expand Down Expand Up @@ -193,9 +200,10 @@ describe("Marinade liquidity pool beam", () => {

// check that the epoch report has been updated
beamClient = await beamClient.refresh();
expect(
beamClient.sunrise.state.epochReport.beamEpochDetails[0].extractableYield.toNumber(),
).to.equal(0);
// the lp beam is the second one (sp being the first)
const beamEpochDetail =
beamClient.sunrise.state.epochReport.beamEpochDetails[1];
expectAmount(beamEpochDetail.extractableYield, ZERO);
});

it("can't deposit due to exceeding allocation", async () => {
Expand Down Expand Up @@ -232,7 +240,8 @@ describe("Marinade liquidity pool beam", () => {
await beamClient.proportionOfPool(withdrawalAmountBN);
const lpSupply = await beamClient.poolTokenSupply();
const withdrawnLpTokens = new BN(
"" + Math.floor(lpSupply.toNumber() * proportionOfPool),
// round up to ensure we withdraw sufficient LP tokens to cover the required SOL amount
"" + Math.ceil(lpSupply.toNumber() * proportionOfPool),
);
const expectedLPTokens = vaultBalance.sub(withdrawnLpTokens);

Expand All @@ -256,9 +265,10 @@ describe("Marinade liquidity pool beam", () => {

// check that the epoch report has been updated
beamClient = await beamClient.refresh();
expect(
beamClient.sunrise.state.epochReport.beamEpochDetails[0].extractableYield.toNumber(),
).to.equal(0);
// the lp beam is the second one (sp being the first)
const beamEpochDetail =
beamClient.sunrise.state.epochReport.beamEpochDetails[1];
expectAmount(beamEpochDetail.extractableYield, ZERO);
});

it("can burn gsol", async () => {
Expand Down Expand Up @@ -287,23 +297,15 @@ describe("Marinade liquidity pool beam", () => {
// This results in less extractable yield for this beam, and more for the Marinade-SP beam.
// (However, in reality, this beam should rarely be extracted from, as it is
// included as a buffer to allow for fee-less gSOL withdrawals)
const expectedFee = burnAmount.toNumber() * 0.003;
const expectedFee =
burnAmount.toNumber() * MARINADE_LP_WITHDRAWAL_FEE_PERCENTAGE;
const effectiveBurnedAmount = burnAmount.subn(expectedFee);
const lpTokenValue =
effectiveBurnedAmount.toNumber() / (await beamClient.poolTokenPrice());
const lpBalance = await beamClient.calculateBalanceFromLpTokens(
new BN(lpTokenValue),
);
netExtractableYield = lpBalance.lamports;
console.log("burnAmount", burnAmount.toNumber());
console.log("expectedFee", expectedFee);
console.log("effectiveBurnedAmount", effectiveBurnedAmount.toNumber());
console.log("poolTokenPrice", await beamClient.poolTokenPrice());
console.log("lpTokenValue", lpTokenValue);
console.log("lpBalance", {
lamports: lpBalance.lamports.toNumber(),
msolLamports: lpBalance.msolLamports.toNumber(),
});

await sendAndConfirmTransaction(
// anyone can update the epoch report, but let's use the staker provider (rather than the admin provider) for this test
Expand All @@ -314,9 +316,14 @@ describe("Marinade liquidity pool beam", () => {

// check that the epoch report has been updated
beamClient = await beamClient.refresh();
expect(
beamClient.sunrise.state.epochReport.beamEpochDetails[0].extractableYield.toNumber(),
).to.equal(netExtractableYield.toNumber());
// the lp beam is the second one (sp being the first)
const beamEpochDetail =
beamClient.sunrise.state.epochReport.beamEpochDetails[1];
expectAmount(
netExtractableYield.toNumber(),
beamEpochDetail.extractableYield.toNumber(),
2,
);
});

it("can extract yield into a stake account", async () => {
Expand Down
2 changes: 0 additions & 2 deletions packages/tests/src/functional/beams/marinade-sp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ import {
} from "../../utils.js";
import { provider, staker, stakerIdentity } from "../setup.js";
import { expect } from "chai";
import { MSOL_MINT } from "../consts.js";

describe("Marinade stake pool beam", () => {
let coreClient: SunriseClient;
let beamClient: MarinadeClient;
Expand Down
7 changes: 6 additions & 1 deletion packages/tests/src/functional/consts.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
// see anchor.toml
import { PublicKey } from "@solana/web3.js";
import BN from "bn.js";

export const ZERO = new BN(0);

export const MARINADE_LP_WITHDRAWAL_FEE_PERCENTAGE = 0;

// see anchor.toml
export const SUNRISE_CORE_STATE = new PublicKey(
"89wj5p56PTFiKQcHLTkx78jM3Cv4jVRCXgMKJvoFvvp",
);
Expand Down
Loading

0 comments on commit 0da9b70

Please sign in to comment.