From b23f354af48773c55a74550b53eda2ac399442a1 Mon Sep 17 00:00:00 2001 From: Ondra Chaloupka Date: Mon, 1 Jul 2024 10:32:45 +0200 Subject: [PATCH] [contract] merge stake fixing correct signature for merging --- .../__tests__/bankrun/mergeStake.spec.ts | 177 +++++++++++++++++- .../src/instructions/mergeStake.ts | 16 +- .../src/instructions/stake/merge_stake.rs | 6 +- 3 files changed, 188 insertions(+), 11 deletions(-) diff --git a/packages/validator-bonds-sdk/__tests__/bankrun/mergeStake.spec.ts b/packages/validator-bonds-sdk/__tests__/bankrun/mergeStake.spec.ts index 32b516ee..5de2a974 100644 --- a/packages/validator-bonds-sdk/__tests__/bankrun/mergeStake.spec.ts +++ b/packages/validator-bonds-sdk/__tests__/bankrun/mergeStake.spec.ts @@ -6,17 +6,27 @@ import { settlementAddress, settlementStakerAuthority, bondsWithdrawerAuthority, + getRentExemptStake, + getConfig, + getSettlement, + fundSettlementInstruction, + getStakeAccount, } from '../../src' import { BankrunExtendedProvider, assertNotExist, + currentEpoch, warpToEpoch, + warpToNextEpoch, } from '@marinade.finance/bankrun-utils' import { + executeInitBondInstruction, executeInitConfigInstruction, + executeInitSettlement, executeWithdraw, } from '../utils/testTransactions' import { + Keypair, LAMPORTS_PER_SOL, PublicKey, SYSVAR_STAKE_HISTORY_PUBKEY, @@ -29,8 +39,9 @@ import { createInitializedStakeAccount, getAndCheckStakeAccount, StakeStates, + createBondsFundedStakeAccount, } from '../utils/staking' -import { pubkey } from '@marinade.finance/web3js-common' +import { pubkey, signer } from '@marinade.finance/web3js-common' import { verifyError } from '@marinade.finance/anchor-common' import { initBankrunTest } from './bankrun' @@ -38,6 +49,7 @@ describe('Staking merge verification/investigation', () => { let provider: BankrunExtendedProvider let program: ValidatorBondsProgram let configAccount: PublicKey + let operatorAuthority: Keypair const startUpEpoch = Math.floor(Math.random() * 100) + 100 beforeAll(async () => { @@ -46,10 +58,12 @@ describe('Staking merge verification/investigation', () => { }) beforeEach(async () => { - ;({ configAccount } = await executeInitConfigInstruction({ - program, - provider, - })) + ;({ configAccount, operatorAuthority } = await executeInitConfigInstruction( + { + program, + provider, + } + )) }) it('cannot merge with withdrawer authority not belonging to bonds', async () => { @@ -115,6 +129,7 @@ describe('Staking merge verification/investigation', () => { sourceStakeAccount: nonDelegatedStakeAccount2, destinationStakeAccount: nonDelegatedStakeAccount, stakerAuthority: pubkey(staker), + settlementAccount: nonDelegatedStakeAccount, }) try { await provider.sendIx([], ixNonBondStaker) @@ -462,7 +477,7 @@ describe('Staking merge verification/investigation', () => { } }) - it('merging', async () => { + it('merging when funded to bond', async () => { const [bondWithdrawer] = bondsWithdrawerAuthority( configAccount, program.programId @@ -510,4 +525,154 @@ describe('Staking merge verification/investigation', () => { const stakeAccount = await provider.connection.getAccountInfo(stakeAccount1) expect(stakeAccount?.lamports).toEqual(9 * LAMPORTS_PER_SOL) }) + + it('merging when funded to settlement', async () => { + const { voteAccount, validatorIdentity } = await createVoteAccount({ + provider, + }) + await executeInitBondInstruction({ + program, + provider, + configAccount, + voteAccount, + validatorIdentity, + }) + const config = await getConfig(program, configAccount) + + const epoch = await currentEpoch(provider) + const maxTotalClaim = LAMPORTS_PER_SOL * 10 + const { settlementAccount } = await executeInitSettlement({ + configAccount, + program, + provider, + voteAccount, + operatorAuthority, + maxTotalClaim, + currentEpoch: epoch, + }) + + const rentExemptStake = await getRentExemptStake(provider) + const stakeAccountMinimalAmount = + rentExemptStake + config.minimumStakeLamports.toNumber() + const lamportsToFund1 = maxTotalClaim / 2 + 2 * LAMPORTS_PER_SOL + const lamportsToFund2 = + maxTotalClaim - + lamportsToFund1 + + 2 * stakeAccountMinimalAmount + + 1 * LAMPORTS_PER_SOL + + const stakeAccount1 = await createBondsFundedStakeAccountActivated( + voteAccount, + lamportsToFund1 + ) + const stakeAccountData1 = + await provider.connection.getAccountInfo(stakeAccount1) + expect(stakeAccountData1?.lamports).toEqual(lamportsToFund1) + const stakeAccount2 = await createBondsFundedStakeAccountActivated( + voteAccount, + lamportsToFund2 + ) + const stakeAccountData2 = + await provider.connection.getAccountInfo(stakeAccount2) + expect(stakeAccountData2?.lamports).toEqual(lamportsToFund2) + + const settlementData = await getSettlement(program, settlementAccount) + expect(settlementData.lamportsFunded).toEqual(0) + + const { instruction: ix1, splitStakeAccount: split1 } = + await fundSettlementInstruction({ + program, + settlementAccount, + stakeAccount: stakeAccount1, + }) + const { instruction: ix2, splitStakeAccount: split2 } = + await fundSettlementInstruction({ + program, + settlementAccount, + stakeAccount: stakeAccount2, + }) + await provider.sendIx( + [signer(split1), signer(split2), operatorAuthority], + ix1, + ix2 + ) + + let settlement = await getSettlement(program, settlementAccount) + // the amount in stake accounts were set to over-fund by 1 sol that cannot be + // split to a separate stake account and this one SOL is thus funded on top of required + expect(settlement.lamportsFunded).toEqual( + maxTotalClaim + 1 * LAMPORTS_PER_SOL + ) + + const [withdrawerAuthority] = bondsWithdrawerAuthority( + configAccount, + program.programId + ) + const [stakerAuthority] = settlementStakerAuthority( + settlementAccount, + program.programId + ) + let stakeAccount1Data = await getStakeAccount( + program.provider, + stakeAccount1, + epoch + ) + const stakeAccount2Data = await getStakeAccount( + program.provider, + stakeAccount2, + epoch + ) + expect(stakeAccount1Data.balanceLamports).toEqual(lamportsToFund1) + expect(stakeAccount1Data.voter).toEqual(voteAccount) + expect(stakeAccount1Data.withdrawer).toEqual(withdrawerAuthority) + expect(stakeAccount1Data.staker).toEqual(stakerAuthority) + expect(stakeAccount2Data.balanceLamports).toEqual(lamportsToFund2) + expect(stakeAccount2Data.voter).toEqual(voteAccount) + expect(stakeAccount2Data.withdrawer).toEqual(withdrawerAuthority) + expect(stakeAccount2Data.staker).toEqual(stakerAuthority) + + // waiting to not getting error 0x5 of TransientState for the stake accounts + await warpToNextEpoch(provider) + + const { instruction } = await mergeStakeInstruction({ + program, + configAccount, + sourceStakeAccount: stakeAccount2, + destinationStakeAccount: stakeAccount1, + settlementAccount, + stakerAuthority, + }) + await provider.sendIx([], instruction) + + await assertNotExist(provider, stakeAccount2) + stakeAccount1Data = await getStakeAccount( + program.provider, + stakeAccount1, + epoch + ) + expect(stakeAccount1Data.staker).toEqual(stakerAuthority) + expect(stakeAccount1Data.balanceLamports).toEqual( + lamportsToFund1 + lamportsToFund2 + ) + + settlement = await getSettlement(program, settlementAccount) + expect(settlement.lamportsFunded).toEqual( + maxTotalClaim + 1 * LAMPORTS_PER_SOL + ) + }) + + async function createBondsFundedStakeAccountActivated( + voteAccount: PublicKey, + lamports: number + ): Promise { + const sa = await createBondsFundedStakeAccount({ + program, + provider, + voteAccount, + lamports, + configAccount, + }) + await warpToNextEpoch(provider) + return sa + } }) diff --git a/packages/validator-bonds-sdk/src/instructions/mergeStake.ts b/packages/validator-bonds-sdk/src/instructions/mergeStake.ts index 6129b669..8ebac840 100644 --- a/packages/validator-bonds-sdk/src/instructions/mergeStake.ts +++ b/packages/validator-bonds-sdk/src/instructions/mergeStake.ts @@ -32,8 +32,20 @@ export async function mergeStakeInstruction({ // stake account staker authority can be either bond managed or settlement managed // it would be good to check settlements automatically by searching all settlements of the bond and validator // and make sdk to find the right settlement to use when the settlement pubkey is not provided as param - stakerAuthority = - stakerAuthority ?? bondsWithdrawerAuthority(configAccount)[0] + + const bondsWithdrawer = bondsWithdrawerAuthority(configAccount)[0] + if ( + stakerAuthority !== undefined && + settlementAccount.equals(PublicKey.default) + ) { + if (!bondsWithdrawer.equals(stakerAuthority)) { + throw new Error( + 'When stakerAuthority provided, please, provide the Settlement account address as well.' + + ' Contract requires the Settlement address to derive the correct merge authority.' + ) + } + } + stakerAuthority = stakerAuthority ?? bondsWithdrawer const instruction = await program.methods .mergeStake({ diff --git a/programs/validator-bonds/src/instructions/stake/merge_stake.rs b/programs/validator-bonds/src/instructions/stake/merge_stake.rs index f1314a13..711fd3e6 100644 --- a/programs/validator-bonds/src/instructions/stake/merge_stake.rs +++ b/programs/validator-bonds/src/instructions/stake/merge_stake.rs @@ -138,7 +138,7 @@ impl<'info> MergeStake<'info> { merge_account_infos, &[&[ SETTLEMENT_STAKER_AUTHORITY_SEED, - &ctx.accounts.config.key().as_ref(), + &settlement.as_ref(), &[settlement_bump], ]], )? @@ -146,9 +146,9 @@ impl<'info> MergeStake<'info> { return Err(error!(ErrorCode::StakerAuthorityMismatch) .with_account_name("staker_authority") .with_values(( - "staker_authority/bonds_withdrawer_authority/settlement_staker_authority", + "accounts.staker_authority [bonds_withdrawer_authority/settlement_staker_authority]", format!( - "{}/{}/{}", + "{} [{}/{}]", ctx.accounts.staker_authority.key(), bonds_withdrawer_authority, settlement_staker_authority