Skip to content

Commit

Permalink
[contract] fix for claim settlement on exact match
Browse files Browse the repository at this point in the history
  • Loading branch information
ochaloup committed Jul 3, 2024
1 parent b23f354 commit 0b17bd8
Show file tree
Hide file tree
Showing 4 changed files with 248 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import {
MerkleTreeNode,
SETTLEMENT_CLAIM_SEED,
ValidatorBondsProgram,
bondsWithdrawerAuthority,
claimSettlementInstruction,
fundSettlementInstruction,
getRentExemptStake,
getSettlement,
getSettlementClaim,
settlementClaimAddress,
settlementStakerAuthority,
} from '../../src'
import {
BankrunExtendedProvider,
Expand Down Expand Up @@ -36,6 +38,7 @@ import {
createSettlementFundedDelegatedStake,
createDelegatedStakeAccount,
createVoteAccount,
createInitializedStakeAccount,
} from '../utils/staking'
import {
signer,
Expand All @@ -50,16 +53,19 @@ import {
totalClaimVoteAccount1,
totalClaimVoteAccount2,
treeNodeBy,
treeNodesVoteAccount1,
voteAccount1Keypair,
voteAccount2Keypair,
withdrawer1,
withdrawer2,
withdrawer3,
withdrawer4,
} from '../utils/merkleTreeTestData'
import { verifyError } from '@marinade.finance/anchor-common'
import BN from 'bn.js'
import { executeTxWithError } from '../utils/helpers'
import { initBankrunTest } from './bankrun'
import assert from 'assert'

describe('Validator Bonds claim settlement', () => {
const epochsToClaimSettlement = 4
Expand All @@ -85,6 +91,7 @@ describe('Validator Bonds claim settlement', () => {

beforeAll(async () => {
;({ provider, program } = await initBankrunTest())

const epochNow = await currentEpoch(provider)
const firstSlotOfEpoch = await getFirstSlotOfEpoch(provider, epochNow)
const firstSlotOfNextEpoch = await getFirstSlotOfEpoch(
Expand Down Expand Up @@ -126,7 +133,9 @@ describe('Validator Bonds claim settlement', () => {
voteAccount: voteAccount2,
validatorIdentity: validatorIdentity2,
}))
})

async function initVariousTest() {
rentCollector = Keypair.generate()
settlementEpoch = await currentEpoch(provider)
;({ settlementAccount: settlementAccount1 } = await executeInitSettlement({
Expand Down Expand Up @@ -193,9 +202,11 @@ describe('Validator Bonds claim settlement', () => {
fundIx2
)
await createWithdrawerUsers(provider)
})
}

it('claim settlement various', async () => {
await initVariousTest()

const treeNode1Withdrawer1 = treeNodeBy(voteAccount1, withdrawer1)
const stakeAccountLamportsBefore = 123 * LAMPORTS_PER_SOL
const stakeAccountTreeNode1Withdrawer1 = await createDelegatedStakeAccount({
Expand Down Expand Up @@ -585,6 +596,108 @@ describe('Validator Bonds claim settlement', () => {
assertNotExist(provider, accTooLate)
})

it('claim settlement with exact match on stake account size', async () => {
await warpToNextEpoch(provider) // we want to have different settlement account address

settlementEpoch = await currentEpoch(provider)
const maxTotalClaim = treeNodesVoteAccount1
.map(t => t.treeNode.data.claim)
.reduce((a, b) => a.add(b))
const { settlementAccount } = await executeInitSettlement({
configAccount,
program,
provider,
voteAccount: voteAccount1,
operatorAuthority,
currentEpoch: settlementEpoch,
merkleRoot: MERKLE_ROOT_VOTE_ACCOUNT_1_BUF,
maxMerkleNodes: treeNodesVoteAccount1.length,
maxTotalClaim,
})
const [withdrawAuth] = bondsWithdrawerAuthority(
configAccount,
program.programId
)
const [stakeAuth] = settlementStakerAuthority(
settlementAccount,
program.programId
)

const amount1 = LAMPORTS_PER_SOL * 1
const amount2 = LAMPORTS_PER_SOL * 42

const treeNode1Withdrawer4 = treeNodesVoteAccount1.filter(t =>
t.treeNode.data.withdrawAuthority.equals(withdrawer4)
)
expect(treeNode1Withdrawer4.length).toEqual(2)
const treeNode1OneLamport = treeNode1Withdrawer4.find(
t => t.treeNode.data.claim.toNumber() === amount1
)
assert(treeNode1OneLamport !== undefined)
const { stakeAccount: stakeAccountOneLamportFrom } =
await createInitializedStakeAccount({
provider,
rentExempt: amount1,
staker: stakeAuth,
withdrawer: withdrawAuth,
})
const treeNode42Lamports = treeNode1Withdrawer4.find(
t => t.treeNode.data.claim.toNumber() === amount2
)
assert(treeNode42Lamports !== undefined)
const { stakeAccount: stakeAccount42LamportsFrom } =
await createInitializedStakeAccount({
provider,
rentExempt: amount2,
staker: stakeAuth,
withdrawer: withdrawAuth,
})

const stakeAccountOneLamportTo = await createDelegatedStakeAccount({
provider,
lamports: 10 * LAMPORTS_PER_SOL,
voteAccount: voteAccount1,
staker: treeNode1OneLamport.treeNode.stakeAuthority,
withdrawer: treeNode1OneLamport.treeNode.withdrawAuthority,
})
const stakeAccount42LamportsTo = await createDelegatedStakeAccount({
provider,
lamports: 10 * LAMPORTS_PER_SOL,
voteAccount: voteAccount1,
staker: treeNode42Lamports.treeNode.stakeAuthority,
withdrawer: treeNode42Lamports.treeNode.withdrawAuthority,
})

// warp to be able to claim (see slotsToStartSettlementClaiming)
warpToNextEpoch(provider)

const { instruction: ix1 } = await claimSettlementInstruction({
program,
claimAmount: amount1,
merkleProof: treeNode1OneLamport.proof,
settlementAccount: settlementAccount,
stakeAccountFrom: stakeAccountOneLamportFrom,
stakeAccountTo: stakeAccountOneLamportTo,
stakeAccountStaker: treeNode1OneLamport.treeNode.stakeAuthority,
stakeAccountWithdrawer: treeNode1OneLamport.treeNode.withdrawAuthority,
})
await provider.sendIx([], ix1)
assertNotExist(provider, stakeAccountOneLamportFrom)

const { instruction: ix2 } = await claimSettlementInstruction({
program,
claimAmount: amount2,
merkleProof: treeNode42Lamports.proof,
settlementAccount: settlementAccount,
stakeAccountFrom: stakeAccount42LamportsFrom,
stakeAccountTo: stakeAccount42LamportsTo,
stakeAccountStaker: treeNode42Lamports.treeNode.stakeAuthority,
stakeAccountWithdrawer: treeNode42Lamports.treeNode.withdrawAuthority,
})
await provider.sendIx([], ix2)
assertNotExist(provider, stakeAccount42LamportsFrom)
})

async function warpToNotBeClaimable() {
await warpOffsetEpoch(provider, epochsToClaimSettlement + 1)
}
Expand Down
110 changes: 90 additions & 20 deletions packages/validator-bonds-sdk/__tests__/utils/merkleTreeTestData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,6 @@ import BN from 'bn.js'
import { ExtendedProvider } from '@marinade.finance/web3js-common'
import { createUserAndFund } from '@marinade.finance/web3js-common'

export const MERKLE_PROOF_VOTE_ACCOUNT_1 =
'EnBJg4qV4GjH3Sgigsi8wkWz966QYgSQkgPMCmWto51f'
export const MERKLE_ROOT_VOTE_ACCOUNT_1_BUF = bs58.decode(
MERKLE_PROOF_VOTE_ACCOUNT_1
)
export const MERKLE_PROOF_VOTE_ACCOUNT_2 =
'Asi9uVpB3Tx29L17X3Z46jrPizKywTRttqsvnLTzgh27'
export const MERKLE_ROOT_VOTE_ACCOUNT_2_BUF = bs58.decode(
MERKLE_PROOF_VOTE_ACCOUNT_2
)
export const MERKLE_PROOF_OPERATOR =
'D8rFThGJXYVFcKdqovz3VMA1nALNugHzvGYhSn8dLwip'
export const MERKLE_ROOT_VOTE_OPERATOR_BUF = bs58.decode(MERKLE_PROOF_OPERATOR)

export const configAccount = new PublicKey(
'4wQELTA1RMEM3cKN7gjbiNN247e3GY9Sga7MKpNV38kL'
)
Expand Down Expand Up @@ -87,6 +73,18 @@ export const withdrawer3Keypair = Keypair.fromSecretKey(
101, 205, 13, 212, 139, 234, 174, 137, 193, 203, 120, 62, 72, 48, 54,
])
)
export const withdrawer4 = new PublicKey(
'DdWhr91hqajDZRaRVt4QhD5yJasjmyeweST5VUbfCKGy'
)
export const withdrawer4Keypair = Keypair.fromSecretKey(
new Uint8Array([
137, 198, 40, 27, 37, 227, 249, 231, 34, 199, 32, 244, 110, 23, 214, 53, 74,
169, 123, 60, 47, 124, 240, 31, 152, 202, 22, 22, 219, 120, 37, 14, 187,
166, 189, 44, 111, 242, 7, 250, 248, 14, 163, 244, 255, 202, 153, 170, 45,
159, 43, 102, 71, 254, 58, 222, 149, 1, 233, 215, 141, 139, 98, 62,
])
)

export const staker1 = new PublicKey(
'82ewSU2zNH87PajZHf7betFbZAaGR8bwDp8azSHNCAnA'
)
Expand Down Expand Up @@ -126,6 +124,11 @@ export type MerkleTreeNodeWithProof = {
proof: number[][]
}

export const MERKLE_PROOF_VOTE_ACCOUNT_1 =
'6H5xisVj8r1aYRX2B2PyeG62ofF9aUiy8qHzwwkJCqqH'
export const MERKLE_ROOT_VOTE_ACCOUNT_1_BUF = bs58.decode(
MERKLE_PROOF_VOTE_ACCOUNT_1
)
export const ITEMS_VOTE_ACCOUNT_1: MerkleTreeNodeWithProof[] = [
{
// tree node hash: 3tSbFBfFg83LCgVneuENUFs8hKgsdTKvfVV6Cqz3q6RT
Expand All @@ -140,9 +143,12 @@ export const ITEMS_VOTE_ACCOUNT_1: MerkleTreeNodeWithProof[] = [
108, 104, 68, 176, 233, 152, 64, 34, 167, 84, 90, 65, 102, 170, 109,
],
[
84, 75, 193, 1, 167, 55, 248, 48, 129, 33, 198, 240, 33, 229, 57, 27,
194, 110, 52, 184, 244, 142, 198, 188, 161, 150, 177, 49, 26, 123, 214,
187,
242, 32, 26, 226, 118, 158, 156, 230, 202, 164, 42, 249, 57, 87, 29, 89,
247, 47, 67, 135, 233, 170, 92, 204, 187, 9, 203, 71, 176, 249, 129, 21,
],
[
100, 183, 165, 4, 15, 25, 171, 235, 171, 51, 238, 200, 78, 13, 144, 57,
166, 114, 241, 15, 80, 249, 164, 234, 94, 171, 12, 64, 164, 69, 112, 50,
],
],
},
Expand All @@ -159,9 +165,12 @@ export const ITEMS_VOTE_ACCOUNT_1: MerkleTreeNodeWithProof[] = [
55, 244, 31, 206, 177, 91, 206, 203, 184, 48, 99, 76, 163, 203, 232, 44,
],
[
84, 75, 193, 1, 167, 55, 248, 48, 129, 33, 198, 240, 33, 229, 57, 27,
194, 110, 52, 184, 244, 142, 198, 188, 161, 150, 177, 49, 26, 123, 214,
187,
242, 32, 26, 226, 118, 158, 156, 230, 202, 164, 42, 249, 57, 87, 29, 89,
247, 47, 67, 135, 233, 170, 92, 204, 187, 9, 203, 71, 176, 249, 129, 21,
],
[
100, 183, 165, 4, 15, 25, 171, 235, 171, 51, 238, 200, 78, 13, 144, 57,
166, 114, 241, 15, 80, 249, 164, 234, 94, 171, 12, 64, 164, 69, 112, 50,
],
],
},
Expand All @@ -172,6 +181,30 @@ export const ITEMS_VOTE_ACCOUNT_1: MerkleTreeNodeWithProof[] = [
stakeAuthority: staker2,
claim: 212121,
}),
proof: [
[
153, 210, 192, 197, 37, 37, 63, 137, 37, 158, 9, 107, 244, 72, 195, 21,
115, 61, 19, 35, 188, 142, 139, 64, 251, 216, 66, 116, 222, 158, 212,
34,
],
[
146, 196, 239, 63, 54, 200, 90, 234, 50, 1, 61, 217, 219, 111, 207, 131,
119, 168, 107, 251, 218, 240, 133, 67, 116, 40, 11, 109, 116, 34, 154,
73,
],
[
100, 183, 165, 4, 15, 25, 171, 235, 171, 51, 238, 200, 78, 13, 144, 57,
166, 114, 241, 15, 80, 249, 164, 234, 94, 171, 12, 64, 164, 69, 112, 50,
],
],
},
{
// tree node hash: CzTkYPYkzXstjNTaqPfWMQDQESnPDrRBK2SjRac8kDYs
treeNode: new MerkleTreeNode({
withdrawAuthority: withdrawer4,
stakeAuthority: staker2,
claim: LAMPORTS_PER_SOL,
}),
proof: [
[
166, 246, 173, 43, 141, 45, 116, 63, 47, 72, 233, 142, 194, 147, 46, 95,
Expand All @@ -183,9 +216,42 @@ export const ITEMS_VOTE_ACCOUNT_1: MerkleTreeNodeWithProof[] = [
119, 168, 107, 251, 218, 240, 133, 67, 116, 40, 11, 109, 116, 34, 154,
73,
],
[
100, 183, 165, 4, 15, 25, 171, 235, 171, 51, 238, 200, 78, 13, 144, 57,
166, 114, 241, 15, 80, 249, 164, 234, 94, 171, 12, 64, 164, 69, 112, 50,
],
],
},
{
// tree node hash: GFn1wci7vwUfPgq4C9vYRYRn8UgTE1i2qVdE3raabvoK
treeNode: new MerkleTreeNode({
withdrawAuthority: withdrawer4,
stakeAuthority: staker3,
claim: 42 * LAMPORTS_PER_SOL,
}),
proof: [
[
65, 32, 94, 245, 186, 250, 39, 41, 113, 203, 105, 228, 195, 14, 19, 171,
171, 3, 51, 176, 218, 36, 182, 71, 65, 5, 255, 231, 202, 30, 200, 169,
],
[
145, 10, 181, 241, 68, 0, 136, 90, 81, 187, 131, 86, 95, 194, 24, 193,
160, 42, 141, 227, 199, 253, 222, 96, 80, 73, 248, 1, 109, 42, 6, 62,
],
[
174, 148, 230, 241, 186, 94, 91, 190, 170, 56, 66, 207, 104, 237, 226,
1, 103, 251, 185, 246, 22, 235, 238, 29, 118, 132, 60, 5, 61, 15, 25,
43,
],
],
},
]

export const MERKLE_PROOF_VOTE_ACCOUNT_2 =
'Asi9uVpB3Tx29L17X3Z46jrPizKywTRttqsvnLTzgh27'
export const MERKLE_ROOT_VOTE_ACCOUNT_2_BUF = bs58.decode(
MERKLE_PROOF_VOTE_ACCOUNT_2
)
export const ITEMS_VOTE_ACCOUNT_2: MerkleTreeNodeWithProof[] = [
{
// tree node hash: 2niLq4dRayu3GE5KWuBUR4hAjSikubd1hmGKLJ56ZzUP
Expand Down Expand Up @@ -217,6 +283,10 @@ export const ITEMS_VOTE_ACCOUNT_2: MerkleTreeNodeWithProof[] = [
],
},
]

export const MERKLE_PROOF_OPERATOR =
'D8rFThGJXYVFcKdqovz3VMA1nALNugHzvGYhSn8dLwip'
export const MERKLE_ROOT_VOTE_OPERATOR_BUF = bs58.decode(MERKLE_PROOF_OPERATOR)
export const ITEMS_OPERATOR: MerkleTreeNodeWithProof[] = [
{
// tree node hash: C8ZfYuKidJa8EGF4YF5Xou3icvqqGQ6fJBE6SN3ixT1w
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,11 +213,13 @@ impl<'info> ClaimSettlement<'info> {
)?;

// The provided stake account must be sufficiently large to cover the claim while remaining valid.
// It is the SDK's responsibility to merge stake accounts if necessary.
// It is the caller's responsibility to merge stake accounts if necessary.
// - The invariant is that the stake account will always be rent-exempt and of minimum size.
// This must be ensured by the fund_settlement instruction.
if ctx.accounts.stake_account_from.get_lamports()
< claim + minimal_size_stake_account(&stake_from_meta, &ctx.accounts.config)
< claim + minimal_size_stake_account(&stake_from_meta, &ctx.accounts.config) &&
// on perfect match when stake account lamports is equal to the claim amount we can withdraw all
ctx.accounts.stake_account_from.get_lamports() != claim
{
return Err(error!(ErrorCode::ClaimingStakeAccountLamportsInsufficient)
.with_account_name("stake_account_from")
Expand All @@ -240,7 +242,7 @@ impl<'info> ClaimSettlement<'info> {
) {
return Err(error!(ErrorCode::ClaimSettlementProofFailed).with_values((
"Merkle proof verification failed",
format!("Tree node: {:?}", tree_node),
format!("Tree node: {:?}, hash: {:?}", tree_node, tree_node_hash),
)));
}

Expand Down
Loading

0 comments on commit 0b17bd8

Please sign in to comment.