diff --git a/.editorconfig b/.editorconfig index 79fe8026..838b5214 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,3 +1,4 @@ + root = true [*] diff --git a/Cargo.lock b/Cargo.lock index 59b98fa7..7a232f1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -971,9 +971,9 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.14.0" +version = "1.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" +checksum = "a2ef034f05691a48569bd920a96c81b9d91bbad1ab5ac7c4616c1f6ef36cb79f" dependencies = [ "bytemuck_derive", ] @@ -6183,7 +6183,7 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "validator-bonds" -version = "0.1.0" +version = "1.1.0" dependencies = [ "anchor-lang", "anchor-spl", diff --git a/README.md b/README.md index dcf32a49..8db4e1bd 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,8 @@ pnpm test:cargo ### Contract deployment ```sh -anchor build --verifiable +anchor build --verifiable \ + --env "GIT_REV=`git rev-parse --short HEAD`" --env 'GIT_REV_NAME=' # 1. DEPLOY ## deploy (devnet, hot wallet upgrade) @@ -82,9 +83,12 @@ anchor idl set-authority --provider.cluster mainnet --provider.wallet [fee-payer # 3.check verifiable deployment ( can be verified as well) +# a) when the target/verifiable/.so has been built already use switch --skip-build anchor --provider.cluster mainnet \ verify -p validator_bonds \ - vBoNdEvzMrSai7is21XgVYik65mqtaKXuSdMBJ1xkW4 + --env "GIT_REV=`git rev-parse --short HEAD`" --env 'GIT_REV_NAME=v1.1.0' \ + + ``` ## TODO diff --git a/insurance-engine/src/merkle_tree_collection.rs b/insurance-engine/src/merkle_tree_collection.rs index 9ff04797..cabba56d 100644 --- a/insurance-engine/src/merkle_tree_collection.rs +++ b/insurance-engine/src/merkle_tree_collection.rs @@ -130,3 +130,118 @@ fn get_proof(merkle_tree: &MerkleTree, i: usize) -> Vec<[u8; 32]> { } proof } + +#[cfg(test)] +mod tests { + use super::*; + use solana_sdk::bs58; + use solana_sdk::hash::hashv; + use solana_sdk::pubkey::Pubkey; + use std::str::FromStr; + + /// This is a constant pubkey test to verify against the TS tree node implementation + /// the TS implementation uses the same static pubkeys and the tests should pass here and there + #[test] + pub fn ts_cross_check_hash_generate() { + let hash_pub = TreeNode { + stake_authority: Pubkey::from_str("EjeWgRiaawLSCUM7uojZgSnwipEiypS986yorgvfAzYW") + .unwrap() + .to_string(), + withdraw_authority: Pubkey::from_str("BT6Y2kX5RLhQ6DDzbjbiHNDyyWJgn9jp7g5rCFn8stqy") + .unwrap() + .to_string(), + vote_account: Pubkey::from_str("DYSosfmS9gp1hTY4jAdKJFWK3XHsemecgVPwjqgwM2Pb") + .unwrap() + .to_string(), + claim: 444, + proof: None, + } + .hash(); + let tree_node = hashv(&[&[0], hash_pub.as_ref()]).to_bytes(); + assert_eq!( + hash_pub.to_string(), + "3LrYLzt4P6LJCyLsbYPAes4d5U8aohjbmW1dJvbrkdse" + ); + assert_eq!( + bs58::encode(tree_node).into_string(), + "37uc7x9LVzJqsPB9un28SJEPbSop8NGHXHQjZCe6GKAX" + ); + } + + // TS cross-check constant test + #[test] + pub fn ts_cross_check_merkle_proof() { + let mut items: Vec = vec![ + TreeNode { + stake_authority: "612S5jWDKhCxdzugJ6JED5whc1dCkZBPrer3mx3D2V5J".to_string(), + withdraw_authority: "3vGstFWWyQbDknu9WKr9vbTn2Kw5qgorP7UkRXVrfe9t".to_string(), + vote_account: "FHUuZcuLB3ZLWZhKoY7metTEJ2Y2Xton99TTuDmzFmgW".to_string(), + claim: 1234, + proof: None, + }, + TreeNode { + stake_authority: "612S5jWDKhCxdzugJ6JED5whc1dCkZBPrer3mx3D2V5J".to_string(), + withdraw_authority: "DBnWKq1Ln9y8HtGwYxFMqMWLY1Ld9xpB28ayKfHejiTs".to_string(), + vote_account: "FHUuZcuLB3ZLWZhKoY7metTEJ2Y2Xton99TTuDmzFmgW".to_string(), + claim: 99999, + proof: None, + }, + TreeNode { + stake_authority: "612S5jWDKhCxdzugJ6JED5whc1dCkZBPrer3mx3D2V5J".to_string(), + withdraw_authority: "CgoqXy3e1hsnuNw6bJ8iuzqZwr93CA4jsRa1AnsseJ53".to_string(), + vote_account: "FHUuZcuLB3ZLWZhKoY7metTEJ2Y2Xton99TTuDmzFmgW".to_string(), + claim: 212121, + proof: None, + }, + TreeNode { + stake_authority: "612S5jWDKhCxdzugJ6JED5whc1dCkZBPrer3mx3D2V5J".to_string(), + withdraw_authority: "3vGstFWWyQbDknu9WKr9vbTn2Kw5qgorP7UkRXVrfe9t".to_string(), + vote_account: "9D6EuvndvhgDBLRzpxNjHdvLWicJE1WvZrdTbapjhKR6".to_string(), + claim: 69, + proof: None, + }, + TreeNode { + stake_authority: "612S5jWDKhCxdzugJ6JED5whc1dCkZBPrer3mx3D2V5J".to_string(), + withdraw_authority: "DBnWKq1Ln9y8HtGwYxFMqMWLY1Ld9xpB28ayKfHejiTs".to_string(), + vote_account: "9D6EuvndvhgDBLRzpxNjHdvLWicJE1WvZrdTbapjhKR6".to_string(), + claim: 111111, + proof: None, + }, + ]; + let item_hashes = items.clone().iter().map(|n| n.hash()).collect::>(); + let merkle_tree = MerkleTree::new(&item_hashes[..], true); + let merkle_tree_root = merkle_tree.get_root().unwrap(); + // println!("merkle tree root: {}", merkle_tree_root); + for (i, tree_node) in items.iter_mut().enumerate() { + tree_node.proof = Some(get_proof(&merkle_tree, i)); + // println!( + // "proof: {:?}, hash tree node: {}", + // tree_node.proof, + // tree_node.hash() + // ) + } + assert_eq!( + merkle_tree_root.to_string(), + "CJWSpJD2yeL1JPUH9pyfAefFetdiSuvPNCqq5LfQ71je" + ); + let check_proof = [ + [ + 217, 141, 69, 36, 65, 205, 32, 76, 165, 35, 197, 94, 188, 141, 93, 158, 129, 239, + 253, 174, 42, 156, 151, 29, 197, 253, 160, 116, 10, 112, 12, 10, + ], + [ + 190, 99, 233, 8, 249, 68, 135, 70, 128, 15, 2, 169, 47, 194, 102, 12, 200, 64, 213, + 103, 134, 64, 112, 215, 201, 36, 212, 236, 32, 93, 76, 106, + ], + [ + 147, 52, 104, 182, 174, 190, 248, 228, 27, 240, 240, 245, 6, 218, 13, 196, 53, 63, + 242, 117, 208, 239, 15, 106, 255, 30, 248, 47, 107, 170, 233, 94, + ], + ]; + assert_eq!(items.get(3).unwrap().proof, Some(check_proof.to_vec())); + assert_eq!( + item_hashes.get(3).unwrap().to_string(), + "C94ftStYh3afdysMEnf4KGMvyQZVcMY6P16UkEJGkYbU" + ); + } +} diff --git a/package.json b/package.json index f7cbd850..31fcd671 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "_test": "pnpm jest --config jest.config.test-validator.js -- \"$FILE\"", "test:validator": "anchor test", "test:bankrun": "pnpm anchor:build && pnpm jest --config jest.config.bankrun.js --runInBand true -- \"$FILE\"", - "test:cargo": "cargo test --features no-entrypoint", + "test:cargo": "cargo test --features no-entrypoint -- --nocapture", "test": "pnpm test:cargo && pnpm test:bankrun && pnpm test:validator", "cli": "ts-node ./packages/validator-bonds-cli/src/", "lint:cargo": "cargo fmt -- --check && cargo clippy", @@ -27,12 +27,12 @@ "@types/bn.js": "^5.1.3", "@types/jest": "^29.5.6", "@types/node": "^18.11.9", - "gts": "^4.0.1", + "gts": "^5.2.0", "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "ts-node": "^10.9.1", + "ts-jest": "^29.1.2", + "ts-node": "^10.9.2", "typescript": "4.9.5", - "@marinade.finance/jest-utils": "^2.2.1" + "@marinade.finance/jest-utils": "^2.2.2" }, "pnpm": { "peerDependencyRules": { diff --git a/packages/validator-bonds-cli/README.md b/packages/validator-bonds-cli/README.md index 257e19f1..d67abfde 100644 --- a/packages/validator-bonds-cli/README.md +++ b/packages/validator-bonds-cli/README.md @@ -92,9 +92,7 @@ validator-bonds -um show-bond ... config: 'vbMaRfmTCg92HWGzmd53APkMNpPnGVGZTUHwUJQkXAU', validatorVoteAccount: '...', authority: '...', - revenueShare: { hundredthBps: 0 }, bump: 255, - reserved: { reserved: [Array] } } } ``` diff --git a/packages/validator-bonds-cli/__tests__/test-validator/configureBond.spec.ts b/packages/validator-bonds-cli/__tests__/test-validator/configureBond.spec.ts index 92c12cf0..da88765c 100644 --- a/packages/validator-bonds-cli/__tests__/test-validator/configureBond.spec.ts +++ b/packages/validator-bonds-cli/__tests__/test-validator/configureBond.spec.ts @@ -56,15 +56,15 @@ describe('Configure bond account using CLI', () => { provider, validatorIdentity )) - ;({ bondAccount } = await executeInitBondInstruction( + ;({ bondAccount } = await executeInitBondInstruction({ program, provider, - configAccount, - bondAuthorityKeypair, + config: configAccount, + bondAuthority: bondAuthorityKeypair, voteAccount, validatorIdentity, - 33 - )) + cpmpe: 33, + })) }) afterEach(async () => { @@ -85,8 +85,6 @@ describe('Configure bond account using CLI', () => { bondAccount.toBase58(), '--authority', bondAuthorityPath, - '--revenue-share', - '42', '--confirmation-finality', 'confirmed', ], @@ -101,9 +99,9 @@ describe('Configure bond account using CLI', () => { const [, bump] = bondAddress(configAccount, voteAccount, program.programId) const bondsData1 = await getBond(program, bondAccount) expect(bondsData1.config).toEqual(configAccount) - expect(bondsData1.validatorVoteAccount).toEqual(voteAccount) + expect(bondsData1.voteAccount).toEqual(voteAccount) expect(bondsData1.authority).toEqual(bondAuthorityKeypair.publicKey) - expect(bondsData1.revenueShare.hundredthBps).toEqual(42 * 10 ** 4) + expect(bondsData1.cpmpe).toEqual(33) expect(bondsData1.bump).toEqual(bump) const newBondAuthority = PublicKey.unique() @@ -125,8 +123,6 @@ describe('Configure bond account using CLI', () => { validatorIdentityPath, '--bond-authority', newBondAuthority.toBase58(), - '--revenue-share', - 43, '--confirmation-finality', 'confirmed', ], @@ -140,7 +136,7 @@ describe('Configure bond account using CLI', () => { const bondsData2 = await getBond(program, bondAccount) expect(bondsData2.authority).toEqual(newBondAuthority) - expect(bondsData2.revenueShare.hundredthBps).toEqual(43 * 10 ** 4) + expect(bondsData2.cpmpe).toEqual(33) }) it('configure bond in print-only mode', async () => { diff --git a/packages/validator-bonds-cli/__tests__/test-validator/fundBond.spec.ts b/packages/validator-bonds-cli/__tests__/test-validator/fundBond.spec.ts index 4b7c7a0c..46807497 100644 --- a/packages/validator-bonds-cli/__tests__/test-validator/fundBond.spec.ts +++ b/packages/validator-bonds-cli/__tests__/test-validator/fundBond.spec.ts @@ -51,17 +51,16 @@ describe('Fund bond account using CLI', () => { provider.connection.getAccountInfo(configAccount) ).resolves.not.toBeNull() const { voteAccount: voteAccountAddress, validatorIdentity } = - await createVoteAccount(provider) + await createVoteAccount({ provider }) voteAccount = voteAccountAddress - ;({ bondAccount } = await executeInitBondInstruction( + ;({ bondAccount } = await executeInitBondInstruction({ program, provider, - configAccount, - undefined, + config: configAccount, voteAccount, validatorIdentity, - 123 - )) + cpmpe: 123, + })) }) afterEach(async () => { diff --git a/packages/validator-bonds-cli/__tests__/test-validator/initBond.spec.ts b/packages/validator-bonds-cli/__tests__/test-validator/initBond.spec.ts index 423999c6..2ad11c57 100644 --- a/packages/validator-bonds-cli/__tests__/test-validator/initBond.spec.ts +++ b/packages/validator-bonds-cli/__tests__/test-validator/initBond.spec.ts @@ -98,8 +98,6 @@ describe('Init bond account using CLI', () => { validatorIdentityPath, '--bond-authority', bondAuthority.publicKey.toBase58(), - '--revenue-share', - '10', '--rent-payer', rentPayerPath, '--confirmation-finality', @@ -120,9 +118,9 @@ describe('Init bond account using CLI', () => { ) const bondsData = await getBond(program, bondAccount) expect(bondsData.config).toEqual(configAccount) - expect(bondsData.validatorVoteAccount).toEqual(voteAccount) + expect(bondsData.voteAccount).toEqual(voteAccount) expect(bondsData.authority).toEqual(bondAuthority.publicKey) - expect(bondsData.revenueShare.hundredthBps).toEqual(10 * 10 ** 4) + expect(bondsData.cpmpe).toEqual(0) expect(bondsData.bump).toEqual(bump) await expect( provider.connection.getBalance(rentPayerKeypair.publicKey) @@ -144,8 +142,6 @@ describe('Init bond account using CLI', () => { configAccount.toBase58(), '--vote-account', voteAccount.toBase58(), - '--revenue-share', - '1000', '--confirmation-finality', 'confirmed', ], @@ -164,9 +160,9 @@ describe('Init bond account using CLI', () => { ) const bondsData = await getBond(program, bondAccount) expect(bondsData.config).toEqual(configAccount) - expect(bondsData.validatorVoteAccount).toEqual(voteAccount) + expect(bondsData.voteAccount).toEqual(voteAccount) expect(bondsData.authority).toEqual(validatorIdentity.publicKey) - expect(bondsData.revenueShare.hundredthBps).toEqual(0) + expect(bondsData.cpmpe).toEqual(0) expect(bondsData.bump).toEqual(bump) }) diff --git a/packages/validator-bonds-cli/__tests__/test-validator/merge.spec.ts b/packages/validator-bonds-cli/__tests__/test-validator/merge.spec.ts index a8419a55..2fe863d8 100644 --- a/packages/validator-bonds-cli/__tests__/test-validator/merge.spec.ts +++ b/packages/validator-bonds-cli/__tests__/test-validator/merge.spec.ts @@ -81,12 +81,10 @@ describe('Merge stake accounts using CLI', () => { stdout: /successfully merged/, }) - const stakeAccount1Info = await provider.connection.getAccountInfo( - stakeAccount1 - ) - const stakeAccount2Info = await provider.connection.getAccountInfo( - stakeAccount2 - ) + const stakeAccount1Info = + await provider.connection.getAccountInfo(stakeAccount1) + const stakeAccount2Info = + await provider.connection.getAccountInfo(stakeAccount2) expect(stakeAccount1Info).toBeNull() expect(stakeAccount2Info).not.toBeNull() expect(stakeAccount2Info?.lamports).toEqual(LAMPORTS_PER_SOL * 5) @@ -177,12 +175,10 @@ async function createMergeStakeAccounts({ staker: bondWithdrawer, withdrawer: bondWithdrawer, }) - const stakeAccount1Info = await provider.connection.getAccountInfo( - stakeAccount1 - ) - const stakeAccount2Info = await provider.connection.getAccountInfo( - stakeAccount2 - ) + const stakeAccount1Info = + await provider.connection.getAccountInfo(stakeAccount1) + const stakeAccount2Info = + await provider.connection.getAccountInfo(stakeAccount2) expect(stakeAccount1Info).not.toBeNull() expect(stakeAccount2Info).not.toBeNull() expect(stakeAccount1Info?.lamports).toEqual(lamports1) diff --git a/packages/validator-bonds-cli/__tests__/test-validator/show.spec.ts b/packages/validator-bonds-cli/__tests__/test-validator/show.spec.ts index 0c917f5f..5f58bc32 100644 --- a/packages/validator-bonds-cli/__tests__/test-validator/show.spec.ts +++ b/packages/validator-bonds-cli/__tests__/test-validator/show.spec.ts @@ -212,17 +212,19 @@ describe('Show command using CLI', () => { expect( provider.connection.getAccountInfo(configAccount) ).resolves.not.toBeNull() - const { voteAccount, validatorIdentity } = await createVoteAccount(provider) + const { voteAccount, validatorIdentity } = await createVoteAccount({ + provider, + }) const bondAuthority = Keypair.generate() - const { bondAccount } = await executeInitBondInstruction( + const { bondAccount } = await executeInitBondInstruction({ program, provider, - configAccount, + config: configAccount, bondAuthority, voteAccount, validatorIdentity, - 222 - ) + cpmpe: 222, + }) const [, bump] = bondAddress(configAccount, voteAccount, program.programId) const expectedData = { @@ -230,12 +232,9 @@ describe('Show command using CLI', () => { publicKey: bondAccount.toBase58(), account: { config: configAccount.toBase58(), - validatorVoteAccount: voteAccount.toBase58(), + voteAccount: voteAccount.toBase58(), authority: bondAuthority.publicKey.toBase58(), - revenueShare: { hundredthBps: 222 }, bump, - // TODO: this is strange format - reserved: { reserved: [150] }, }, } @@ -299,7 +298,7 @@ describe('Show command using CLI', () => { '--program-id', program.programId.toBase58(), 'show-bond', - '--validator-vote-account', + '--vote-account', voteAccount.toBase58(), '-f', 'yaml', @@ -351,7 +350,7 @@ describe('Show command using CLI', () => { 'show-bond', '--config', configAccount.toBase58(), - '--validator-vote-account', + '--vote-account', voteAccount.toBase58(), '--bond-authority', bondAuthority.publicKey.toBase58(), @@ -378,7 +377,7 @@ describe('Show command using CLI', () => { '--program-id', program.programId.toBase58(), 'show-bond', - '--validator-vote-account', + '--vote-account', Keypair.generate().publicKey, '-f', 'yaml', diff --git a/packages/validator-bonds-cli/package.json b/packages/validator-bonds-cli/package.json index c4826e79..fe50e01c 100644 --- a/packages/validator-bonds-cli/package.json +++ b/packages/validator-bonds-cli/package.json @@ -26,21 +26,21 @@ "dependencies": { "@marinade.finance/validator-bonds-sdk": "^1.1.10", "@coral-xyz/anchor": "^0.29.0", - "@solana/web3.js": "^1.87.6", + "@solana/web3.js": "^1.89.1", "@marinade.finance/ledger-utils": "^3.0.1", - "@marinade.finance/cli-common": "^2.2.1", - "@marinade.finance/anchor-common": "^2.2.1", - "@marinade.finance/web3js-common": "^2.2.1", - "@marinade.finance/ts-common": "^2.2.1", + "@marinade.finance/cli-common": "^2.2.2", + "@marinade.finance/anchor-common": "^2.2.2", + "@marinade.finance/web3js-common": "^2.2.2", + "@marinade.finance/ts-common": "^2.2.2", "bn.js": "^5.2.1", "jsbi": "^4.3.0", - "commander": "^9.5.0", - "pino": "^8.16.1", - "pino-pretty": "^10.2.3", - "solana-spl-token-modern": "npm:@solana/spl-token@^0.3.8", - "yaml": "^2.3.3" + "commander": "^11.1.0", + "pino": "^8.17.2", + "pino-pretty": "^10.3.1", + "solana-spl-token-modern": "npm:@solana/spl-token@^0.3.11", + "yaml": "^2.3.4" }, "devDependencies": { - "@marinade.finance/jest-utils": "^2.2.1" + "@marinade.finance/jest-utils": "^2.2.2" } } diff --git a/packages/validator-bonds-cli/src/commands/manage/configureBond.ts b/packages/validator-bonds-cli/src/commands/manage/configureBond.ts index 45018b34..a50a016f 100644 --- a/packages/validator-bonds-cli/src/commands/manage/configureBond.ts +++ b/packages/validator-bonds-cli/src/commands/manage/configureBond.ts @@ -16,7 +16,6 @@ import { CONFIG_ADDRESS, configureBondInstruction, } from '@marinade.finance/validator-bonds-sdk' -import { toHundredsBps } from '@marinade.finance/validator-bonds-sdk/src/utils' import { Wallet as WalletInterface } from '@marinade.finance/web3js-common' export function installConfigureBond(program: Command) { @@ -55,11 +54,6 @@ export function installConfigureBond(program: Command) { 'New value of authority that is permitted to operate with bond account.', parsePubkeyOrPubkeyFromWallet ) - .option( - '--revenue-share ', - 'New value of the revenue share in percents (the precision is 1/10000 of the percent, use e.g. 1.0001).', - toHundredsBps - ) .action( async ( @@ -69,13 +63,11 @@ export function installConfigureBond(program: Command) { voteAccount, authority, bondAuthority, - revenueShare, }: { config?: Promise voteAccount?: Promise authority?: Promise bondAuthority?: Promise - revenueShare?: number } ) => { await manageConfigureBond({ @@ -84,7 +76,6 @@ export function installConfigureBond(program: Command) { voteAccount: await voteAccount, authority: await authority, newBondAuthority: await bondAuthority, - newRevenueShareHundredthBps: revenueShare, }) } ) @@ -96,14 +87,12 @@ async function manageConfigureBond({ voteAccount, authority, newBondAuthority, - newRevenueShareHundredthBps, }: { bondAccountAddress?: PublicKey config?: PublicKey voteAccount?: PublicKey authority?: WalletInterface | PublicKey newBondAuthority?: PublicKey - newRevenueShareHundredthBps?: number }) { const { program, @@ -128,9 +117,8 @@ async function manageConfigureBond({ program, bondAccount: bondAccountAddress, configAccount: config, - validatorVoteAccount: voteAccount, + voteAccount, authority, - newRevenueShareHundredthBps, newBondAuthority, }) tx.add(instruction) diff --git a/packages/validator-bonds-cli/src/commands/manage/fundBond.ts b/packages/validator-bonds-cli/src/commands/manage/fundBond.ts index 2c585469..b846a09e 100644 --- a/packages/validator-bonds-cli/src/commands/manage/fundBond.ts +++ b/packages/validator-bonds-cli/src/commands/manage/fundBond.ts @@ -114,7 +114,7 @@ async function manageFundBond({ program, bondAccount: bondAccountAddress, configAccount: config, - validatorVoteAccount: voteAccount, + voteAccount, stakeAccount, stakeAccountAuthority: stakeAuthority, }) diff --git a/packages/validator-bonds-cli/src/commands/manage/initBond.ts b/packages/validator-bonds-cli/src/commands/manage/initBond.ts index 084a2e88..3318c62d 100644 --- a/packages/validator-bonds-cli/src/commands/manage/initBond.ts +++ b/packages/validator-bonds-cli/src/commands/manage/initBond.ts @@ -16,7 +16,6 @@ import { getVoteAccount, initBondInstruction, } from '@marinade.finance/validator-bonds-sdk' -import { toHundredsBps } from '@marinade.finance/validator-bonds-sdk/src/utils' import { Wallet as WalletInterface } from '@marinade.finance/web3js-common' import { PublicKey, Signer } from '@solana/web3.js' @@ -38,7 +37,7 @@ export function installInitBond(program: Command) { .option( '--validator-identity ', 'Validator identity linked to the vote account. ' + - 'Permission-ed execution requires the validator identity signature. Then possible to set --bond-authority and --revenue-share. ' + + 'Permission-ed execution requires the validator identity signature, possible possible to configure --bond-authority. ' + 'Permission-less execution requires no signature, bond account configuration is possible later with validator identity signature (default: NONE)', parseWalletOrPubkey ) @@ -47,12 +46,6 @@ export function installInitBond(program: Command) { 'Authority that is permitted to operate with bond account (default: vote account validator identity)', parsePubkeyOrPubkeyFromWallet ) - .option( - '--revenue-share ', - 'Revenue share in percents (the precision is 1/10000 of the percent)', - toHundredsBps, - 0 - ) .option( '--rent-payer ', 'Rent payer for the account creation (default: wallet keypair)', @@ -65,14 +58,12 @@ export function installInitBond(program: Command) { voteAccount, validatorIdentity, bondAuthority, - revenueShare, rentPayer, }: { config?: Promise voteAccount: Promise validatorIdentity?: Promise bondAuthority: Promise - revenueShare: number rentPayer?: Promise }) => { await manageInitBond({ @@ -80,7 +71,6 @@ export function installInitBond(program: Command) { voteAccount: await voteAccount, validatorIdentity: await validatorIdentity, bondAuthority: await bondAuthority, - revenueShare: revenueShare, rentPayer: await rentPayer, }) } @@ -92,14 +82,12 @@ async function manageInitBond({ voteAccount, validatorIdentity, bondAuthority, - revenueShare, rentPayer, }: { config?: PublicKey voteAccount: PublicKey validatorIdentity?: WalletInterface | PublicKey bondAuthority: PublicKey - revenueShare: number rentPayer?: WalletInterface | PublicKey }) { const { @@ -135,9 +123,8 @@ async function manageInitBond({ program, configAccount: config, bondAuthority, - validatorVoteAccount: voteAccount, + voteAccount, validatorIdentity, - revenueShareHundredthBps: revenueShare, rentPayer, }) tx.add(instruction) diff --git a/packages/validator-bonds-cli/src/commands/show.ts b/packages/validator-bonds-cli/src/commands/show.ts index cb901ec8..726bd6ab 100644 --- a/packages/validator-bonds-cli/src/commands/show.ts +++ b/packages/validator-bonds-cli/src/commands/show.ts @@ -7,6 +7,7 @@ import { FormatType, reformat, reformatReserved, + ReformatAction, } from '@marinade.finance/cli-common' import { PublicKey } from '@solana/web3.js' import { Command } from 'commander' @@ -88,7 +89,7 @@ export function installShowBond(program: Command) { parsePubkeyOrPubkeyFromWallet ) .option( - '--validator-vote-account ', + '--vote-account ', 'Validator vote account to filter the bond accounts with', parsePubkeyOrPubkeyFromWallet ) @@ -107,12 +108,12 @@ export function installShowBond(program: Command) { address: Promise, { config, - validatorVoteAccount, + voteAccount, bondAuthority, format, }: { config?: Promise - validatorVoteAccount?: Promise + voteAccount?: Promise bondAuthority?: Promise format: FormatType } @@ -120,7 +121,7 @@ export function installShowBond(program: Command) { await showBond({ address: await address, config: await config, - validatorVoteAccount: await validatorVoteAccount, + voteAccount: await voteAccount, bondAuthority: await bondAuthority, format, }) @@ -204,13 +205,13 @@ async function showConfig({ async function showBond({ address, config, - validatorVoteAccount, + voteAccount, bondAuthority, format, }: { address?: PublicKey config?: PublicKey - validatorVoteAccount?: PublicKey + voteAccount?: PublicKey bondAuthority?: PublicKey format: FormatType }) { @@ -241,7 +242,7 @@ async function showBond({ const foundData = await findBonds({ program, config, - validatorVoteAccount, + voteAccount, bondAuthority, }) data = foundData.map(bondData => ({ @@ -251,15 +252,15 @@ async function showBond({ })) } catch (err) { throw new CliCommandError({ - valueName: '--config|--validator-vote-account|--bond-authority', - value: `${config?.toBase58()}}|${validatorVoteAccount?.toBase58()}|${bondAuthority?.toBase58()}}`, + valueName: '--config|--vote-account|--bond-authority', + value: `${config?.toBase58()}}|${voteAccount?.toBase58()}|${bondAuthority?.toBase58()}}`, msg: 'Error while fetching bond account based on filter parameters', cause: err as Error, }) } } - const reformatted = reformat(data, reformatReserved) + const reformatted = reformat(data, reformatBonds) print_data(reformatted, format) } @@ -270,3 +271,18 @@ async function showEvent({ eventData }: { eventData: string }) { const reformattedData = reformat(decodedData) print_data(reformattedData, 'text') } + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function reformatBonds(key: string, value: any): ReformatAction { + if ( + typeof key === 'string' && + (key as string).startsWith('reserved') && + (Array.isArray(value) || value instanceof Uint8Array) + ) { + return { type: 'Remove' } + } + if (key.toLowerCase() === 'cpmpe') { + return { type: 'Remove' } + } + return { type: 'UsePassThrough' } +} diff --git a/packages/validator-bonds-cli/src/context.ts b/packages/validator-bonds-cli/src/context.ts index 3a9751cf..8e3fc452 100644 --- a/packages/validator-bonds-cli/src/context.ts +++ b/packages/validator-bonds-cli/src/context.ts @@ -126,9 +126,8 @@ export async function setProgramIdByOwner( ): Promise { const cliContext = getCliContext() if (cliContext.programId === undefined && accountPubkey !== undefined) { - const accountInfo = await cliContext.provider.connection.getAccountInfo( - accountPubkey - ) + const accountInfo = + await cliContext.provider.connection.getAccountInfo(accountPubkey) if (accountInfo === null) { throw new Error( `setProgramIdByOwner: account ${accountPubkey.toBase58()} does not exist` + diff --git a/packages/validator-bonds-sdk/__tests__/bankrun/bankrun.ts b/packages/validator-bonds-sdk/__tests__/bankrun/bankrun.ts index 1aa660dd..63dd0ca2 100644 --- a/packages/validator-bonds-sdk/__tests__/bankrun/bankrun.ts +++ b/packages/validator-bonds-sdk/__tests__/bankrun/bankrun.ts @@ -1,5 +1,9 @@ import { Wallet as WalletInterface } from '@coral-xyz/anchor/dist/cjs/provider' -import { ValidatorBondsProgram, getProgram } from '../../src' +import { + ValidatorBondsProgram, + checkAndGetBondAddress, + getProgram, +} from '../../src' import { BanksTransactionMeta, startAnchor } from 'solana-bankrun' import { BankrunProvider } from 'anchor-bankrun' import { @@ -12,6 +16,8 @@ import { } from '@solana/web3.js' import { instanceOfWallet } from '@marinade.finance/web3js-common' import { ExtendedProvider } from '../utils/provider' +import { delegatedStakeAccount } from '../utils/staking' +import { executeFundBondInstruction } from '../utils/testTransactions' export class BankrunExtendedProvider extends BankrunProvider @@ -109,18 +115,62 @@ export function warpToEpoch(provider: BankrunProvider, epoch: number) { const epochBigInt = BigInt(epoch) const { slotsPerEpoch, firstNormalEpoch, firstNormalSlot } = provider.context.genesisConfig.epochSchedule - let warpToEpoch: bigint + let warpToSlot: bigint if (epochBigInt <= firstNormalEpoch) { - warpToEpoch = BigInt(((2 ^ epoch) - 1) * MINIMUM_SLOTS_PER_EPOCH) + warpToSlot = BigInt((2 ** epoch - 1) * MINIMUM_SLOTS_PER_EPOCH) } else { - warpToEpoch = + warpToSlot = (epochBigInt - firstNormalEpoch) * slotsPerEpoch + firstNormalSlot } - provider.context.warpToSlot(warpToEpoch) + provider.context.warpToSlot(warpToSlot) } export async function warpToNextEpoch(provider: BankrunProvider) { - const nextEpoch = - Number((await provider.context.banksClient.getClock()).epoch) + 1 + await warpOffsetEpoch(provider, 1) +} + +export async function warpOffsetEpoch( + provider: BankrunProvider, + plusEpochs: number +) { + const nextEpoch = (await currentEpoch(provider)) + plusEpochs warpToEpoch(provider, nextEpoch) } + +export async function currentEpoch(provider: BankrunProvider): Promise { + return Number((await provider.context.banksClient.getClock()).epoch) +} + +// this cannot be in generic testTransactions.ts because of warping requires BankrunProvider +export async function delegateAndFund({ + program, + provider, + lamports, + voteAccount, + bond, + config, +}: { + program: ValidatorBondsProgram + provider: BankrunExtendedProvider + lamports: number + voteAccount: PublicKey + bond?: PublicKey + config?: PublicKey +}): Promise<{ stakeAccount: PublicKey }> { + const { stakeAccount, withdrawer: stakeAccountWithdrawer } = + await delegatedStakeAccount({ + provider, + lamports, + voteAccountToDelegate: voteAccount, + }) + bond = checkAndGetBondAddress(bond, config, voteAccount, program.programId) + await warpToNextEpoch(provider) // activating stake account + await executeFundBondInstruction({ + program, + provider, + bondAccount: bond, + stakeAccount, + stakeAccountAuthority: stakeAccountWithdrawer, + }) + return { stakeAccount } +} diff --git a/packages/validator-bonds-sdk/__tests__/bankrun/cancelWithdrawRequest.spec.ts b/packages/validator-bonds-sdk/__tests__/bankrun/cancelWithdrawRequest.spec.ts index 0e4b7a9e..4d6a0736 100644 --- a/packages/validator-bonds-sdk/__tests__/bankrun/cancelWithdrawRequest.spec.ts +++ b/packages/validator-bonds-sdk/__tests__/bankrun/cancelWithdrawRequest.spec.ts @@ -57,7 +57,11 @@ describe('Validator Bonds cancel withdraw request', () => { bondAccount, validatorIdentity: nodeIdentity, bondAuthority: bondAuth, - } = await executeInitBondInstruction(program, provider, config.publicKey) + } = await executeInitBondInstruction({ + program, + provider, + config: config.publicKey, + }) bondAuthority = bondAuth validatorIdentity = nodeIdentity bond = { @@ -80,9 +84,8 @@ describe('Validator Bonds cancel withdraw request', () => { expect(rentCollectorInfo).not.toBeNull() assert(rentCollectorInfo !== null) expect(rentCollectorInfo.lamports).toEqual(LAMPORTS_PER_SOL) - const withdrawRequestInfo = await provider.connection.getAccountInfo( - withdrawRequest - ) + const withdrawRequestInfo = + await provider.connection.getAccountInfo(withdrawRequest) assert(withdrawRequestInfo !== null) const rentExempt = await provider.connection.getMinimumBalanceForRentExemption( @@ -214,7 +217,7 @@ describe('Validator Bonds cancel withdraw request', () => { bondAccount, configAccount: config.publicKey, authority: valIdent, - validatorVoteAccount: voteAccount, + voteAccount, })) await provider.sendIx([valIdent!], instruction) await assertNotExist(provider, withdrawRequest) @@ -230,7 +233,7 @@ describe('Validator Bonds cancel withdraw request', () => { ;({ instruction } = await cancelWithdrawRequestInstruction({ program, configAccount: config.publicKey, - validatorVoteAccount: voteAccount, + voteAccount, })) await provider.sendIx([bondIdent!], instruction) await assertNotExist(provider, withdrawRequest) diff --git a/packages/validator-bonds-sdk/__tests__/bankrun/claimSettlement.spec.ts b/packages/validator-bonds-sdk/__tests__/bankrun/claimSettlement.spec.ts new file mode 100644 index 00000000..6e5e7269 --- /dev/null +++ b/packages/validator-bonds-sdk/__tests__/bankrun/claimSettlement.spec.ts @@ -0,0 +1,454 @@ +import { + Config, + Errors, + MerkleTreeNode, + SETTLEMENT_CLAIM_SEED, + ValidatorBondsProgram, + claimSettlementInstruction, + fundSettlementInstruction, + getConfig, + getSettlementClaim, + settlementClaimAddress, + withdrawerAuthority, +} from '../../src' +import { + BankrunExtendedProvider, + assertNotExist, + currentEpoch, + initBankrunTest, + warpOffsetEpoch, + warpToNextEpoch, +} from './bankrun' +import { + computeUnitIx, + createUserAndFund, + executeInitBondInstruction, + executeInitConfigInstruction, + executeInitSettlement, +} from '../utils/testTransactions' +import { ProgramAccount } from '@coral-xyz/anchor' +import { + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + SYSVAR_STAKE_HISTORY_PUBKEY, + SYSVAR_CLOCK_PUBKEY, + StakeProgram, + SystemProgram, +} from '@solana/web3.js' +import { + createBondsFundedStakeAccount, + createVoteAccount, +} from '../utils/staking' +import { signer, pubkey } from '@marinade.finance/web3js-common' +import { + MERKLE_ROOT_BUF, + configAccountKeypair, + totalClaimVoteAccount1, + totalClaimVoteAccount2, + treeNodeBy, + voteAccount1Keypair, + voteAccount2Keypair, + withdrawer1, + withdrawer1Keypair, + withdrawer2, + withdrawer2Keypair, + withdrawer3, + withdrawer3Keypair, +} from '../utils/merkleTreeTestData' +import { checkErrorMessage, verifyError } from '@marinade.finance/anchor-common' +import BN from 'bn.js' + +describe('Validator Bonds claim settlement', () => { + const epochsToClaimSettlement = 3 + let provider: BankrunExtendedProvider + let program: ValidatorBondsProgram + let config: ProgramAccount + let bondAccount: PublicKey + let operatorAuthority: Keypair + let validatorIdentity1: Keypair + let voteAccount1: PublicKey + let validatorIdentity2: Keypair + let voteAccount2: PublicKey + let settlementAccount1: PublicKey + let settlementAccount2: PublicKey + let settlementEpoch: number + let rentCollector: Keypair + let stakeAccount1: PublicKey + let stakeAccount2: PublicKey + + beforeAll(async () => { + ;({ provider, program } = await initBankrunTest()) + const { configAccount, operatorAuthority: operatorAuth } = + await executeInitConfigInstruction({ + program, + provider, + epochsToClaimSettlement, + configAccountKeypair: configAccountKeypair, + }) + operatorAuthority = operatorAuth + config = { + publicKey: configAccount, + account: await getConfig(program, configAccount), + } + ;({ voteAccount: voteAccount1, validatorIdentity: validatorIdentity1 } = + await createVoteAccount({ + voteAccount: voteAccount1Keypair, + provider, + })) + await executeInitBondInstruction({ + program, + provider, + config: config.publicKey, + voteAccount: voteAccount1, + validatorIdentity: validatorIdentity1, + }) + ;({ voteAccount: voteAccount2, validatorIdentity: validatorIdentity2 } = + await createVoteAccount({ + voteAccount: voteAccount2Keypair, + provider, + })) + ;({ bondAccount } = await executeInitBondInstruction({ + program, + provider, + config: config.publicKey, + voteAccount: voteAccount2, + validatorIdentity: validatorIdentity2, + })) + + rentCollector = Keypair.generate() + settlementEpoch = await currentEpoch(provider) + ;({ settlementAccount: settlementAccount1 } = await executeInitSettlement({ + config: config.publicKey, + program, + provider, + voteAccount: voteAccount1, + operatorAuthority, + currentEpoch: settlementEpoch, + rentCollector: rentCollector.publicKey, + merkleRoot: MERKLE_ROOT_BUF, + maxMerkleNodes: 1, + maxTotalClaim: totalClaimVoteAccount1, + })) + ;({ settlementAccount: settlementAccount2 } = await executeInitSettlement({ + config: config.publicKey, + program, + provider, + voteAccount: voteAccount2, + operatorAuthority, + currentEpoch: settlementEpoch, + merkleRoot: MERKLE_ROOT_BUF, + // wrongly setup to be able to get errors from contract + maxMerkleNodes: 1, + maxTotalClaim: 100, // has to be lower than 111111 + })) + stakeAccount1 = await createBondsFundedStakeAccount({ + program, + provider, + config: config.publicKey, + voteAccount: voteAccount1, + lamports: totalClaimVoteAccount1.toNumber() + LAMPORTS_PER_SOL * 5, + }) + stakeAccount2 = await createBondsFundedStakeAccount({ + program, + provider, + config: config.publicKey, + voteAccount: voteAccount2, + lamports: totalClaimVoteAccount2.toNumber() + LAMPORTS_PER_SOL * 6, + }) + + await warpToNextEpoch(provider) // activate stake account + + const { instruction: fundIx1, splitStakeAccount: split1 } = + await fundSettlementInstruction({ + program, + settlementAccount: settlementAccount1, + stakeAccount: stakeAccount1, + }) + const { instruction: fundIx2, splitStakeAccount: split2 } = + await fundSettlementInstruction({ + program, + settlementAccount: settlementAccount2, + stakeAccount: stakeAccount2, + }) + await provider.sendIx( + [signer(split1), signer(split2), operatorAuthority], + fundIx1, + fundIx2 + ) + if ((await provider.context.banksClient.getAccount(withdrawer1)) === null) { + await createUserAndFund(provider, LAMPORTS_PER_SOL, withdrawer1Keypair) + await createUserAndFund(provider, LAMPORTS_PER_SOL, withdrawer2Keypair) + await createUserAndFund(provider, LAMPORTS_PER_SOL, withdrawer3Keypair) + } + }) + + it('claim settlement various', async () => { + const treeNode1Withdrawer1 = treeNodeBy(voteAccount1, withdrawer1) + const { instruction: ixWrongTreeNode } = await claimSettlementInstruction({ + program, + claimAmount: treeNode1Withdrawer1.treeNode.data.claim.subn(1), + merkleProof: treeNode1Withdrawer1.proof, + withdrawer: withdrawer1, + settlementAccount: settlementAccount1, + stakeAccount: stakeAccount1, + }) + try { + await provider.sendIx([], computeUnitIx, ixWrongTreeNode) + throw new Error('should have failed; wrong tree node proof') + } catch (e) { + verifyError(e, Errors, 6029, 'claim proof failed') + } + + const rentPayer = await createUserAndFund(provider, LAMPORTS_PER_SOL) + const { instruction, settlementClaimAccount } = + await claimSettlementInstruction({ + program, + claimAmount: treeNode1Withdrawer1.treeNode.data.claim, + merkleProof: treeNode1Withdrawer1.proof, + withdrawer: withdrawer1, + settlementAccount: settlementAccount1, + stakeAccount: stakeAccount1, + rentPayer: rentPayer, + }) + try { + await provider.sendIx([signer(rentPayer)], computeUnitIx, instruction) + throw new Error('should have failed; stake is not deactivated') + } catch (e) { + expect(checkErrorMessage(e, 'insufficient funds')).toBeTruthy() + } + + warpToNextEpoch(provider) // deactivate stake account + + await provider.sendIx([signer(rentPayer)], computeUnitIx, instruction) + + const [bondsWithdrawerAuthority] = withdrawerAuthority( + configAccountKeypair.publicKey, + program.programId + ) + const [settlementClaimAddr, bump] = settlementClaimAddress( + { + settlement: settlementAccount1, + stakeAuthority: bondsWithdrawerAuthority, + voteAccount: voteAccount1, + withdrawAuthority: withdrawer1, + claim: treeNode1Withdrawer1.treeNode.data.claim, + }, + program.programId + ) + expect(settlementClaimAccount).toEqual(settlementClaimAddr) + const settlementClaim = await getSettlementClaim( + program, + settlementClaimAccount + ) + expect(settlementClaim.amount).toEqual( + treeNode1Withdrawer1.treeNode.data.claim + ) + expect(settlementClaim.bump).toEqual(bump) + expect(settlementClaim.rentCollector).toEqual(pubkey(rentPayer)) + expect(settlementClaim.settlement).toEqual(pubkey(settlementAccount1)) + expect(settlementClaim.stakeAuthority).toEqual(bondsWithdrawerAuthority) + expect(settlementClaim.voteAccount).toEqual(pubkey(voteAccount1)) + expect(settlementClaim.withdrawAuthority).toEqual(withdrawer1) + const settlementClaimAccountInfo = await provider.connection.getAccountInfo( + settlementClaimAccount + ) + expect( + (await provider.connection.getAccountInfo(pubkey(rentPayer)))?.lamports + ).toEqual(LAMPORTS_PER_SOL - settlementClaimAccountInfo!.lamports) + + await warpToNextEpoch(provider) + + try { + await provider.sendIx([signer(rentPayer)], computeUnitIx, instruction) + throw new Error('should have failed; already claimed') + } catch (e) { + expect((e as Error).message).toMatch('custom program error: 0x0') + } + + try { + const wrongBumpIx = await claimSettlementWrongBump({ + proof: treeNode1Withdrawer1.proof, + claim: treeNode1Withdrawer1.treeNode.data.claim, + withdrawer: withdrawer1, + configAccount: config.publicKey, + bondAccount: bondAccount, + settlementAccount: settlementAccount1, + stakeAccount: stakeAccount1, + voteAccount: voteAccount1, + }) + await provider.sendIx([signer(rentPayer)], wrongBumpIx, instruction) + throw new Error('should have failed; already claimed') + } catch (e) { + expect((e as Error).message).toMatch('custom program error: 0x7d6') + } + + const treeNode1Withdrawer2 = treeNodeBy(voteAccount1, withdrawer2) + const { instruction: ixWrongMerkleTreeNodes } = + await claimSettlementInstruction({ + program, + claimAmount: treeNode1Withdrawer2.treeNode.data.claim, + merkleProof: treeNode1Withdrawer2.proof, + withdrawer: withdrawer2, + settlementAccount: settlementAccount1, + stakeAccount: stakeAccount1, + }) + try { + await provider.sendIx([], computeUnitIx, ixWrongMerkleTreeNodes) + throw new Error('should have failed; wrong stake account') + } catch (e) { + verifyError(e, Errors, 6033, 'exceeded number of claimable nodes') + } + + const treeNode2Withdrawer2 = treeNodeBy(voteAccount2, withdrawer2) + const { instruction: treeNode2Withdrawer2Ix } = + await claimSettlementInstruction({ + program, + claimAmount: treeNode2Withdrawer2.treeNode.data.claim, + merkleProof: treeNode2Withdrawer2.proof, + withdrawer: withdrawer2, + settlementAccount: settlementAccount2, + stakeAccount: stakeAccount2, + }) + try { + await provider.sendIx([], computeUnitIx, treeNode2Withdrawer2Ix) + throw new Error( + 'should have failed; over claimed (wrong argument on settlement)' + ) + } catch (e) { + verifyError(e, Errors, 6032, 'the max total claim') + } + + const treeNode2Withdrawer1 = treeNodeBy(voteAccount2, withdrawer1) + const { instruction: ixWrongStakeAccount } = + await claimSettlementInstruction({ + program, + claimAmount: treeNode2Withdrawer1.treeNode.data.claim, + merkleProof: treeNode2Withdrawer1.proof, + withdrawer: withdrawer1, + settlementAccount: settlementAccount2, + stakeAccount: stakeAccount1, + }) + try { + await provider.sendIx([], computeUnitIx, ixWrongStakeAccount) + throw new Error('should have failed; wrong stake account') + } catch (e) { + verifyError(e, Errors, 6036, 'not funded under the settlement') + } + + const { instruction: treeNode2Withdrawer1Ix } = + await claimSettlementInstruction({ + program, + claimAmount: treeNode2Withdrawer1.treeNode.data.claim, + merkleProof: treeNode2Withdrawer1.proof, + withdrawer: withdrawer1, + settlementAccount: settlementAccount2, + stakeAccount: stakeAccount2, + }) + await provider.sendIx([], computeUnitIx, treeNode2Withdrawer1Ix) + + await warpToNotBeClaimable() + + const treeNode1Withdrawer3 = treeNodeBy(voteAccount1, withdrawer3) + const { instruction: ixTooLate, settlementClaimAccount: accTooLate } = + await claimSettlementInstruction({ + program, + claimAmount: treeNode1Withdrawer3.treeNode.data.claim, + merkleProof: treeNode1Withdrawer3.proof, + withdrawer: withdrawer3, + settlementAccount: settlementAccount1, + stakeAccount: stakeAccount1, + }) + try { + await provider.sendIx([], computeUnitIx, ixTooLate) + throw new Error('should have failed; too late to claim') + } catch (e) { + verifyError(e, Errors, 6023, 'already expired') + } + assertNotExist(provider, accTooLate) + }) + + async function warpToNotBeClaimable() { + await warpOffsetEpoch(provider, epochsToClaimSettlement + 1) + } + + async function claimSettlementWrongBump({ + proof, + claim, + withdrawer, + configAccount, + bondAccount, + settlementAccount, + stakeAccount, + voteAccount, + }: { + proof: number[][] + claim: BN | number + withdrawer: PublicKey + configAccount: PublicKey + bondAccount: PublicKey + settlementAccount: PublicKey + stakeAccount: PublicKey + voteAccount: PublicKey + }) { + const [bondsWithdrawerAuthority] = withdrawerAuthority( + configAccount, + program.programId + ) + let [, bump] = settlementClaimAddress( + { + settlement: settlementAccount, + stakeAuthority: bondsWithdrawerAuthority, + voteAccount, + withdrawAuthority: withdrawer, + claim, + }, + program.programId + ) + let settlementAccountWrongBump: PublicKey | undefined + const seeds = [ + SETTLEMENT_CLAIM_SEED, + settlementAccount.toBytes(), + MerkleTreeNode.hash({ + stakeAuthority: bondsWithdrawerAuthority.toBase58(), + withdrawAuthority: withdrawer.toBase58(), + voteAccount: voteAccount.toBase58(), + claim: claim, + }).buffer, + ] + while (settlementAccountWrongBump === undefined && bump > 0) { + bump-- + const seedsWithBump = seeds.concat(Buffer.from([bump])) + try { + settlementAccountWrongBump = PublicKey.createProgramAddressSync( + seedsWithBump, + program.programId + ) + } catch (e) { + if (e instanceof TypeError) { + throw e + } + } + } + // console.log('correct claim settlement', correct, 'wrong bump', settlementAccountWrongBump?.toBase58(), 'with bump', bump) + return await program.methods + .claimSettlement({ + proof, + claim: new BN(claim), + }) + .accounts({ + withdrawAuthority: withdrawer, + config: configAccount, + bond: bondAccount, + settlement: settlementAccount, + settlementClaim: settlementAccountWrongBump, + stakeAccount, + rentPayer: provider.walletPubkey, + systemProgram: SystemProgram.programId, + stakeHistory: SYSVAR_STAKE_HISTORY_PUBKEY, + clock: SYSVAR_CLOCK_PUBKEY, + stakeProgram: StakeProgram.programId, + }) + .instruction() + } +}) diff --git a/packages/validator-bonds-sdk/__tests__/bankrun/claimWithdrawRequest.spec.ts b/packages/validator-bonds-sdk/__tests__/bankrun/claimWithdrawRequest.spec.ts index abc6fdf3..164fc3a8 100644 --- a/packages/validator-bonds-sdk/__tests__/bankrun/claimWithdrawRequest.spec.ts +++ b/packages/validator-bonds-sdk/__tests__/bankrun/claimWithdrawRequest.spec.ts @@ -13,13 +13,14 @@ import { import { BankrunExtendedProvider, assertNotExist, + delegateAndFund, initBankrunTest, + warpOffsetEpoch, warpToEpoch, warpToNextEpoch, } from './bankrun' import { createUserAndFund, - executeFundBondInstruction, executeInitBondInstruction, executeInitConfigInstruction, executeInitWithdrawRequestInstruction, @@ -72,7 +73,11 @@ describe('Validator Bonds claim withdraw request', () => { validatorIdentity: nodeIdentity, voteAccount: voteAcc, bondAuthority: bondAuth, - } = await executeInitBondInstruction(program, provider, config.publicKey) + } = await executeInitBondInstruction({ + program, + provider, + config: config.publicKey, + }) voteAccount = voteAcc bondAuthority = bondAuth validatorIdentity = nodeIdentity @@ -93,7 +98,7 @@ describe('Validator Bonds claim withdraw request', () => { await createStakeAccountAndInitWithdraw(initAmount, requestedAmount) let withdrawRequestData = await getWithdrawRequest(program, withdrawRequest) - expect(withdrawRequestData.validatorVoteAccount).toEqual(voteAccount) + expect(withdrawRequestData.voteAccount).toEqual(voteAccount) expect(withdrawRequestData.withdrawnAmount).toEqual(0) expect(withdrawRequestData.requestedAmount).toEqual(requestedAmount) @@ -112,7 +117,7 @@ describe('Validator Bonds claim withdraw request', () => { await provider.sendIx([splitStakeAccount, validatorIdentity], instruction) throw new Error('Expected withdraw request should not be elapsed') } catch (err) { - verifyError(err, Errors, 6019, 'Withdraw request has not elapsed') + verifyError(err, Errors, 6021, 'Withdraw request has not elapsed') } // withdrawLockupEpochs is 1, then the warp should make the withdraw request unlocked @@ -124,9 +129,8 @@ describe('Validator Bonds claim withdraw request', () => { expect(withdrawRequestData.withdrawnAmount).toEqual(requestedAmount) expect(withdrawRequestData.requestedAmount).toEqual(requestedAmount) - const originalStakeAccountInfo = await provider.connection.getAccountInfo( - stakeAccount - ) + const originalStakeAccountInfo = + await provider.connection.getAccountInfo(stakeAccount) expect(originalStakeAccountInfo?.lamports).toEqual(requestedAmount) assert( @@ -231,7 +235,7 @@ describe('Validator Bonds claim withdraw request', () => { stakeAccount, }) - await warpToUnlock() + await warpToUnlockClaiming() await provider.sendIx([splitStakeAccount, validatorIdentity], instruction) // withdraw request exists until is cancelled @@ -242,9 +246,8 @@ describe('Validator Bonds claim withdraw request', () => { expect(withdrawRequestData.withdrawnAmount).toEqual(requestedAmount) expect(withdrawRequestData.requestedAmount).toEqual(requestedAmount) - const originalStakeAccountInfo = await provider.connection.getAccountInfo( - stakeAccount - ) + const originalStakeAccountInfo = + await provider.connection.getAccountInfo(stakeAccount) expect(originalStakeAccountInfo?.lamports).toEqual(requestedAmount) assert( @@ -294,7 +297,7 @@ describe('Validator Bonds claim withdraw request', () => { await provider.sendIx([splitStakeAccount, validatorIdentity], instruction) throw new Error('failure expected; already claimed') } catch (err) { - verifyError(err, Errors, 6047, 'already fulfilled') + verifyError(err, Errors, 6049, 'already fulfilled') } }) @@ -317,18 +320,17 @@ describe('Validator Bonds claim withdraw request', () => { stakeAccount, configAccount: config.publicKey, splitStakeRentPayer: rentPayerUser, - validatorVoteAccount: voteAccount, + voteAccount, withdrawer: pubkey(withdrawer), }) - await warpToUnlock() + await warpToUnlockClaiming() await provider.sendIx( [splitStakeAccount, signer(rentPayerUser), bondAuthority], instruction ) - const originalStakeAccountInfo = await provider.connection.getAccountInfo( - stakeAccount - ) + const originalStakeAccountInfo = + await provider.connection.getAccountInfo(stakeAccount) expect(originalStakeAccountInfo?.lamports).toEqual(requestedAmount) assert(originalStakeAccountInfo !== null) const originalStakeAccountData = deserializeStakeState( @@ -360,13 +362,21 @@ describe('Validator Bonds claim withdraw request', () => { const stakeAmount = 320 * LAMPORTS_PER_SOL const { withdrawRequest, stakeAccount } = await createStakeAccountAndInitWithdraw(stakeAmount, requestedAmount) - const { stakeAccount: stakeAccountCannotSplit } = await delegateAndFund( - 2 * LAMPORTS_PER_SOL - ) - const { stakeAccount: stakeAccountCannotSplit2 } = await delegateAndFund( - 3 * LAMPORTS_PER_SOL - ) - await warpToUnlock() + const { stakeAccount: stakeAccountCannotSplit } = await delegateAndFund({ + program, + provider, + lamports: 2 * LAMPORTS_PER_SOL, + voteAccount, + bond: bond.publicKey, + }) + const { stakeAccount: stakeAccountCannotSplit2 } = await delegateAndFund({ + program, + provider, + lamports: 3 * LAMPORTS_PER_SOL, + voteAccount, + bond: bond.publicKey, + }) + await warpToUnlockClaiming() // partially fulfill const { instruction: ix1, splitStakeAccount: split1 } = @@ -389,7 +399,7 @@ describe('Validator Bonds claim withdraw request', () => { await provider.sendIx([splitCannotSplit, bondAuthority], ixCannotSplit) throw new Error('failure expected; cannot split') } catch (err) { - verifyError(err, Errors, 6029, 'Stake account is not big enough') + verifyError(err, Errors, 6031, 'Stake account is not big enough') } const { @@ -408,7 +418,7 @@ describe('Validator Bonds claim withdraw request', () => { ) throw new Error('failure expected; cannot split') } catch (err) { - verifyError(err, Errors, 6046, 'cancel and init new one') + verifyError(err, Errors, 6048, 'cancel and init new one') } }) @@ -428,12 +438,12 @@ describe('Validator Bonds claim withdraw request', () => { stakeAccount, }) - await warpToUnlock() + await warpToUnlockClaiming() try { await provider.sendIx([splitStakeAccount, validatorIdentity], instruction) throw new Error('is less to 1 SOL: should fail as split is not possible') } catch (err) { - verifyError(err, Errors, 6029, 'not big enough to be split') + verifyError(err, Errors, 6031, 'not big enough to be split') } }) @@ -444,8 +454,20 @@ describe('Validator Bonds claim withdraw request', () => { const stake3Amount = 1.5 * LAMPORTS_PER_SOL const { withdrawRequest, stakeAccount: stakeAccount1 } = await createStakeAccountAndInitWithdraw(stake1Amount, requestedAmount) - const { stakeAccount: stakeAccount2 } = await delegateAndFund(stake2Amount) - const { stakeAccount: stakeAccount3 } = await delegateAndFund(stake3Amount) + const { stakeAccount: stakeAccount2 } = await delegateAndFund({ + program, + provider, + lamports: stake2Amount, + voteAccount, + bond: bond.publicKey, + }) + const { stakeAccount: stakeAccount3 } = await delegateAndFund({ + program, + provider, + lamports: stake3Amount, + voteAccount, + bond: bond.publicKey, + }) const { instruction: ix1, splitStakeAccount: split1 } = await claimWithdrawRequestInstruction({ @@ -472,7 +494,7 @@ describe('Validator Bonds claim withdraw request', () => { stakeAccount: stakeAccount3, }) - await warpToUnlock() + await warpToUnlockClaiming() await provider.sendIx( [split1, split2, split3, validatorIdentity], ix1, @@ -505,7 +527,7 @@ describe('Validator Bonds claim withdraw request', () => { 2 * LAMPORTS_PER_SOL, 10 * LAMPORTS_PER_SOL ) - await warpToUnlock() + await warpToUnlockClaiming() const { instruction, splitStakeAccount } = await claimWithdrawRequestInstruction({ program, @@ -526,7 +548,7 @@ describe('Validator Bonds claim withdraw request', () => { const { stakeAccount: nonDelegatedStakeAccount } = await initializedStakeAccount(provider) const { withdrawRequest } = await initWithdrawRequest(2 * LAMPORTS_PER_SOL) - await warpToUnlock() + await warpToUnlockClaiming() const { instruction, splitStakeAccount } = await claimWithdrawRequestInstruction({ program, @@ -540,13 +562,13 @@ describe('Validator Bonds claim withdraw request', () => { await provider.sendIx([splitStakeAccount, validatorIdentity], instruction) throw new Error('failure expected; stake account is not delegated') } catch (e) { - verifyError(e, Errors, 6017, 'cannot be used for bonds') + verifyError(e, Errors, 6019, 'cannot be used for bonds') } }) it('cannot claim with wrong delegation', async () => { const { withdrawRequest } = await initWithdrawRequest(4 * LAMPORTS_PER_SOL) - await warpToUnlock() + await warpToUnlockClaiming() const { stakeAccount } = await delegatedStakeAccount({ provider, @@ -566,7 +588,7 @@ describe('Validator Bonds claim withdraw request', () => { await provider.sendIx([splitStakeAccount, validatorIdentity], instruction) throw new Error('failure expected as not activated') } catch (e) { - verifyError(e, Errors, 6023, 'not fully activated') + verifyError(e, Errors, 6025, 'not fully activated') } await warpToNextEpoch(provider) @@ -574,7 +596,7 @@ describe('Validator Bonds claim withdraw request', () => { await provider.sendIx([splitStakeAccount, validatorIdentity], instruction) throw new Error('failure expected as delegated to wrong validator') } catch (e) { - verifyError(e, Errors, 6018, 'delegated to a wrong validator') + verifyError(e, Errors, 6020, 'delegated to a wrong validator') } }) @@ -595,7 +617,7 @@ describe('Validator Bonds claim withdraw request', () => { voteAccountToDelegate: voteAccount, }) const { withdrawRequest } = await initWithdrawRequest(4 * LAMPORTS_PER_SOL) - await warpToUnlock() + await warpToUnlockClaiming() const { instruction, splitStakeAccount } = await claimWithdrawRequestInstruction({ @@ -610,7 +632,7 @@ describe('Validator Bonds claim withdraw request', () => { await provider.sendIx([splitStakeAccount, validatorIdentity], instruction) throw new Error('failure expected; wrong withdrawer') } catch (e) { - verifyError(e, Errors, 6010, 'Wrong withdrawer authority') + verifyError(e, Errors, 6012, 'Wrong withdrawer authority') } await authorizeStakeAccount({ @@ -624,7 +646,7 @@ describe('Validator Bonds claim withdraw request', () => { await provider.sendIx([splitStakeAccount, validatorIdentity], instruction) throw new Error('failure expected; wrong withdrawer') } catch (e) { - verifyError(e, Errors, 6010, 'Wrong withdrawer authority') + verifyError(e, Errors, 6012, 'Wrong withdrawer authority') } await authorizeStakeAccount({ @@ -638,7 +660,7 @@ describe('Validator Bonds claim withdraw request', () => { await provider.sendIx([splitStakeAccount, validatorIdentity], instruction) throw new Error('failure expected; wrong staker') } catch (e) { - verifyError(e, Errors, 6026, 'already funded to a settlement') + verifyError(e, Errors, 6028, 'already funded to a settlement') } }) @@ -648,7 +670,7 @@ describe('Validator Bonds claim withdraw request', () => { const { stakeAccount } = await delegatedStakeAccount({ provider, lamports: LAMPORTS_PER_SOL * 2, - voteAccountToDelegate: bond.account.validatorVoteAccount, + voteAccountToDelegate: bond.account.voteAccount, lockup: { custodian: Keypair.generate().publicKey, // locked up epoch is bigger than to one we will warp to @@ -657,7 +679,7 @@ describe('Validator Bonds claim withdraw request', () => { }, }) const { withdrawRequest } = await initWithdrawRequest(33 * LAMPORTS_PER_SOL) - await warpToUnlock() + await warpToUnlockClaiming() const { instruction, splitStakeAccount } = await claimWithdrawRequestInstruction({ @@ -673,15 +695,12 @@ describe('Validator Bonds claim withdraw request', () => { await provider.sendIx([splitStakeAccount, validatorIdentity], instruction) throw new Error('failure expected as should be locked') } catch (e) { - verifyError(e, Errors, 6028, 'stake account is locked-up') + verifyError(e, Errors, 6030, 'stake account is locked-up') } }) - async function warpToUnlock() { - // waiting two epochs to unlock; the first one is not enough (withdrawLockupEpochs = 1) - expect(withdrawLockupEpochs).toEqual(1) - await warpToNextEpoch(provider) - await warpToNextEpoch(provider) + async function warpToUnlockClaiming() { + await warpOffsetEpoch(provider, withdrawLockupEpochs + 1) } async function createStakeAccountAndInitWithdraw( @@ -691,7 +710,13 @@ describe('Validator Bonds claim withdraw request', () => { withdrawRequest: PublicKey stakeAccount: PublicKey }> { - const { stakeAccount } = await delegateAndFund(fundStakeLamports) + const { stakeAccount: stakeAccount } = await delegateAndFund({ + program, + provider, + lamports: fundStakeLamports, + voteAccount, + bond: bond.publicKey, + }) const { withdrawRequest } = await initWithdrawRequest(initWithdrawAmount) return { withdrawRequest, stakeAccount } } @@ -710,27 +735,7 @@ describe('Validator Bonds claim withdraw request', () => { program, withdrawRequest ) - expect(withdrawRequestData.validatorVoteAccount).toEqual(voteAccount) + expect(withdrawRequestData.voteAccount).toEqual(voteAccount) return { withdrawRequest } } - - async function delegateAndFund( - amountLamports: number - ): Promise<{ stakeAccount: PublicKey }> { - const { stakeAccount, withdrawer: stakeAccountWithdrawer } = - await delegatedStakeAccount({ - provider, - lamports: amountLamports, - voteAccountToDelegate: voteAccount, - }) - await warpToNextEpoch(provider) // activating stake account - await executeFundBondInstruction({ - program, - provider, - bondAccount: bond.publicKey, - stakeAccount, - stakeAccountAuthority: stakeAccountWithdrawer, - }) - return { stakeAccount } - } }) diff --git a/packages/validator-bonds-sdk/__tests__/bankrun/closeSettlement.spec.ts b/packages/validator-bonds-sdk/__tests__/bankrun/closeSettlement.spec.ts new file mode 100644 index 00000000..cba62047 --- /dev/null +++ b/packages/validator-bonds-sdk/__tests__/bankrun/closeSettlement.spec.ts @@ -0,0 +1,271 @@ +import { + Config, + Errors, + ValidatorBondsProgram, + closeSettlementInstruction, + fundSettlementInstruction, + getConfig, + getSettlement, +} from '../../src' +import { + BankrunExtendedProvider, + assertNotExist, + currentEpoch, + initBankrunTest, + warpOffsetEpoch, + warpToNextEpoch, +} from './bankrun' +import { + createUserAndFund, + executeInitBondInstruction, + executeInitConfigInstruction, + executeInitSettlement, +} from '../utils/testTransactions' +import { ProgramAccount } from '@coral-xyz/anchor' +import { Keypair, LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js' +import { + createBondsFundedStakeAccount, + createVoteAccount, + getRentExemptStake, +} from '../utils/staking' +import { getRentExempt } from '../utils/helpers' +import assert from 'assert' +import { pubkey, signer } from '@marinade.finance/web3js-common' +import { verifyError } from '@marinade.finance/anchor-common' + +describe('Validator Bonds close settlement', () => { + const epochsToClaimSettlement = 1 + let provider: BankrunExtendedProvider + let program: ValidatorBondsProgram + let config: ProgramAccount + let operatorAuthority: Keypair + let validatorIdentity: Keypair + let voteAccount: PublicKey + let settlementAccount: PublicKey + let settlementEpoch: number + let rentCollector: Keypair + + beforeAll(async () => { + ;({ provider, program } = await initBankrunTest()) + }) + + beforeEach(async () => { + const { configAccount, operatorAuthority: operatorAuth } = + await executeInitConfigInstruction({ + program, + provider, + epochsToClaimSettlement, + }) + config = { + publicKey: configAccount, + account: await getConfig(program, configAccount), + } + operatorAuthority = operatorAuth + ;({ voteAccount, validatorIdentity } = await createVoteAccount({ + provider, + })) + const { bondAccount } = await executeInitBondInstruction({ + program, + provider, + config: config.publicKey, + voteAccount, + validatorIdentity, + }) + settlementEpoch = await currentEpoch(provider) + rentCollector = Keypair.generate() + ;({ settlementAccount } = await executeInitSettlement({ + config: config.publicKey, + program, + provider, + voteAccount, + operatorAuthority, + currentEpoch: settlementEpoch, + rentCollector: rentCollector.publicKey, + })) + const settlementData = await getSettlement(program, settlementAccount) + expect(bondAccount).toEqual(settlementData.bond) + }) + + it('close settlement', async () => { + await createUserAndFund(provider, LAMPORTS_PER_SOL, pubkey(rentCollector)) + let rentCollectorInfo = await provider.connection.getAccountInfo( + rentCollector.publicKey + ) + assert(rentCollectorInfo !== null) + expect(rentCollectorInfo.lamports).toEqual(LAMPORTS_PER_SOL) + const rentExemptSettlement = await getRentExempt( + provider, + settlementAccount + ) + + const { instruction } = await closeSettlementInstruction({ + program, + settlementAccount, + rentCollector: rentCollector.publicKey, + }) + + await warpToBeClosable() + await provider.sendIx([], instruction) + assertNotExist(provider, settlementAccount) + + rentCollectorInfo = await provider.connection.getAccountInfo( + rentCollector.publicKey + ) + expect(rentCollectorInfo).not.toBeNull() + assert(rentCollectorInfo !== null) + expect(rentCollectorInfo.lamports).toEqual( + LAMPORTS_PER_SOL + rentExemptSettlement + ) + }) + + it('cannot close settlement when permitted rent collector does not match', async () => { + const rentCollectorTest = await createUserAndFund( + provider, + LAMPORTS_PER_SOL + ) + expect(pubkey(rentCollectorTest)).not.toEqual(rentCollector.publicKey) + const { instruction } = await closeSettlementInstruction({ + program, + settlementAccount, + rentCollector: pubkey(rentCollectorTest), + }) + + await warpToBeClosable() + try { + await provider.sendIx([], instruction) + } catch (e) { + verifyError(e, Errors, 6043, 'does not match permitted rent collector') + } + }) + + it('cannot close settlement when not expired', async () => { + const { instruction } = await closeSettlementInstruction({ + program, + settlementAccount, + }) + try { + await provider.sendIx([], instruction) + throw new Error( + 'failure expected as settlement claim has not expired yet' + ) + } catch (e) { + verifyError(e, Errors, 6022, 'has not expired yet') + } + }) + + it('close funded settlement with split', async () => { + const settlementData = await getSettlement(program, settlementAccount) + const lamportsToFund = + settlementData.maxTotalClaim.toNumber() + 6 * LAMPORTS_PER_SOL + const stakeAccount = await createBondsFundedStakeAccount({ + program, + provider, + config: config.publicKey, + voteAccount, + lamports: lamportsToFund, + }) + await warpToNextEpoch(provider) // activate stake account + + const splitStakeRentPayer = await createUserAndFund( + provider, + LAMPORTS_PER_SOL + ) + const { instruction: fundIx, splitStakeAccount } = + await fundSettlementInstruction({ + program, + settlementAccount, + stakeAccount, + splitStakeRentPayer, + }) + await provider.sendIx( + [ + signer(splitStakeRentPayer), + signer(splitStakeAccount), + operatorAuthority, + ], + fundIx + ) + + const rentExemptStake = await getRentExemptStake(provider) + expect( + (await provider.connection.getAccountInfo(pubkey(splitStakeRentPayer))) + ?.lamports + ).toEqual(LAMPORTS_PER_SOL - rentExemptStake) + expect( + (await provider.connection.getAccountInfo(stakeAccount))?.lamports + ).toEqual( + settlementData.maxTotalClaim.toNumber() + + 2 * rentExemptStake + + config.account.minimumStakeLamports.toNumber() + ) + + const { instruction } = await closeSettlementInstruction({ + program, + settlementAccount, + splitRentRefundAccount: rentCollector.publicKey, + }) + try { + await provider.sendIx([], instruction) + throw new Error('error expected; settlement has not expired yet') + } catch (e) { + verifyError(e, Errors, 6022, 'has not expired yet') + } + + await warpToBeClosable() + try { + await provider.sendIx([], instruction) + throw new Error('error expected; wrong split stake account') + } catch (e) { + verifyError(e, Errors, 6006, 'not owned by the stake account') + } + + const { instruction: ixWrongStake } = await closeSettlementInstruction({ + program, + settlementAccount, + splitRentRefundAccount: pubkey(splitStakeAccount), + }) + try { + await provider.sendIx([], ixWrongStake) + throw new Error('error expected; wrong stake account') + } catch (e) { + verifyError(e, Errors, 6036, 'not funded under the settlement') + } + + const { instruction: ixWrongCollector } = await closeSettlementInstruction({ + program, + settlementAccount, + splitRentRefundAccount: pubkey(stakeAccount), + splitRentCollector: provider.walletPubkey, + }) + try { + await provider.sendIx([], ixWrongCollector) + throw new Error('error expected; wrong rent collector') + } catch (e) { + verifyError(e, Errors, 6043, 'does not match permitted rent collector') + } + + const { instruction: ixOk } = await closeSettlementInstruction({ + program, + settlementAccount, + splitRentRefundAccount: pubkey(stakeAccount), + splitRentCollector: pubkey(splitStakeRentPayer), + }) + await provider.sendIx([], ixOk) + + expect( + (await provider.connection.getAccountInfo(pubkey(splitStakeRentPayer))) + ?.lamports + ).toEqual(LAMPORTS_PER_SOL) + expect( + (await provider.connection.getAccountInfo(stakeAccount))?.lamports + ).toEqual( + settlementData.maxTotalClaim.toNumber() + + rentExemptStake + + config.account.minimumStakeLamports.toNumber() + ) + }) + + async function warpToBeClosable() { + await warpOffsetEpoch(provider, epochsToClaimSettlement + 1) + } +}) diff --git a/packages/validator-bonds-sdk/__tests__/bankrun/closeSettlementClaim.spec.ts b/packages/validator-bonds-sdk/__tests__/bankrun/closeSettlementClaim.spec.ts new file mode 100644 index 00000000..9078cb33 --- /dev/null +++ b/packages/validator-bonds-sdk/__tests__/bankrun/closeSettlementClaim.spec.ts @@ -0,0 +1,175 @@ +import { + Config, + Errors, + ValidatorBondsProgram, + claimSettlementInstruction, + closeSettlementClaimInstruction, + closeSettlementInstruction, + fundSettlementInstruction, + getConfig, +} from '../../src' +import { + BankrunExtendedProvider, + assertNotExist, + currentEpoch, + initBankrunTest, + warpOffsetEpoch, + warpToNextEpoch, +} from './bankrun' +import { + createUserAndFund, + executeInitBondInstruction, + executeInitConfigInstruction, + executeInitSettlement, +} from '../utils/testTransactions' +import { ProgramAccount } from '@coral-xyz/anchor' +import { + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + ComputeBudgetProgram, +} from '@solana/web3.js' +import { + createBondsFundedStakeAccount, + createVoteAccount, +} from '../utils/staking' +import { signer } from '@marinade.finance/web3js-common' +import { + MERKLE_ROOT_BUF, + configAccountKeypair, + totalClaimVoteAccount1, + treeNodeBy, + treeNodesVoteAccount1, + voteAccount1Keypair, + withdrawer1, + withdrawer1Keypair, + withdrawer2Keypair, + withdrawer3Keypair, +} from '../utils/merkleTreeTestData' +import { verifyError } from '@marinade.finance/anchor-common' + +describe('Validator Bonds close settlement claim', () => { + const epochsToClaimSettlement = 3 + let provider: BankrunExtendedProvider + let program: ValidatorBondsProgram + let config: ProgramAccount + let operatorAuthority: Keypair + let validatorIdentity1: Keypair + let voteAccount1: PublicKey + let settlementAccount1: PublicKey + let settlementEpoch: number + let stakeAccount1: PublicKey + + const computeUnitIx = ComputeBudgetProgram.setComputeUnitLimit({ + units: 1_000_000, + }) + + beforeAll(async () => { + ;({ provider, program } = await initBankrunTest()) + await warpToNextEpoch(provider) + const { configAccount, operatorAuthority: operatorAuth } = + await executeInitConfigInstruction({ + program, + provider, + epochsToClaimSettlement, + configAccountKeypair: configAccountKeypair, + }) + operatorAuthority = operatorAuth + config = { + publicKey: configAccount, + account: await getConfig(program, configAccount), + } + ;({ voteAccount: voteAccount1, validatorIdentity: validatorIdentity1 } = + await createVoteAccount({ + voteAccount: voteAccount1Keypair, + provider, + })) + await executeInitBondInstruction({ + program, + provider, + config: config.publicKey, + voteAccount: voteAccount1, + validatorIdentity: validatorIdentity1, + }) + + settlementEpoch = await currentEpoch(provider) + ;({ settlementAccount: settlementAccount1 } = await executeInitSettlement({ + config: config.publicKey, + program, + provider, + voteAccount: voteAccount1, + operatorAuthority, + currentEpoch: settlementEpoch, + merkleRoot: MERKLE_ROOT_BUF, + maxMerkleNodes: treeNodesVoteAccount1.length, + maxTotalClaim: totalClaimVoteAccount1, + })) + stakeAccount1 = await createBondsFundedStakeAccount({ + program, + provider, + config: config.publicKey, + voteAccount: voteAccount1, + lamports: totalClaimVoteAccount1.toNumber() + LAMPORTS_PER_SOL * 5, + }) + + await warpToNextEpoch(provider) // activate stake account + + const { instruction: fundIx1, splitStakeAccount: split1 } = + await fundSettlementInstruction({ + program, + settlementAccount: settlementAccount1, + stakeAccount: stakeAccount1, + }) + await provider.sendIx([signer(split1), operatorAuthority], fundIx1) + if ((await provider.context.banksClient.getAccount(withdrawer1)) === null) { + await createUserAndFund(provider, LAMPORTS_PER_SOL, withdrawer1Keypair) + await createUserAndFund(provider, LAMPORTS_PER_SOL, withdrawer2Keypair) + await createUserAndFund(provider, LAMPORTS_PER_SOL, withdrawer3Keypair) + } + }) + + it('close settlement claim', async () => { + const treeNode1Withdrawer1 = treeNodeBy(voteAccount1, withdrawer1) + await warpToNextEpoch(provider) + const { instruction: claimIx, settlementClaimAccount } = + await claimSettlementInstruction({ + program, + claimAmount: treeNode1Withdrawer1.treeNode.data.claim, + merkleProof: treeNode1Withdrawer1.proof, + withdrawer: withdrawer1, + settlementAccount: settlementAccount1, + stakeAccount: stakeAccount1, + }) + await provider.sendIx([], computeUnitIx, claimIx) + expect( + provider.connection.getAccountInfo(settlementClaimAccount) + ).resolves.not.toBeNull() + + const { instruction: closeIx } = await closeSettlementClaimInstruction({ + program, + settlementAccount: settlementAccount1, + settlementClaimAccount, + }) + + try { + await provider.sendIx([], closeIx) + throw new Error('Failure expected; the settlement has not been closed') + } catch (e) { + verifyError(e, Errors, 6027, 'settlement to be closed') + } + + await warpToNotBeClaimable() // we can close settlement here + const { instruction: closeSettle1 } = await closeSettlementInstruction({ + program, + settlementAccount: settlementAccount1, + splitRentRefundAccount: stakeAccount1, + }) + await provider.sendIx([], closeSettle1, closeIx) + await assertNotExist(provider, settlementAccount1) + await assertNotExist(provider, settlementClaimAccount) + }) + + async function warpToNotBeClaimable() { + await warpOffsetEpoch(provider, epochsToClaimSettlement + 1) + } +}) diff --git a/packages/validator-bonds-sdk/__tests__/bankrun/configureBond.spec.ts b/packages/validator-bonds-sdk/__tests__/bankrun/configureBond.spec.ts index 8bf82399..c27fd940 100644 --- a/packages/validator-bonds-sdk/__tests__/bankrun/configureBond.spec.ts +++ b/packages/validator-bonds-sdk/__tests__/bankrun/configureBond.spec.ts @@ -6,6 +6,7 @@ import { configureBondInstruction, getBond, getConfig, + initBondInstruction, } from '../../src' import { BankrunExtendedProvider, initBankrunTest } from './bankrun' import { @@ -13,7 +14,7 @@ import { executeInitConfigInstruction, } from '../utils/testTransactions' import { ProgramAccount } from '@coral-xyz/anchor' -import { Keypair } from '@solana/web3.js' +import { Keypair, PublicKey } from '@solana/web3.js' import { createVoteAccount } from '../utils/staking' import { verifyError } from '@marinade.finance/anchor-common' @@ -43,17 +44,17 @@ describe('Validator Bonds configure bond account', () => { voteAccount, validatorIdentity: nodePubkey, authorizedVoter, - } = await createVoteAccount(provider) + } = await createVoteAccount({ provider }) bondAuthority = Keypair.generate() - const { bondAccount } = await executeInitBondInstruction( + const { bondAccount } = await executeInitBondInstruction({ program, provider, - config.publicKey, + config: config.publicKey, bondAuthority, voteAccount, - nodePubkey, - 123 - ) + validatorIdentity: nodePubkey, + cpmpe: 123, + }) bond = { publicKey: bondAccount, account: await getBond(program, bondAccount), @@ -69,14 +70,14 @@ describe('Validator Bonds configure bond account', () => { bondAccount: bond.publicKey, authority: bondAuthority, newBondAuthority: newBondAuthority.publicKey, - newRevenueShareHundredthBps: 321, + newCpmpe: 321, }) await provider.sendIx([bondAuthority], ix1) let bondData = await getBond(program, bond.publicKey) expect(bondData.config).toEqual(config.publicKey) expect(bondData.authority).toEqual(newBondAuthority.publicKey) - expect(bondData.revenueShare).toEqual({ hundredthBps: 321 }) + expect(bondData.cpmpe).toEqual(321) const { instruction: ix2 } = await configureBondInstruction({ program, @@ -110,7 +111,7 @@ describe('Validator Bonds configure bond account', () => { const { instruction } = await configureBondInstruction({ program, configAccount: config.publicKey, - validatorVoteAccount: bond.account.validatorVoteAccount, + voteAccount: bond.account.voteAccount, authority: voterAuthority, newBondAuthority: newBondAuthority.publicKey, }) @@ -118,10 +119,12 @@ describe('Validator Bonds configure bond account', () => { await provider.sendIx([voterAuthority], instruction) throw new Error('failure expected as wrong admin') } catch (e) { - verifyError(e, Errors, 6016, 'Wrong authority') + verifyError(e, Errors, 6018, 'Wrong authority') } }) + // TODO: add a test that fails when configuring with a wrong validator identity + it('fails to configure with a random authority', async () => { const newBondAuthority = Keypair.generate() const { instruction } = await configureBondInstruction({ @@ -134,7 +137,49 @@ describe('Validator Bonds configure bond account', () => { await provider.sendIx([newBondAuthority], instruction) throw new Error('failure expected as wrong admin') } catch (e) { - verifyError(e, Errors, 6016, 'Wrong authority') + verifyError(e, Errors, 6018, 'Wrong authority') } }) + + it('configure bond with validator identity', async () => { + const { voteAccount, validatorIdentity } = await createVoteAccount({ + provider, + }) + // permission-less creation + const { + instruction: createBondIx, + bondAccount: permissionLessBondAccount, + } = await initBondInstruction({ + program, + configAccount: config.publicKey, + voteAccount, + }) + await provider.sendIx([], createBondIx) + + const randomAuthority = Keypair.generate() + const { instruction: ixWrongAuth } = await configureBondInstruction({ + program, + bondAccount: permissionLessBondAccount, + authority: randomAuthority.publicKey, + newBondAuthority: PublicKey.default, + }) + try { + await provider.sendIx([randomAuthority], ixWrongAuth) + throw new Error('failure expected; wrong authority validator identity') + } catch (e) { + verifyError(e, Errors, 6018, 'Wrong authority') + } + let bondsData = await getBond(program, permissionLessBondAccount) + expect(bondsData.authority).toEqual(validatorIdentity.publicKey) + + const { instruction } = await configureBondInstruction({ + program, + bondAccount: permissionLessBondAccount, + authority: validatorIdentity.publicKey, + newBondAuthority: PublicKey.default, + }) + await provider.sendIx([validatorIdentity], instruction) + bondsData = await getBond(program, permissionLessBondAccount) + expect(bondsData.authority).toEqual(PublicKey.default) + }) }) diff --git a/packages/validator-bonds-sdk/__tests__/bankrun/fundBond.spec.ts b/packages/validator-bonds-sdk/__tests__/bankrun/fundBond.spec.ts index f74563ef..46997c37 100644 --- a/packages/validator-bonds-sdk/__tests__/bankrun/fundBond.spec.ts +++ b/packages/validator-bonds-sdk/__tests__/bankrun/fundBond.spec.ts @@ -54,17 +54,19 @@ describe('Validator Bonds fund bond account', () => { publicKey: configAccount, account: await getConfig(program, configAccount), } - const { voteAccount, validatorIdentity } = await createVoteAccount(provider) + const { voteAccount, validatorIdentity } = await createVoteAccount({ + provider, + }) bondAuthority = Keypair.generate() - const { bondAccount } = await executeInitBondInstruction( + const { bondAccount } = await executeInitBondInstruction({ program, provider, - config.publicKey, + config: config.publicKey, bondAuthority, voteAccount, validatorIdentity, - 123 - ) + cpmpe: 123, + }) bond = { publicKey: bondAccount, account: await getBond(program, bondAccount), @@ -85,7 +87,7 @@ describe('Validator Bonds fund bond account', () => { await provider.sendIx([signer(withdrawer)], instruction) throw new Error('failure expected as not delegated') } catch (e) { - verifyError(e, Errors, 6017, 'cannot be used for bonds') + verifyError(e, Errors, 6019, 'cannot be used for bonds') } }) @@ -106,7 +108,7 @@ describe('Validator Bonds fund bond account', () => { await provider.sendIx([withdrawer], instruction) throw new Error('failure expected as not activated') } catch (e) { - verifyError(e, Errors, 6023, 'Stake account is not fully activated') + verifyError(e, Errors, 6025, 'Stake account is not fully activated') } await warpToNextEpoch(provider) @@ -114,7 +116,7 @@ describe('Validator Bonds fund bond account', () => { await provider.sendIx([withdrawer], instruction) throw new Error('failure expected as delegated to wrong validator') } catch (e) { - verifyError(e, Errors, 6018, 'delegated to a wrong validator') + verifyError(e, Errors, 6020, 'delegated to a wrong validator') } }) @@ -124,7 +126,7 @@ describe('Validator Bonds fund bond account', () => { const { stakeAccount, withdrawer } = await delegatedStakeAccount({ provider, lamports: LAMPORTS_PER_SOL * 2, - voteAccountToDelegate: bond.account.validatorVoteAccount, + voteAccountToDelegate: bond.account.voteAccount, lockup: { custodian: Keypair.generate().publicKey, epoch: nextEpoch + 1, @@ -145,7 +147,7 @@ describe('Validator Bonds fund bond account', () => { await provider.sendIx([withdrawer], instruction) throw new Error('failure expected as should be locked') } catch (e) { - verifyError(e, Errors, 6028, 'stake account is locked-up') + verifyError(e, Errors, 6030, 'stake account is locked-up') } }) @@ -153,7 +155,7 @@ describe('Validator Bonds fund bond account', () => { const { stakeAccount, withdrawer } = await delegatedStakeAccount({ provider, lamports: LAMPORTS_PER_SOL * 2, - voteAccountToDelegate: bond.account.validatorVoteAccount, + voteAccountToDelegate: bond.account.voteAccount, }) const [bondWithdrawer] = withdrawerAuthority( config.publicKey, diff --git a/packages/validator-bonds-sdk/__tests__/bankrun/fundSettlement.spec.ts b/packages/validator-bonds-sdk/__tests__/bankrun/fundSettlement.spec.ts new file mode 100644 index 00000000..55ede209 --- /dev/null +++ b/packages/validator-bonds-sdk/__tests__/bankrun/fundSettlement.spec.ts @@ -0,0 +1,576 @@ +import { + Config, + Errors, + U64_MAX, + ValidatorBondsProgram, + closeSettlementInstruction, + fundSettlementInstruction, + getConfig, + getSettlement, +} from '../../src' +import { + BankrunExtendedProvider, + assertNotExist, + bankrunExecuteIx, + currentEpoch, + delegateAndFund, + initBankrunTest, + warpOffsetEpoch, + warpToNextEpoch, +} from './bankrun' +import { + createUserAndFund, + executeInitBondInstruction, + executeInitConfigInstruction, + executeInitSettlement, +} from '../utils/testTransactions' +import { ProgramAccount } from '@coral-xyz/anchor' +import { Keypair, LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js' +import { + StakeStates, + createBondsFundedStakeAccount, + createSettlementFundedStakeAccount, + createVoteAccount, + getAndCheckStakeAccount, + getRentExemptStake, +} from '../utils/staking' +import { pubkey, signer } from '@marinade.finance/web3js-common' +import { verifyError } from '@marinade.finance/anchor-common' + +describe('Validator Bonds fund settlement', () => { + const epochsToClaimSettlement = 3 + let provider: BankrunExtendedProvider + let program: ValidatorBondsProgram + let config: ProgramAccount + let operatorAuthority: Keypair + let validatorIdentity: Keypair + let bondAccount: PublicKey + let voteAccount: PublicKey + let settlementEpoch: number + let rentCollector: Keypair + let rentExemptStake: number + let stakeAccountMinimalAmount: number + + beforeAll(async () => { + ;({ provider, program } = await initBankrunTest()) + rentExemptStake = await getRentExemptStake(provider) + }) + + beforeEach(async () => { + const { configAccount, operatorAuthority: operatorAuth } = + await executeInitConfigInstruction({ + program, + provider, + epochsToClaimSettlement, + }) + config = { + publicKey: configAccount, + account: await getConfig(program, configAccount), + } + operatorAuthority = operatorAuth + ;({ voteAccount, validatorIdentity } = await createVoteAccount({ + provider, + })) + ;({ bondAccount } = await executeInitBondInstruction({ + program, + provider, + config: config.publicKey, + voteAccount, + validatorIdentity, + })) + settlementEpoch = await currentEpoch(provider) + rentCollector = Keypair.generate() + stakeAccountMinimalAmount = + rentExemptStake + config.account.minimumStakeLamports.toNumber() + }) + + it('fund settlement fully with precise amount', async () => { + const maxTotalClaim = LAMPORTS_PER_SOL * 10 + const { settlementAccount } = await executeInitSettlement({ + config: config.publicKey, + program, + provider, + voteAccount, + operatorAuthority, + currentEpoch: settlementEpoch, + rentCollector: rentCollector.publicKey, + maxTotalClaim, + }) + + const splitRentPayer = await createUserAndFund(provider, LAMPORTS_PER_SOL) + const lamportsToFund1 = maxTotalClaim / 2 + const lamportsToFund2 = + maxTotalClaim - lamportsToFund1 + 2 * stakeAccountMinimalAmount + const stakeAccount1 = + await createBondsFundedStakeAccountActivated(lamportsToFund1) + const stakeAccountData = + await provider.connection.getAccountInfo(stakeAccount1) + expect(stakeAccountData?.lamports).toEqual(lamportsToFund1) + let settlementData = await getSettlement(program, settlementAccount) + expect(settlementData.lamportsFunded).toEqual(0) + + const { instruction: ix1, splitStakeAccount } = + await fundSettlementInstruction({ + program, + settlementAccount, + stakeAccount: stakeAccount1, + splitStakeRentPayer: splitRentPayer, + }) + await provider.sendIx( + [signer(splitRentPayer), signer(splitStakeAccount), operatorAuthority], + ix1 + ) + + settlementData = await getSettlement(program, settlementAccount) + expect(settlementData.lamportsFunded).toEqual( + lamportsToFund1 - stakeAccountMinimalAmount + ) + expect(settlementData.splitRentAmount).toEqual(0) + expect(settlementData.splitRentCollector).toEqual(null) + expect( + (await provider.connection.getAccountInfo(pubkey(splitRentPayer))) + ?.lamports + ).toEqual(LAMPORTS_PER_SOL) + expect( + (await provider.connection.getAccountInfo(stakeAccount1))?.lamports + ).toEqual(lamportsToFund1) + await assertNotExist(provider, pubkey(splitStakeAccount)) + + const stakeAccount2 = + await createBondsFundedStakeAccountActivated(lamportsToFund2) + const { instruction: ix2 } = await fundSettlementInstruction({ + program, + settlementAccount, + stakeAccount: stakeAccount2, + splitStakeRentPayer: splitRentPayer, + splitStakeAccount, + }) + await provider.sendIx( + [signer(splitRentPayer), signer(splitStakeAccount), operatorAuthority], + ix2 + ) + + settlementData = await getSettlement(program, settlementAccount) + expect(settlementData.lamportsFunded).toEqual(maxTotalClaim) + expect(settlementData.splitRentAmount).toEqual(0) + expect(settlementData.splitRentCollector).toEqual(null) + expect( + (await provider.connection.getAccountInfo(pubkey(splitRentPayer))) + ?.lamports + ).toEqual(LAMPORTS_PER_SOL) + expect( + (await provider.connection.getAccountInfo(stakeAccount2))?.lamports + ).toEqual(lamportsToFund2) + await assertNotExist(provider, pubkey(splitStakeAccount)) + + const stakeAccount3 = await createBondsFundedStakeAccountActivated( + LAMPORTS_PER_SOL * 2 + ) + const { instruction: ix3 } = await fundSettlementInstruction({ + program, + settlementAccount, + stakeAccount: stakeAccount3, + splitStakeRentPayer: splitRentPayer, + splitStakeAccount, + }) + const txLog = await bankrunExecuteIx( + provider, + [ + provider.wallet, + signer(splitRentPayer), + signer(splitStakeAccount), + operatorAuthority, + ], + ix3 + ) + + expect( + txLog.logMessages.find(v => v.includes('already fully funded')) + ).toBeDefined() + settlementData = await getSettlement(program, settlementAccount) + expect(settlementData.lamportsFunded).toEqual(maxTotalClaim) + expect(settlementData.splitRentAmount).toEqual(0) + expect(settlementData.splitRentCollector).toEqual(null) + await assertNotExist(provider, pubkey(splitStakeAccount)) + }) + + it('fund fully without split as not split-able', async () => { + const maxTotalClaim = LAMPORTS_PER_SOL * 2 + const { settlementAccount } = await executeInitSettlement({ + config: config.publicKey, + program, + provider, + voteAccount, + operatorAuthority, + currentEpoch: settlementEpoch, + rentCollector: rentCollector.publicKey, + maxTotalClaim, + }) + + const splitStakeRentPayer = await createUserAndFund( + provider, + LAMPORTS_PER_SOL + ) + const lamportsToFund = maxTotalClaim + LAMPORTS_PER_SOL + const stakeAccount = + await createBondsFundedStakeAccountActivated(lamportsToFund) + + const { instruction, splitStakeAccount } = await fundSettlementInstruction({ + program, + settlementAccount, + stakeAccount, + splitStakeRentPayer, + }) + await provider.sendIx( + [ + signer(splitStakeRentPayer), + signer(splitStakeAccount), + operatorAuthority, + ], + instruction + ) + + const settlementData = await getSettlement(program, settlementAccount) + expect(settlementData.lamportsFunded).toEqual( + lamportsToFund - stakeAccountMinimalAmount + ) + expect(settlementData.splitRentAmount).toEqual(0) + expect(settlementData.splitRentCollector).toEqual(null) + expect( + (await provider.connection.getAccountInfo(stakeAccount))?.lamports + ).toEqual(lamportsToFund) + + expect( + (await provider.connection.getAccountInfo(pubkey(splitStakeRentPayer))) + ?.lamports + ).toEqual(LAMPORTS_PER_SOL) + await assertNotExist(provider, pubkey(splitStakeAccount)) + }) + + it('fund settlement with split', async () => { + const maxTotalClaim = LAMPORTS_PER_SOL * 2 + const { settlementAccount } = await executeInitSettlement({ + config: config.publicKey, + program, + provider, + voteAccount, + operatorAuthority, + currentEpoch: settlementEpoch, + rentCollector: rentCollector.publicKey, + maxTotalClaim, + }) + + const splitStakeRentPayer = await createUserAndFund( + provider, + LAMPORTS_PER_SOL + ) + const lamportsToFund = maxTotalClaim + 3 * LAMPORTS_PER_SOL + const stakeAccount = + await createBondsFundedStakeAccountActivated(lamportsToFund) + + let [stakeAccountData] = await getAndCheckStakeAccount( + provider, + stakeAccount, + StakeStates.Delegated + ) + + const executionEpoch = await currentEpoch(provider) + expect(stakeAccountData.Stake?.stake.delegation.deactivationEpoch).toEqual( + U64_MAX + ) + expect(stakeAccountData.Stake?.stake.delegation.activationEpoch).toEqual( + executionEpoch - 1 + ) + + const { instruction, splitStakeAccount } = await fundSettlementInstruction({ + program, + settlementAccount, + stakeAccount, + splitStakeRentPayer, + }) + await provider.sendIx( + [ + signer(splitStakeRentPayer), + signer(splitStakeAccount), + operatorAuthority, + ], + instruction + ) + + const settlementData = await getSettlement(program, settlementAccount) + expect(settlementData.lamportsFunded).toEqual(maxTotalClaim) + expect(settlementData.splitRentAmount).toEqual(rentExemptStake) + expect(settlementData.splitRentCollector).toEqual( + pubkey(splitStakeRentPayer) + ) + expect( + (await provider.connection.getAccountInfo(pubkey(splitStakeRentPayer))) + ?.lamports + ).toEqual(LAMPORTS_PER_SOL - rentExemptStake) + const splitStakeAccountInfo = await provider.connection.getAccountInfo( + pubkey(splitStakeAccount) + ) + expect(splitStakeAccountInfo?.lamports).toEqual( + lamportsToFund - maxTotalClaim - stakeAccountMinimalAmount + ) + // stake account consist of what to be claimed + amount needed for existence a stake account + rent exempt to refund split payer + expect( + (await provider.connection.getAccountInfo(stakeAccount))?.lamports + ).toEqual(maxTotalClaim + stakeAccountMinimalAmount + rentExemptStake) + + // stake account expected to be deactivated in next epoch + await warpToNextEpoch(provider) + const epochNow = await currentEpoch(provider) + ;[stakeAccountData] = await getAndCheckStakeAccount( + provider, + stakeAccount, + StakeStates.Delegated + ) + expect(stakeAccountData.Stake?.stake.delegation.deactivationEpoch).toEqual( + epochNow - 1 + ) + expect(stakeAccountData.Stake?.stake.delegation.activationEpoch).toEqual( + executionEpoch - 1 + ) + }) + + it('fund settlement with bond funded account', async () => { + const maxTotalClaim = LAMPORTS_PER_SOL * 2 + const { stakeAccount: bondsFundedStakeAccount } = await delegateAndFund({ + program, + provider, + voteAccount, + lamports: maxTotalClaim, + bond: bondAccount, + }) + const { settlementAccount } = await executeInitSettlement({ + config: config.publicKey, + program, + provider, + voteAccount, + operatorAuthority, + currentEpoch: await currentEpoch(provider), + rentCollector: rentCollector.publicKey, + maxTotalClaim, + }) + + const { instruction, splitStakeAccount } = await fundSettlementInstruction({ + program, + settlementAccount, + stakeAccount: bondsFundedStakeAccount, + }) + await provider.sendIx( + [signer(splitStakeAccount), operatorAuthority], + instruction + ) + + const settlementData = await getSettlement(program, settlementAccount) + expect(settlementData.lamportsFunded).toEqual( + maxTotalClaim - stakeAccountMinimalAmount + ) + expect(settlementData.splitRentAmount).toEqual(0) + expect(settlementData.splitRentCollector).toEqual(null) + expect( + (await provider.connection.getAccountInfo(bondsFundedStakeAccount)) + ?.lamports + ).toEqual(maxTotalClaim) + await assertNotExist(provider, pubkey(splitStakeAccount)) + }) + + it('cannot fund closed settlement', async () => { + const { settlementAccount } = await executeInitSettlement({ + config: config.publicKey, + program, + provider, + voteAccount, + operatorAuthority, + currentEpoch: await currentEpoch(provider), + rentCollector: rentCollector.publicKey, + }) + const { instruction: closeIx } = await closeSettlementInstruction({ + program, + settlementAccount, + rentCollector: rentCollector.publicKey, + }) + await warpOffsetEpoch(provider, epochsToClaimSettlement + 1) + await provider.sendIx([], closeIx) + + const { stakeAccount } = await delegateAndFund({ + program, + provider, + voteAccount, + lamports: 3 * LAMPORTS_PER_SOL, + bond: bondAccount, + }) + const { instruction: fundIx, splitStakeAccount } = + await fundSettlementInstruction({ + program, + settlementAccount, + stakeAccount, + configAccount: config.publicKey, + bondAccount, + operatorAuthority, + voteAccount, + }) + try { + await provider.sendIx( + [signer(splitStakeAccount), operatorAuthority], + fundIx + ) + throw new Error('cannot fund closed settlement') + } catch (e) { + // 3012. Error Message: The program expected this account to be already initialized. + expect( + (e as Error).message.includes('custom program error: 0xbc4') + ).toBeTruthy() + } + }) + + it('cannot fund settlement with wrong authority', async () => { + const wrongOperator = Keypair.generate() + const { settlementAccount } = await executeInitSettlement({ + config: config.publicKey, + program, + provider, + voteAccount, + operatorAuthority, + currentEpoch: await currentEpoch(provider), + rentCollector: rentCollector.publicKey, + }) + const { stakeAccount } = await delegateAndFund({ + program, + provider, + voteAccount, + lamports: 3 * LAMPORTS_PER_SOL, + bond: bondAccount, + }) + + const { instruction, splitStakeAccount } = await fundSettlementInstruction({ + program, + settlementAccount, + stakeAccount, + operatorAuthority: wrongOperator, + }) + try { + await provider.sendIx( + [wrongOperator, signer(splitStakeAccount)], + instruction + ) + throw new Error('cannot fund as wrong authority') + } catch (e) { + verifyError(e, Errors, 6003, 'operator authority signature') + } + assertNotExist(provider, pubkey(splitStakeAccount)) + expect( + (await getSettlement(program, settlementAccount)).lamportsFunded + ).toEqual(0) + }) + + it('cannot fund already funded', async () => { + const maxTotalClaim = 3 * LAMPORTS_PER_SOL + const { settlementAccount } = await executeInitSettlement({ + config: config.publicKey, + program, + provider, + voteAccount, + operatorAuthority, + currentEpoch: await currentEpoch(provider), + rentCollector: rentCollector.publicKey, + maxTotalClaim, + }) + const { stakeAccount } = await delegateAndFund({ + program, + provider, + voteAccount, + lamports: 2 * LAMPORTS_PER_SOL, + bond: bondAccount, + }) + + const { instruction, splitStakeAccount } = await fundSettlementInstruction({ + program, + settlementAccount, + stakeAccount, + operatorAuthority, + }) + await provider.sendIx( + [operatorAuthority, signer(splitStakeAccount)], + instruction + ) + const beingFunded = 2 * LAMPORTS_PER_SOL - stakeAccountMinimalAmount + expect( + (await getSettlement(program, settlementAccount)).lamportsFunded + ).toEqual(beingFunded) + assertNotExist(provider, pubkey(splitStakeAccount)) + + await warpToNextEpoch(provider) + try { + await provider.sendIx( + [operatorAuthority, signer(splitStakeAccount)], + instruction + ) + throw new Error('cannot fund as already funded') + } catch (e) { + verifyError(e, Errors, 6028, 'has been already funded') + } + assertNotExist(provider, pubkey(splitStakeAccount)) + expect( + (await getSettlement(program, settlementAccount)).lamportsFunded + ).toEqual(beingFunded) + + const manuallyCreated = await createSettlementFundedStakeAccountActivated( + maxTotalClaim * 20, + settlementAccount + ) + const { instruction: ixManual, splitStakeAccount: splitManual } = + await fundSettlementInstruction({ + program, + settlementAccount, + stakeAccount: manuallyCreated, + operatorAuthority, + bondAccount, + configAccount: config.publicKey, + splitStakeAccount: splitStakeAccount, + }) + + try { + await provider.sendIx([operatorAuthority, signer(splitManual)], ixManual) + throw new Error('cannot fund as already funded') + } catch (e) { + verifyError(e, Errors, 6028, 'has been already funded') + } + assertNotExist(provider, pubkey(splitManual)) + expect( + (await getSettlement(program, settlementAccount)).lamportsFunded + ).toEqual(beingFunded) + }) + + async function createBondsFundedStakeAccountActivated( + lamports: number + ): Promise { + const sa = await createBondsFundedStakeAccount({ + program, + provider, + voteAccount, + lamports, + config: config.publicKey, + }) + await warpToNextEpoch(provider) + return sa + } + + async function createSettlementFundedStakeAccountActivated( + lamports: number, + settlementAccount: PublicKey + ): Promise { + const sa = await createSettlementFundedStakeAccount({ + program, + provider, + voteAccount, + lamports, + config: config.publicKey, + settlement: settlementAccount, + }) + await warpToNextEpoch(provider) + return sa + } +}) diff --git a/packages/validator-bonds-sdk/__tests__/bankrun/initBond.spec.ts b/packages/validator-bonds-sdk/__tests__/bankrun/initBond.spec.ts index e4fb8a02..27e7ff01 100644 --- a/packages/validator-bonds-sdk/__tests__/bankrun/initBond.spec.ts +++ b/packages/validator-bonds-sdk/__tests__/bankrun/initBond.spec.ts @@ -46,14 +46,16 @@ describe('Validator Bonds init bond account', () => { it('init bond', async () => { const bondAuthority = Keypair.generate() - const { voteAccount, validatorIdentity } = await createVoteAccount(provider) + const { voteAccount, validatorIdentity } = await createVoteAccount({ + provider, + }) const rentWallet = await createUserAndFund(provider, LAMPORTS_PER_SOL) const { instruction, bondAccount } = await initBondInstruction({ program, configAccount: config.publicKey, bondAuthority: bondAuthority.publicKey, - revenueShareHundredthBps: 30, - validatorVoteAccount: voteAccount, + cpmpe: 30, + voteAccount: voteAccount, validatorIdentity: validatorIdentity.publicKey, rentPayer: pubkey(rentWallet), }) @@ -62,9 +64,8 @@ describe('Validator Bonds init bond account', () => { const rentWalletInfo = await provider.connection.getAccountInfo( pubkey(rentWallet) ) - const bondAccountInfo = await provider.connection.getAccountInfo( - bondAccount - ) + const bondAccountInfo = + await provider.connection.getAccountInfo(bondAccount) if (bondAccountInfo === null) { throw new Error(`Bond account ${bondAccountInfo} not found`) } @@ -73,9 +74,9 @@ describe('Validator Bonds init bond account', () => { bondAccountInfo.data.length ) expect(rentWalletInfo!.lamports).toEqual(LAMPORTS_PER_SOL - rentExempt) - console.log( - `Bond record data length ${bondAccountInfo.data.length}, exempt rent: ${rentExempt}` - ) + // NO overflow of account size from the first deployment on mainnet + expect(bondAccountInfo?.data.byteLength).toBeLessThanOrEqual(260) + console.log('bond account length', bondAccountInfo?.data.byteLength) const bondData = await getBond(program, bondAccount) expect(bondData.authority).toEqual(bondAuthority.publicKey) @@ -83,19 +84,21 @@ describe('Validator Bonds init bond account', () => { bondAddress(config.publicKey, voteAccount, program.programId)[1] ) expect(bondData.config).toEqual(config.publicKey) - expect(bondData.revenueShare).toEqual({ hundredthBps: 30 }) - expect(bondData.validatorVoteAccount).toEqual(voteAccount) + expect(bondData.cpmpe).toEqual(30) + expect(bondData.voteAccount).toEqual(voteAccount) }) it('init bond permission-less', async () => { const bondAuthority = Keypair.generate() - const { voteAccount, validatorIdentity } = await createVoteAccount(provider) + const { voteAccount, validatorIdentity } = await createVoteAccount({ + provider, + }) const { instruction, bondAccount } = await initBondInstruction({ program, configAccount: config.publicKey, bondAuthority: bondAuthority.publicKey, - revenueShareHundredthBps: 88, - validatorVoteAccount: voteAccount, + cpmpe: 88, + voteAccount, }) await provider.sendIx([], instruction) @@ -105,23 +108,23 @@ describe('Validator Bonds init bond account', () => { bondAddress(config.publicKey, voteAccount, program.programId)[1] ) expect(bondData.config).toEqual(config.publicKey) - expect(bondData.revenueShare).toEqual({ hundredthBps: 0 }) - expect(bondData.validatorVoteAccount).toEqual(voteAccount) + expect(bondData.cpmpe).toEqual(0) + expect(bondData.voteAccount).toEqual(voteAccount) }) it('init bond failure on vote account withdrawer signature', async () => { const bondAuthority = Keypair.generate() - const { voteAccount, authorizedWithdrawer } = await createVoteAccount( - provider - ) + const { voteAccount, authorizedWithdrawer } = await createVoteAccount({ + provider, + }) try { const { instruction } = await initBondInstruction({ program, configAccount: config.publicKey, bondAuthority: bondAuthority.publicKey, - revenueShareHundredthBps: 30, - validatorVoteAccount: voteAccount, + cpmpe: 30, + voteAccount: voteAccount, validatorIdentity: authorizedWithdrawer.publicKey, }) await provider.sendIx([authorizedWithdrawer], instruction) @@ -130,7 +133,7 @@ describe('Validator Bonds init bond account', () => { verifyError( e, Errors, - 6035, + 6037, 'does not match to provided validator identity' ) } @@ -138,7 +141,11 @@ describe('Validator Bonds init bond account', () => { it('cannot init bond when already exists', async () => { const { bondAccount, voteAccount, validatorIdentity } = - await executeInitBondInstruction(program, provider, config.publicKey) + await executeInitBondInstruction({ + program, + provider, + config: config.publicKey, + }) expect( provider.connection.getAccountInfo(bondAccount) ).resolves.not.toBeNull() @@ -148,8 +155,8 @@ describe('Validator Bonds init bond account', () => { program, configAccount: config.publicKey, bondAuthority: PublicKey.default, - revenueShareHundredthBps: 30, - validatorVoteAccount: voteAccount, + cpmpe: 30, + voteAccount: voteAccount, validatorIdentity: validatorIdentity.publicKey, }) await provider.sendIx([validatorIdentity], instruction) diff --git a/packages/validator-bonds-sdk/__tests__/bankrun/initConfig.spec.ts b/packages/validator-bonds-sdk/__tests__/bankrun/initConfig.spec.ts index fc6585cb..091665f9 100644 --- a/packages/validator-bonds-sdk/__tests__/bankrun/initConfig.spec.ts +++ b/packages/validator-bonds-sdk/__tests__/bankrun/initConfig.spec.ts @@ -30,12 +30,12 @@ describe('Validator Bonds config account tests', () => { expect(configData.epochsToClaimSettlement).toEqual(1) expect(configData.withdrawLockupEpochs).toEqual(2) - const configAccountInfo = await provider.connection.getAccountInfo( - configAccount - ) - // not account size change from the first deployment on mainnet + const configAccountInfo = + await provider.connection.getAccountInfo(configAccount) + // NO change of account size from the first deployment on mainnet // account size is 609 bytes and aligned to 8 bytes alignment expect(configAccountInfo?.data.byteLength).toEqual(616) + console.log('config account length', configAccountInfo?.data.byteLength) }) it('cannot init config when already exists', async () => { diff --git a/packages/validator-bonds-sdk/__tests__/bankrun/initSettlement.spec.ts b/packages/validator-bonds-sdk/__tests__/bankrun/initSettlement.spec.ts new file mode 100644 index 00000000..fb215825 --- /dev/null +++ b/packages/validator-bonds-sdk/__tests__/bankrun/initSettlement.spec.ts @@ -0,0 +1,194 @@ +import { + Bond, + Config, + Errors, + ValidatorBondsProgram, + getBond, + getConfig, + getSettlement, + initSettlementInstruction, + settlementAddress, + settlementAuthority, +} from '../../src' +import { + BankrunExtendedProvider, + assertNotExist, + currentEpoch, + initBankrunTest, +} from './bankrun' +import { + executeInitBondInstruction, + executeInitConfigInstruction, +} from '../utils/testTransactions' +import { ProgramAccount } from '@coral-xyz/anchor' +import { Keypair, PublicKey } from '@solana/web3.js' +import { createVoteAccount } from '../utils/staking' +import { verifyError } from '@marinade.finance/anchor-common' + +describe('Validator Bonds init settlement', () => { + let provider: BankrunExtendedProvider + let program: ValidatorBondsProgram + let config: ProgramAccount + let bond: ProgramAccount + let operatorAuthority: Keypair + let validatorIdentity: Keypair + let voteAccount: PublicKey + + beforeAll(async () => { + ;({ provider, program } = await initBankrunTest()) + }) + + beforeEach(async () => { + const { configAccount, operatorAuthority: operatorAuth } = + await executeInitConfigInstruction({ + program, + provider, + }) + config = { + publicKey: configAccount, + account: await getConfig(program, configAccount), + } + operatorAuthority = operatorAuth + ;({ voteAccount, validatorIdentity } = await createVoteAccount({ + provider, + })) + const { bondAccount } = await executeInitBondInstruction({ + program, + provider, + config: config.publicKey, + voteAccount, + validatorIdentity, + }) + bond = { + publicKey: bondAccount, + account: await getBond(program, bondAccount), + } + }) + + it('init settlement', async () => { + const merkleRoot = Buffer.from( + Array.from({ length: 32 }, () => Math.floor(Math.random() * 256)) + ) + const epochNow = await currentEpoch(provider) + const rentCollector = Keypair.generate().publicKey + const { instruction, settlementAccount, epoch } = + await initSettlementInstruction({ + program, + bondAccount: bond.publicKey, + operatorAuthority, + merkleRoot, + maxMerkleNodes: 1, + maxTotalClaim: 3, + voteAccount, + currentEpoch: epochNow, + configAccount: config.publicKey, + rentCollector, + }) + await provider.sendIx([operatorAuthority], instruction) + + expect(epoch.toString()).toEqual(epochNow.toString()) + + const [settlementAddr, bump] = settlementAddress( + bond.publicKey, + merkleRoot, + epoch, + program.programId + ) + expect(settlementAddr).toEqual(settlementAccount) + const [authorityAddr, authorityBump] = settlementAuthority( + settlementAccount, + program.programId + ) + + const settlementData = await getSettlement(program, settlementAccount) + expect(settlementData.bond).toEqual(bond.publicKey) + expect(settlementData.bumps.pda).toEqual(bump) + expect(settlementData.bumps.authority).toEqual(authorityBump) + expect(settlementData.authority).toEqual(authorityAddr) + expect(settlementData.epochCreatedAt.gte(epoch)).toBeTruthy() + expect(settlementData.maxMerkleNodes).toEqual(1) + expect(settlementData.maxTotalClaim).toEqual(3) + expect(settlementData.merkleRoot).toEqual(Array.from(merkleRoot)) + expect(settlementData.merkleNodesClaimed).toEqual(0) + expect(settlementData.lamportsFunded).toEqual(0) + expect(settlementData.lamportsClaimed).toEqual(0) + expect(settlementData.rentCollector).toEqual(rentCollector) + expect(settlementData.splitRentAmount).toEqual(0) + expect(settlementData.splitRentCollector).toEqual(null) + }) + + it('cannot init settlement with wrong buffer size', async () => { + const merkleRoot = Buffer.from( + Array.from({ length: 30 }, () => Math.floor(Math.random() * 256)) + ) + const { instruction, settlementAccount } = await initSettlementInstruction({ + program, + bondAccount: bond.publicKey, + merkleRoot, + maxMerkleNodes: 1, + maxTotalClaim: 3, + voteAccount, + configAccount: config.publicKey, + currentEpoch: await currentEpoch(provider), + }) + try { + await provider.sendIx([operatorAuthority], instruction) + throw new Error('failure; expected wrong seeds constraint') + } catch (e) { + // Error Code: ConstraintSeeds. Error Number: 2006. Error Message: A seeds constraint was violated. + if (!(e as Error).message.includes('custom program error: 0x7d6')) { + throw e + } + } + assertNotExist(provider, settlementAccount) + }) + + it('cannot init settlement with wrong epoch', async () => { + const merkleRoot = Buffer.alloc(32) + const futureEpoch = (await currentEpoch(provider)) + 2024 + const { instruction, settlementAccount } = await initSettlementInstruction({ + program, + bondAccount: bond.publicKey, + operatorAuthority, + merkleRoot, + maxMerkleNodes: 1, + maxTotalClaim: 3, + voteAccount, + configAccount: config.publicKey, + currentEpoch: futureEpoch, + }) + try { + await provider.sendIx([operatorAuthority], instruction) + throw new Error('failure; expected wrong constraint') + } catch (e) { + // Error Code: ConstraintSeeds. Error Number: 2006. Error Message: A seeds constraint was violated. + if (!(e as Error).message.includes('custom program error: 0x7d6')) { + throw e + } + } + assertNotExist(provider, settlementAccount) + }) + + it('cannot init settlement with wrong authority', async () => { + const merkleRoot = Buffer.alloc(32) + const wrongOperator = Keypair.generate() + const { instruction, settlementAccount } = await initSettlementInstruction({ + program, + bondAccount: bond.publicKey, + operatorAuthority: wrongOperator, + merkleRoot, + maxMerkleNodes: 1, + maxTotalClaim: 3, + voteAccount, + currentEpoch: await currentEpoch(provider), + configAccount: config.publicKey, + }) + try { + await provider.sendIx([wrongOperator], instruction) + throw new Error('failure; expected wrong operator authority') + } catch (e) { + verifyError(e, Errors, 6003, 'operator authority signature') + } + assertNotExist(provider, settlementAccount) + }) +}) diff --git a/packages/validator-bonds-sdk/__tests__/bankrun/initWithdrawRequest.spec.ts b/packages/validator-bonds-sdk/__tests__/bankrun/initWithdrawRequest.spec.ts index 0375834f..28680918 100644 --- a/packages/validator-bonds-sdk/__tests__/bankrun/initWithdrawRequest.spec.ts +++ b/packages/validator-bonds-sdk/__tests__/bankrun/initWithdrawRequest.spec.ts @@ -34,10 +34,15 @@ describe('Validator Bonds init withdraw request', () => { let bond: ProgramAccount let bondAuthority: Keypair let validatorIdentity: Keypair - const startUpEpoch = Math.floor(Math.random() * 100) + 100 + let startUpEpoch: number beforeAll(async () => { ;({ provider, program } = await initBankrunTest()) + const startUpEpochPlus = Math.floor(Math.random() * 100) + 100 + const currentEpoch = Number( + (await provider.context.banksClient.getClock()).epoch + ) + startUpEpoch = currentEpoch + startUpEpochPlus warpToEpoch(provider, startUpEpoch) }) @@ -51,18 +56,17 @@ describe('Validator Bonds init withdraw request', () => { account: await getConfig(program, configAccount), } const { voteAccount, validatorIdentity: nodeIdentity } = - await createVoteAccount(provider) + await createVoteAccount({ provider }) validatorIdentity = nodeIdentity bondAuthority = Keypair.generate() - const { bondAccount } = await executeInitBondInstruction( + const { bondAccount } = await executeInitBondInstruction({ program, provider, - config.publicKey, + config: config.publicKey, bondAuthority, voteAccount, validatorIdentity, - 123 - ) + }) bond = { publicKey: bondAccount, account: await getBond(program, bondAccount), @@ -110,9 +114,7 @@ describe('Validator Bonds init withdraw request', () => { expect(withdrawRequestData.bump).toEqual(bump) expect(withdrawRequestData.epoch).toEqual(epoch) expect(withdrawRequestData.requestedAmount).toEqual(LAMPORTS_PER_SOL) - expect(withdrawRequestData.validatorVoteAccount).toEqual( - bond.account.validatorVoteAccount - ) + expect(withdrawRequestData.voteAccount).toEqual(bond.account.voteAccount) expect(withdrawRequestData.withdrawnAmount).toEqual(0) }) @@ -132,9 +134,8 @@ describe('Validator Bonds init withdraw request', () => { const rentWalletInfo = await provider.connection.getAccountInfo( pubkey(rentWallet) ) - const withdrawRequestInfo = await provider.connection.getAccountInfo( - withdrawRequest - ) + const withdrawRequestInfo = + await provider.connection.getAccountInfo(withdrawRequest) if (withdrawRequestInfo === null) { throw new Error(`Withdraw request account ${withdrawRequest} not found`) } @@ -150,9 +151,7 @@ describe('Validator Bonds init withdraw request', () => { ) expect(withdrawRequestData.bond).toEqual(bond.publicKey) expect(withdrawRequestData.requestedAmount).toEqual(123) - expect(withdrawRequestData.validatorVoteAccount).toEqual( - bond.account.validatorVoteAccount - ) + expect(withdrawRequestData.voteAccount).toEqual(bond.account.voteAccount) expect(withdrawRequestData.withdrawnAmount).toEqual(0) }) diff --git a/packages/validator-bonds-sdk/__tests__/bankrun/merge.spec.ts b/packages/validator-bonds-sdk/__tests__/bankrun/merge.spec.ts index 6331951d..9f6961f5 100644 --- a/packages/validator-bonds-sdk/__tests__/bankrun/merge.spec.ts +++ b/packages/validator-bonds-sdk/__tests__/bankrun/merge.spec.ts @@ -88,7 +88,7 @@ describe('Validator Bonds fund bond account', () => { 'failure expected as accounts are not owned by bonds program' ) } catch (e) { - verifyError(e, Errors, 6043, 'does not belong to bonds program') + verifyError(e, Errors, 6045, 'does not belong to bonds program') } }) @@ -123,7 +123,7 @@ describe('Validator Bonds fund bond account', () => { 'failure expected as accounts are not owned by bonds program' ) } catch (e) { - verifyError(e, Errors, 6043, 'does not belong to bonds program') + verifyError(e, Errors, 6045, 'does not belong to bonds program') } }) @@ -161,7 +161,7 @@ describe('Validator Bonds fund bond account', () => { verifyError( e, Errors, - 6045, + 6047, 'Delegation of provided stake account mismatches' ) } @@ -213,7 +213,7 @@ describe('Validator Bonds fund bond account', () => { verifyError( e, Errors, - 6045, + 6047, 'Delegation of provided stake account mismatches' ) } @@ -230,7 +230,7 @@ describe('Validator Bonds fund bond account', () => { verifyError( e, Errors, - 6045, + 6047, 'Delegation of provided stake account mismatches' ) } @@ -333,7 +333,7 @@ describe('Validator Bonds fund bond account', () => { verifyError( e, Errors, - 6045, + 6047, 'Delegation of provided stake account mismatches' ) } @@ -350,22 +350,24 @@ describe('Validator Bonds fund bond account', () => { verifyError( e, Errors, - 6045, + 6047, 'Delegation of provided stake account mismatches' ) } }) it('cannot merge settlement and bond authority', async () => { - const voteAccount = (await createVoteAccount(provider)).voteAccount + const voteAccount = (await createVoteAccount({ provider })).voteAccount const [bondWithdrawer] = withdrawerAuthority( config.publicKey, program.programId ) + const currentEpoch = (await provider.context.banksClient.getClock()).epoch const [bond] = bondAddress(config.publicKey, program.programId) const [settlement] = settlementAddress( bond, Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), + currentEpoch, program.programId ) const [settlementStaker] = settlementAuthority( @@ -412,7 +414,7 @@ describe('Validator Bonds fund bond account', () => { await provider.sendIx([], instruction) throw new Error('failure expected; wrong authorities') } catch (e) { - verifyError(e, Errors, 6042, 'staker does not match') + verifyError(e, Errors, 6044, 'staker does not match') } const { instruction: instruction2 } = await mergeInstruction({ program, @@ -424,7 +426,7 @@ describe('Validator Bonds fund bond account', () => { await provider.sendIx([], instruction2) throw new Error('failure expected; wrong authorities') } catch (e) { - verifyError(e, Errors, 6042, 'staker does not match') + verifyError(e, Errors, 6044, 'staker does not match') } }) diff --git a/packages/validator-bonds-sdk/__tests__/bankrun/merkleTree.spec.ts b/packages/validator-bonds-sdk/__tests__/bankrun/merkleTree.spec.ts new file mode 100644 index 00000000..b898715c --- /dev/null +++ b/packages/validator-bonds-sdk/__tests__/bankrun/merkleTree.spec.ts @@ -0,0 +1,56 @@ +import BN from 'bn.js' +import { MerkleTreeNode } from '../../src/merkleTree' + +describe('Testing tree node creation', () => { + it('a hash from string', () => { + // expected generated from the rust code: + // TreeNode { + // stakeAuthority: "Solana".to_string(), + // withdrawAuthority: "blockchain".to_string(), + // voteAccount: "stars".to_string(), + // claim: 1, + // }.hash() + const expectedBase58 = 'BXWkZ8cyzndF9HJwMUYTCxDGv93kMaB8sGZQbLyL8NG' + expect(expectedBase58).toEqual( + MerkleTreeNode.hash({ + stakeAuthority: 'Solana', + withdrawAuthority: 'blockchain', + voteAccount: 'stars', + claim: new BN(1), + }).base58 + ) + + expect('6hcVpnceUAJbAi4ScnMpQ7WPZXXiWkeX3PnZ97DRYYKC').toEqual( + MerkleTreeNode.leafNodeHash({ + stakeAuthority: 'AshNazgDurbatuLk999999999999999999999999999', + withdrawAuthority: 'AshNazgG1mbatuL9999999999999999999999999999', + voteAccount: 'AshNazgThrakatuLuk99999999999999999999999999', + claim: 69, + }).base58 + ) + expect('Dz3iv3tvPSTjSsdZy4ft32GqYW6m3Tk63HnvMCmmAuBe').toEqual( + MerkleTreeNode.hash({ + stakeAuthority: 'AshNazgDurbatuLk999999999999999999999999999', + withdrawAuthority: 'AshNazgG1mbatuL9999999999999999999999999999', + voteAccount: 'AshNazgThrakatuLuk99999999999999999999999999', + claim: 69, + }).base58 + ) + }) + + it('a tree node from pubkey', () => { + const expectedBase58 = '3LrYLzt4P6LJCyLsbYPAes4d5U8aohjbmW1dJvbrkdse' + const hash = MerkleTreeNode.hash({ + stakeAuthority: 'EjeWgRiaawLSCUM7uojZgSnwipEiypS986yorgvfAzYW', + withdrawAuthority: 'BT6Y2kX5RLhQ6DDzbjbiHNDyyWJgn9jp7g5rCFn8stqy', + voteAccount: 'DYSosfmS9gp1hTY4jAdKJFWK3XHsemecgVPwjqgwM2Pb', + claim: new BN(444), + }) + expect(expectedBase58).toEqual(hash.base58) + + const expectedBase58TreeNode = + '37uc7x9LVzJqsPB9un28SJEPbSop8NGHXHQjZCe6GKAX' + const treeNode = MerkleTreeNode.treeNodeFromHash(hash) + expect(treeNode.base58).toEqual(expectedBase58TreeNode) + }) +}) diff --git a/packages/validator-bonds-sdk/__tests__/bankrun/reset.spec.ts b/packages/validator-bonds-sdk/__tests__/bankrun/reset.spec.ts new file mode 100644 index 00000000..31eaa2a0 --- /dev/null +++ b/packages/validator-bonds-sdk/__tests__/bankrun/reset.spec.ts @@ -0,0 +1,167 @@ +import { + Config, + Errors, + U64_MAX, + ValidatorBondsProgram, + getConfig, + resetInstruction, + withdrawerAuthority, +} from '../../src' +import { + BankrunExtendedProvider, + currentEpoch, + initBankrunTest, +} from './bankrun' +import { + executeInitBondInstruction, + executeInitConfigInstruction, + executeInitSettlement, +} from '../utils/testTransactions' +import { ProgramAccount } from '@coral-xyz/anchor' +import { Keypair, LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js' +import { + StakeStates, + createBondsFundedStakeAccount, + createSettlementFundedStakeAccount, + createVoteAccount, + getAndCheckStakeAccount, +} from '../utils/staking' +import { verifyError } from '@marinade.finance/anchor-common' + +describe('Validator Bonds reset', () => { + let provider: BankrunExtendedProvider + let program: ValidatorBondsProgram + let config: ProgramAccount + let operatorAuthority: Keypair + let validatorIdentity: Keypair + let voteAccount: PublicKey + + beforeAll(async () => { + ;({ provider, program } = await initBankrunTest()) + }) + + beforeEach(async () => { + const { configAccount, operatorAuthority: operatorAuth } = + await executeInitConfigInstruction({ + program, + provider, + }) + config = { + publicKey: configAccount, + account: await getConfig(program, configAccount), + } + operatorAuthority = operatorAuth + ;({ voteAccount, validatorIdentity } = await createVoteAccount({ + provider, + })) + await executeInitBondInstruction({ + program, + provider, + config: config.publicKey, + voteAccount, + validatorIdentity, + }) + }) + + it('reset settlement stake account', async () => { + const fakeSettlement = Keypair.generate().publicKey + const stakeAccount = await createSettlementFundedStakeAccount({ + program, + provider, + config: config.publicKey, + settlement: fakeSettlement, + voteAccount, + lamports: LAMPORTS_PER_SOL * 5, + }) + + const { instruction } = await resetInstruction({ + program, + configAccount: config.publicKey, + stakeAccount, + voteAccount, + settlementAccount: fakeSettlement, + }) + await provider.sendIx([], instruction) + + const epochNow = await currentEpoch(provider) + const [bondsAuth] = withdrawerAuthority(config.publicKey, program.programId) + const [stakeAccountData] = await getAndCheckStakeAccount( + provider, + stakeAccount, + StakeStates.Delegated + ) + expect(stakeAccountData.Stake?.stake.delegation.voterPubkey).toEqual( + voteAccount + ) + expect(stakeAccountData.Stake?.stake.delegation.activationEpoch).toEqual( + epochNow + ) + expect(stakeAccountData.Stake?.stake.delegation.deactivationEpoch).toEqual( + U64_MAX + ) + expect(stakeAccountData.Stake?.meta.authorized.staker).toEqual(bondsAuth) + expect(stakeAccountData.Stake?.meta.authorized.withdrawer).toEqual( + bondsAuth + ) + }) + + it('cannot reset stake account not funded to a settlement', async () => { + const fakeSettlement = Keypair.generate().publicKey + const stakeAccount = await createBondsFundedStakeAccount({ + program, + provider, + config: config.publicKey, + voteAccount, + lamports: LAMPORTS_PER_SOL * 5, + }) + + const { instruction } = await resetInstruction({ + program, + configAccount: config.publicKey, + stakeAccount, + voteAccount, + settlementAccount: fakeSettlement, + }) + try { + await provider.sendIx([], instruction) + throw new Error( + 'Expected error as stake account is not funded to a settlement' + ) + } catch (e) { + verifyError(e, Errors, 6046, 'Stake account staker authority mismatches') + } + }) + + it('cannot reset with existing settlement', async () => { + const { settlementAccount } = await executeInitSettlement({ + config: config.publicKey, + program, + provider, + voteAccount, + operatorAuthority, + currentEpoch: await currentEpoch(provider), + }) + const stakeAccount = await createSettlementFundedStakeAccount({ + program, + provider, + config: config.publicKey, + settlement: settlementAccount, + voteAccount, + lamports: LAMPORTS_PER_SOL * 5, + }) + + const { instruction } = await resetInstruction({ + program, + configAccount: config.publicKey, + stakeAccount, + voteAccount, + settlementAccount, + }) + try { + await provider.sendIx([], instruction) + throw new Error('Expected error; settlement account exists') + } catch (e) { + verifyError(e, Errors, 6027, 'settlement to be closed') + } + }) +}) diff --git a/packages/validator-bonds-sdk/__tests__/bankrun/solanaStake.spec.ts b/packages/validator-bonds-sdk/__tests__/bankrun/solanaStake.spec.ts index 06424158..1579c62f 100644 --- a/packages/validator-bonds-sdk/__tests__/bankrun/solanaStake.spec.ts +++ b/packages/validator-bonds-sdk/__tests__/bankrun/solanaStake.spec.ts @@ -303,8 +303,6 @@ describe('Solana stake account behavior verification', () => { authorizedPubkey: staker.publicKey, newAuthorizedPubkey: newStaker.publicKey, stakeAuthorizationType: StakeAuthorizationLayout.Staker, - // using random non-existent custodian here - custodianPubkey: Keypair.generate().publicKey, }) await bankrunExecuteIx( provider, @@ -451,7 +449,10 @@ describe('Solana stake account behavior verification', () => { await bankrunExecuteIx(provider, [provider.wallet], transferIx) // creating vote account to delegate to it - const { voteAccount } = await createVoteAccount(provider, rentExemptVote) + const { voteAccount } = await createVoteAccount({ + provider, + rentExempt: rentExemptVote, + }) const delegateIx = StakeProgram.delegate({ stakePubkey: stakeAccount1, authorizedPubkey: newStaker.publicKey, @@ -791,8 +792,9 @@ describe('Solana stake account behavior verification', () => { ) console.log('3. MERGING deactivated with different delegation') - const otherVoteAccount = (await createVoteAccount(provider, rentExemptVote)) - .voteAccount + const otherVoteAccount = ( + await createVoteAccount({ provider, rentExempt: rentExemptVote }) + ).voteAccount const delegateIx = StakeProgram.delegate({ stakePubkey: stakeAccountLocked, authorizedPubkey: staker.publicKey, diff --git a/packages/validator-bonds-sdk/__tests__/test-validator/claimSettlementAndClose.spec.ts b/packages/validator-bonds-sdk/__tests__/test-validator/claimSettlementAndClose.spec.ts new file mode 100644 index 00000000..23b20106 --- /dev/null +++ b/packages/validator-bonds-sdk/__tests__/test-validator/claimSettlementAndClose.spec.ts @@ -0,0 +1,312 @@ +import { Keypair, LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js' +import { + ValidatorBondsProgram, + ClaimSettlementEvent, + CLAIM_SETTLEMENT_EVENT, + claimSettlementInstruction, + fundSettlementInstruction, + withdrawerAuthority, + settlementClaimAddress, + findSettlementClaims, + CloseSettlementClaimEvent, + CLOSE_SETTLEMENT_CLAIM_EVENT, + closeSettlementInstruction, + closeSettlementClaimInstruction, +} from '../../src' +import { getValidatorInfo, initTest, waitForNextEpoch } from './testValidator' +import { + computeUnitIx, + createUserAndFund, + executeInitBondInstruction, + executeInitConfigInstruction, + executeInitSettlement, +} from '../utils/testTransactions' +import { ExtendedProvider } from '../utils/provider' +import { signer } from '@marinade.finance/web3js-common' +import { + MERKLE_ROOT_BUF, + configAccountKeypair, + totalClaimVoteAccount1, + treeNodeBy, + treeNodesVoteAccount1, + voteAccount1, + voteAccount1Keypair, + withdrawer1, + withdrawer1Keypair, + withdrawer2, + withdrawer2Keypair, + withdrawer3, + withdrawer3Keypair, +} from '../utils/merkleTreeTestData' +import { + createBondsFundedStakeAccount, + createVoteAccount, +} from '../utils/staking' + +// NOTE: order of tests need to be maintained +describe('Validator Bonds claim settlement', () => { + let provider: ExtendedProvider + let program: ValidatorBondsProgram + let configAccount: PublicKey + let operatorAuthority: Keypair + let validatorIdentity: Keypair + let voteAccount: PublicKey + let settlementAccount: PublicKey + let stakeAccount: PublicKey + let fundSettlementRentPayer: Keypair + + beforeAll(async () => { + ;({ provider, program } = await initTest()) + ;({ validatorIdentity } = await getValidatorInfo(provider.connection)) + ;({ configAccount, operatorAuthority } = await executeInitConfigInstruction( + { + program, + provider, + configAccountKeypair: configAccountKeypair, + epochsToClaimSettlement: 1, + } + )) + await createVoteAccount({ + voteAccount: voteAccount1Keypair, + provider, + validatorIdentity, + }) + ;({ voteAccount } = await executeInitBondInstruction({ + config: configAccount, + program, + provider, + voteAccount: voteAccount1, + validatorIdentity, + })) + ;({ settlementAccount } = await executeInitSettlement({ + config: configAccount, + program, + provider, + voteAccount: voteAccount1, + operatorAuthority, + merkleRoot: MERKLE_ROOT_BUF, + maxMerkleNodes: treeNodesVoteAccount1.length, + maxTotalClaim: totalClaimVoteAccount1, + })) + stakeAccount = await createBondsFundedStakeAccount({ + program, + provider, + config: configAccount, + voteAccount: voteAccount1, + lamports: totalClaimVoteAccount1.toNumber() + LAMPORTS_PER_SOL * 555, + }) + const { instruction: fundIx, splitStakeAccount } = + await fundSettlementInstruction({ + program, + settlementAccount, + stakeAccount, + }) + await provider.sendIx( + [signer(splitStakeAccount), operatorAuthority], + fundIx + ) + // will be used for rent payer of claim settlement + fundSettlementRentPayer = (await createUserAndFund( + provider, + LAMPORTS_PER_SOL + )) as Keypair + }) + + afterAll(async () => { + // workaround: "Jest has detected the following 1 open handle", see `initConfig.spec.ts` + await new Promise(resolve => setTimeout(resolve, 500)) + }) + + it('claim settlement', async () => { + const event = new Promise(resolve => { + const listener = program.addEventListener( + CLAIM_SETTLEMENT_EVENT, + async event => { + await program.removeEventListener(listener) + resolve(event) + } + ) + }) + + await createUserAndFund(provider, LAMPORTS_PER_SOL, withdrawer1Keypair) + const treeNodeVoteAccount1Withdrawer1 = treeNodeBy( + voteAccount1, + withdrawer1 + ) + + const { instruction, settlementClaimAccount } = + await claimSettlementInstruction({ + program, + claimAmount: treeNodeVoteAccount1Withdrawer1.treeNode.data.claim, + merkleProof: treeNodeVoteAccount1Withdrawer1.proof, + withdrawer: withdrawer1, + settlementAccount, + stakeAccount, + rentPayer: fundSettlementRentPayer.publicKey, + }) + await provider.sendIx([fundSettlementRentPayer], computeUnitIx, instruction) + + const [bondsWithdrawerAuthority] = withdrawerAuthority( + configAccount, + program.programId + ) + const [settlementClaimAddr, bumpSettlementClaim] = settlementClaimAddress( + { + settlement: settlementAccount, + stakeAuthority: bondsWithdrawerAuthority, + voteAccount, + withdrawAuthority: withdrawer1, + claim: treeNodeVoteAccount1Withdrawer1.treeNode.data.claim, + }, + program.programId + ) + expect(settlementClaimAccount).toEqual(settlementClaimAddr) + + await event.then(e => { + expect(e.settlement).toEqual(settlementAccount) + expect(e.amount).toEqual( + treeNodeVoteAccount1Withdrawer1.treeNode.data.claim + ) + expect(e.bump).toEqual(bumpSettlementClaim) + expect(e.rentCollector).toEqual(fundSettlementRentPayer.publicKey) + expect(e.settlement).toEqual(settlementAccount) + expect(e.settlementClaim).toEqual(settlementClaimAccount) + expect(e.settlementLamportsClaimed).toEqual( + treeNodeVoteAccount1Withdrawer1.treeNode.data.claim + ) + expect(e.settlementMerkleNodesClaimed).toEqual(1) + expect(e.voteAccount).toEqual(voteAccount) + expect(e.withdrawAuthority).toEqual(withdrawer1) + }) + }) + + it('find claim settlements', async () => { + await createUserAndFund(provider, LAMPORTS_PER_SOL, withdrawer2Keypair) + const treeNodeWithdrawer2 = treeNodeBy(voteAccount1, withdrawer2) + const { instruction: ix1 } = await claimSettlementInstruction({ + program, + claimAmount: treeNodeWithdrawer2.treeNode.data.claim, + merkleProof: treeNodeWithdrawer2.proof, + withdrawer: withdrawer2, + settlementAccount, + stakeAccount, + }) + await createUserAndFund(provider, LAMPORTS_PER_SOL, withdrawer3Keypair) + const treeNodeWithdrawer3 = treeNodeBy(voteAccount1, withdrawer3) + const { instruction: ix2 } = await claimSettlementInstruction({ + program, + claimAmount: treeNodeWithdrawer3.treeNode.data.claim, + merkleProof: treeNodeWithdrawer3.proof, + withdrawer: withdrawer3, + settlementAccount, + stakeAccount, + }) + + await provider.sendIx([], computeUnitIx, ix1, ix2) + + let findSettlementList = await findSettlementClaims({ + program, + settlement: settlementAccount, + }) + expect(findSettlementList.length).toBeGreaterThanOrEqual(2) + + const [bondsWithdrawerAuthority] = withdrawerAuthority( + configAccount, + program.programId + ) + findSettlementList = await findSettlementClaims({ + program, + stakeAuthority: bondsWithdrawerAuthority, + }) + expect(findSettlementList.length).toBeGreaterThanOrEqual(2) + findSettlementList = await findSettlementClaims({ + program, + voteAccount, + }) + expect(findSettlementList.length).toBeGreaterThanOrEqual(2) + findSettlementList = await findSettlementClaims({ + program, + withdrawAuthority: withdrawer1, + }) + expect(findSettlementList.length).toEqual(1) + findSettlementList = await findSettlementClaims({ + program, + settlement: settlementAccount, + stakeAuthority: bondsWithdrawerAuthority, + voteAccount, + }) + expect(findSettlementList.length).toBeGreaterThanOrEqual(2) + }) + + it('close settlement claim', async () => { + const event = new Promise(resolve => { + const listener = program.addEventListener( + CLOSE_SETTLEMENT_CLAIM_EVENT, + async event => { + await program.removeEventListener(listener) + resolve(event) + } + ) + }) + + expect( + ( + await provider.connection.getAccountInfo( + fundSettlementRentPayer.publicKey + ) + )?.lamports + ).toBeLessThan(LAMPORTS_PER_SOL) + + const treeNodeVoteAccount1Withdrawer1 = treeNodeBy( + voteAccount1, + withdrawer1 + ) + const [bondsWithdrawerAuthority] = withdrawerAuthority( + configAccount, + program.programId + ) + const [settlementClaimAccount] = settlementClaimAddress( + { + settlement: settlementAccount, + stakeAuthority: bondsWithdrawerAuthority, + voteAccount, + withdrawAuthority: withdrawer1, + claim: treeNodeVoteAccount1Withdrawer1.treeNode.data.claim, + }, + program.programId + ) + const { instruction: closeSettle1 } = await closeSettlementInstruction({ + program, + settlementAccount, + splitRentRefundAccount: stakeAccount, + }) + const { instruction: closeIx } = await closeSettlementClaimInstruction({ + program, + settlementAccount: settlementAccount, + settlementClaimAccount, + rentCollector: fundSettlementRentPayer.publicKey, + }) + await waitForNextEpoch(provider.connection, 15) + await waitForNextEpoch(provider.connection, 15) + await provider.sendIx([], closeSettle1, closeIx) + + expect( + provider.connection.getAccountInfo(settlementClaimAccount) + ).resolves.toBeNull() + expect( + provider.connection.getAccountInfo(settlementAccount) + ).resolves.toBeNull() + expect( + ( + await provider.connection.getAccountInfo( + fundSettlementRentPayer.publicKey + ) + )?.lamports + ).toEqual(LAMPORTS_PER_SOL) + + await event.then(e => { + expect(e.settlement).toEqual(settlementAccount) + expect(e.rentCollector).toEqual(fundSettlementRentPayer.publicKey) + }) + }) +}) diff --git a/packages/validator-bonds-sdk/__tests__/test-validator/claimWithdrawRequest.spec.ts b/packages/validator-bonds-sdk/__tests__/test-validator/claimWithdrawRequest.spec.ts index f2119da6..6da7ad54 100644 --- a/packages/validator-bonds-sdk/__tests__/test-validator/claimWithdrawRequest.spec.ts +++ b/packages/validator-bonds-sdk/__tests__/test-validator/claimWithdrawRequest.spec.ts @@ -54,7 +54,7 @@ describe('Validator Bonds claim withdraw request', () => { ) }) - const { withdrawRequest, bondAccount, voteAccount } = + const { withdrawRequest, bondAccount, voteAccount, bondAuthority } = await executeNewWithdrawRequest({ program, provider, @@ -99,12 +99,13 @@ describe('Validator Bonds claim withdraw request', () => { const { instruction, splitStakeAccount } = await claimWithdrawRequestInstruction({ program, + authority: bondAuthority, withdrawRequestAccount: withdrawRequest, bondAccount, stakeAccount, }) - await provider.sendIx([splitStakeAccount], instruction) + await provider.sendIx([splitStakeAccount, bondAuthority], instruction) stakeAccountData = await getStakeAccount(provider, stakeAccount) const voteAccountData = await getVoteAccount(provider, voteAccount) @@ -133,7 +134,7 @@ describe('Validator Bonds claim withdraw request', () => { ) expect(e.splitStake?.amount).toEqual(splitStakeLamports) expect(e.splitStake?.address).toEqual(splitStakeAccount.publicKey) - expect(e.validatorVoteAccount).toEqual(voteAccount) + expect(e.voteAccount).toEqual(voteAccount) expect(e.withdrawRequest).toEqual(withdrawRequest) expect(e.withdrawingAmount).toEqual(requestedAmount) expect(e.withdrawnAmount).toEqual({ diff --git a/packages/validator-bonds-sdk/__tests__/test-validator/closeSettlement.spec.ts b/packages/validator-bonds-sdk/__tests__/test-validator/closeSettlement.spec.ts new file mode 100644 index 00000000..f8d53f68 --- /dev/null +++ b/packages/validator-bonds-sdk/__tests__/test-validator/closeSettlement.spec.ts @@ -0,0 +1,104 @@ +import { Keypair, PublicKey } from '@solana/web3.js' +import { + ValidatorBondsProgram, + CLOSE_SETTLEMENT_EVENT, + closeSettlementInstruction, + CloseSettlementEvent, +} from '../../src' +import { getValidatorInfo, initTest, waitForNextEpoch } from './testValidator' +import { + executeInitBondInstruction, + executeInitConfigInstruction, + executeInitSettlement, +} from '../utils/testTransactions' +import { ExtendedProvider } from '../utils/provider' +import { transaction } from '@marinade.finance/web3js-common' + +describe('Validator Bonds close settlement', () => { + let provider: ExtendedProvider + let program: ValidatorBondsProgram + let configAccount: PublicKey + let operatorAuthority: Keypair + let validatorIdentity: Keypair + let voteAccount: PublicKey + let bondAccount: PublicKey + + beforeAll(async () => { + ;({ provider, program } = await initTest()) + ;({ validatorIdentity } = await getValidatorInfo(provider.connection)) + }) + + afterAll(async () => { + // workaround: "Jest has detected the following 1 open handle", see `initConfig.spec.ts` + await new Promise(resolve => setTimeout(resolve, 500)) + }) + + beforeEach(async () => { + ;({ configAccount, operatorAuthority } = await executeInitConfigInstruction( + { + program, + provider, + epochsToClaimSettlement: 0, + } + )) + ;({ voteAccount, bondAccount } = await executeInitBondInstruction({ + config: configAccount, + program, + provider, + validatorIdentity, + })) + }) + + it('close settlement', async () => { + const rentCollector = Keypair.generate() + const { settlementAccount, epoch, maxMerkleNodes, maxTotalClaim } = + await executeInitSettlement({ + config: configAccount, + program, + provider, + voteAccount, + operatorAuthority, + rentCollector: rentCollector.publicKey, + }) + + const event = new Promise(resolve => { + const listener = program.addEventListener( + CLOSE_SETTLEMENT_EVENT, + async event => { + await program.removeEventListener(listener) + resolve(event) + } + ) + }) + + const splitRentRefundAccount = Keypair.generate().publicKey + const tx = await transaction(provider) + const { instruction } = await closeSettlementInstruction({ + program, + settlementAccount, + rentCollector: rentCollector.publicKey, + splitRentRefundAccount, + }) + tx.add(instruction) + await waitForNextEpoch(provider.connection, 15) + await provider.sendIx([], instruction) + expect( + provider.connection.getAccountInfo(settlementAccount) + ).resolves.toBeNull() + + await event.then(e => { + expect(e.settlement).toEqual(settlementAccount) + expect(e.bond).toEqual(bondAccount) + expect(e.currentEpoch).toEqual(epoch.addn(1)) + expect(e.expirationEpoch).toEqual(epoch) + expect(e.lamportsClaimed).toEqual(0) + expect(e.lamportsFunded).toEqual(0) + expect(e.merkleNodesClaimed).toEqual(0) + expect(e.maxMerkleNodes).toEqual(maxMerkleNodes) + expect(e.maxTotalClaim).toEqual(maxTotalClaim) + expect(e.splitRentCollector).toEqual(null) + expect(e.splitRentRefundAccount).toEqual(splitRentRefundAccount) + expect(e.rentCollector).toEqual(rentCollector.publicKey) + }) + }) +}) diff --git a/packages/validator-bonds-sdk/__tests__/test-validator/configureBond.spec.ts b/packages/validator-bonds-sdk/__tests__/test-validator/configureBond.spec.ts index 8072d8df..3496fe48 100644 --- a/packages/validator-bonds-sdk/__tests__/test-validator/configureBond.spec.ts +++ b/packages/validator-bonds-sdk/__tests__/test-validator/configureBond.spec.ts @@ -37,15 +37,13 @@ describe('Validator Bonds configure bond', () => { }) it('configure bond', async () => { - const { bondAccount, bondAuthority } = await executeInitBondInstruction( + const { bondAccount, bondAuthority } = await executeInitBondInstruction({ program, provider, - configAccount, - undefined, - undefined, + config: configAccount, validatorIdentity, - 22 - ) + cpmpe: 22, + }) const event = new Promise(resolve => { const listener = program.addEventListener( @@ -63,14 +61,14 @@ describe('Validator Bonds configure bond', () => { bondAccount, authority: bondAuthority, newBondAuthority: newBondAuthority.publicKey, - newRevenueShareHundredthBps: 31, + newCpmpe: 31, }) await provider.sendIx([bondAuthority], instruction) const bondData = await getBond(program, bondAccount) expect(bondData.authority).toEqual(newBondAuthority.publicKey) expect(bondData.config).toEqual(configAccount) - expect(bondData.revenueShare).toEqual({ hundredthBps: 31 }) + expect(bondData.cpmpe).toEqual(31) expect(bondData.authority).toEqual(newBondAuthority.publicKey) await event.then(e => { @@ -78,9 +76,9 @@ describe('Validator Bonds configure bond', () => { old: bondAuthority.publicKey, new: newBondAuthority.publicKey, }) - expect(e.revenueShare).toEqual({ - old: { hundredthBps: 22 }, - new: { hundredthBps: 31 }, + expect(e.cpmpe).toEqual({ + old: 22, + new: 31, }) }) }) diff --git a/packages/validator-bonds-sdk/__tests__/test-validator/fundBond.spec.ts b/packages/validator-bonds-sdk/__tests__/test-validator/fundBond.spec.ts index 0d6d6706..8284c9dc 100644 --- a/packages/validator-bonds-sdk/__tests__/test-validator/fundBond.spec.ts +++ b/packages/validator-bonds-sdk/__tests__/test-validator/fundBond.spec.ts @@ -20,7 +20,7 @@ describe('Validator Bonds fund bond', () => { let program: ValidatorBondsProgram let configAccount: PublicKey let bondAccount: PublicKey - let validatorVoteAccount: PublicKey + let voteAccount: PublicKey beforeAll(async () => { ;({ provider, program } = await initTest()) @@ -36,16 +36,16 @@ describe('Validator Bonds fund bond', () => { program, provider, })) - const { voteAccount, validatorIdentity } = await createVoteAccount(provider) - validatorVoteAccount = voteAccount - ;({ bondAccount } = await executeInitBondInstruction( + const { voteAccount: validatorVoteAccount, validatorIdentity } = + await createVoteAccount({ provider }) + voteAccount = validatorVoteAccount + ;({ bondAccount } = await executeInitBondInstruction({ program, provider, - configAccount, - undefined, - validatorVoteAccount, - validatorIdentity - )) + config: configAccount, + voteAccount, + validatorIdentity, + })) }) it('fund bond', async () => { @@ -62,7 +62,7 @@ describe('Validator Bonds fund bond', () => { const { stakeAccount, withdrawer } = await delegatedStakeAccount({ provider, lamports: LAMPORTS_PER_SOL * 2, - voteAccountToDelegate: validatorVoteAccount, + voteAccountToDelegate: voteAccount, }) console.debug( `Waiting for activation of stake account: ${stakeAccount.toBase58()}` @@ -95,7 +95,7 @@ describe('Validator Bonds fund bond', () => { expect(e.depositedAmount).toEqual(2 * LAMPORTS_PER_SOL) expect(e.stakeAccount).toEqual(stakeAccount) expect(e.stakeAuthoritySigner).toEqual(withdrawer.publicKey) - expect(e.validatorVote).toEqual(validatorVoteAccount) + expect(e.voteAccount).toEqual(voteAccount) }) }) }) diff --git a/packages/validator-bonds-sdk/__tests__/test-validator/fundSettlement.spec.ts b/packages/validator-bonds-sdk/__tests__/test-validator/fundSettlement.spec.ts new file mode 100644 index 00000000..aeefad97 --- /dev/null +++ b/packages/validator-bonds-sdk/__tests__/test-validator/fundSettlement.spec.ts @@ -0,0 +1,130 @@ +import { Keypair, LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js' +import { + ValidatorBondsProgram, + FundSettlementEvent, + FUND_SETTLEMENT_EVENT, + fundSettlementInstruction, + withdrawerAuthority, + getConfig, +} from '../../src' +import { getValidatorInfo, initTest } from './testValidator' +import { + createUserAndFund, + executeInitBondInstruction, + executeInitConfigInstruction, + executeInitSettlement, +} from '../utils/testTransactions' +import { ExtendedProvider } from '../utils/provider' +import { + authorizeStakeAccount, + delegatedStakeAccount, + getRentExemptStake, +} from '../utils/staking' +import { pubkey, signer } from '@marinade.finance/web3js-common' + +describe('Validator Bonds fund settlement', () => { + let provider: ExtendedProvider + let program: ValidatorBondsProgram + let configAccount: PublicKey + let operatorAuthority: Keypair + let validatorIdentity: Keypair + let voteAccount: PublicKey + let bondAccount: PublicKey + + beforeAll(async () => { + ;({ provider, program } = await initTest()) + ;({ validatorIdentity } = await getValidatorInfo(provider.connection)) + }) + + afterAll(async () => { + // workaround: "Jest has detected the following 1 open handle", see `initConfig.spec.ts` + await new Promise(resolve => setTimeout(resolve, 500)) + }) + + beforeEach(async () => { + ;({ configAccount, operatorAuthority } = await executeInitConfigInstruction( + { + program, + provider, + epochsToClaimSettlement: 0, + } + )) + ;({ voteAccount, bondAccount } = await executeInitBondInstruction({ + config: configAccount, + program, + provider, + validatorIdentity, + })) + }) + + it('fund settlement', async () => { + const rentCollector = Keypair.generate() + const { settlementAccount } = await executeInitSettlement({ + config: configAccount, + program, + provider, + voteAccount, + operatorAuthority, + rentCollector: rentCollector.publicKey, + maxTotalClaim: 2 * LAMPORTS_PER_SOL, + }) + const fundedAmount = 10 * LAMPORTS_PER_SOL + const { stakeAccount, withdrawer: initWithdrawer } = + await delegatedStakeAccount({ + provider, + lamports: fundedAmount, + voteAccountToDelegate: voteAccount, + }) + // not needed to activate for this test case + const [bondsAuth] = withdrawerAuthority(configAccount, program.programId) + await authorizeStakeAccount({ + provider, + authority: initWithdrawer, + stakeAccount: stakeAccount, + withdrawer: bondsAuth, + staker: bondsAuth, + }) + + const event = new Promise(resolve => { + const listener = program.addEventListener( + FUND_SETTLEMENT_EVENT, + async event => { + await program.removeEventListener(listener) + resolve(event) + } + ) + }) + + const splitRentPayer = await createUserAndFund(provider, LAMPORTS_PER_SOL) + const { instruction, splitStakeAccount } = await fundSettlementInstruction({ + program, + settlementAccount, + stakeAccount, + splitStakeRentPayer: splitRentPayer, + }) + await provider.sendIx( + [operatorAuthority, signer(splitStakeAccount), signer(splitRentPayer)], + instruction + ) + + const rentExemptStake = await getRentExemptStake(provider) + const minimalStakeAccountSize = + rentExemptStake + + (await getConfig(program, configAccount)).minimumStakeLamports.toNumber() + + await event.then(e => { + expect(e.settlement).toEqual(settlementAccount) + expect(e.bond).toEqual(bondAccount) + expect(e.fundingAmount).toEqual(2 * LAMPORTS_PER_SOL) + expect(e.lamportsClaimed).toEqual(0) + expect(e.merkleNodesClaimed).toEqual(0) + expect(e.splitRentAmount).toEqual(rentExemptStake) + expect(e.splitRentCollector).toEqual(pubkey(splitRentPayer)) + expect(e.splitStakeAccount?.address).toEqual(pubkey(splitStakeAccount)) + expect(e.splitStakeAccount?.amount).toEqual( + fundedAmount - 2 * LAMPORTS_PER_SOL - minimalStakeAccountSize + ) + expect(e.voteAccount).toEqual(voteAccount) + }) + }) +}) diff --git a/packages/validator-bonds-sdk/__tests__/test-validator/initBond.spec.ts b/packages/validator-bonds-sdk/__tests__/test-validator/initBond.spec.ts index b005b8f7..83c4e54c 100644 --- a/packages/validator-bonds-sdk/__tests__/test-validator/initBond.spec.ts +++ b/packages/validator-bonds-sdk/__tests__/test-validator/initBond.spec.ts @@ -54,15 +54,17 @@ describe('Validator Bonds init bond', () => { ) }) - const { voteAccount: validatorVoteAccount } = - await createVoteAccountWithIdentity(provider, validatorIdentity) + const { voteAccount } = await createVoteAccountWithIdentity( + provider, + validatorIdentity + ) const bondAuthority = PublicKey.unique() const { instruction, bondAccount } = await initBondInstruction({ program, configAccount, bondAuthority, - revenueShareHundredthBps: 22, - validatorVoteAccount, + cpmpe: 22, + voteAccount, validatorIdentity: validatorIdentity.publicKey, }) await provider.sendIx([validatorIdentity], instruction) @@ -70,7 +72,7 @@ describe('Validator Bonds init bond', () => { const bondsDataFromList = await findBonds({ program, config: configAccount, - validatorVoteAccount, + voteAccount, bondAuthority, }) expect(bondsDataFromList.length).toEqual(1) @@ -79,23 +81,23 @@ describe('Validator Bonds init bond', () => { const [bondCalculatedAddress, bondBump] = bondAddress( configAccount, - validatorVoteAccount, + voteAccount, program.programId ) expect(bondCalculatedAddress).toEqual(bondAccount) expect(bondData.authority).toEqual(bondAuthority) expect(bondData.bump).toEqual(bondBump) expect(bondData.config).toEqual(configAccount) - expect(bondData.revenueShare).toEqual({ hundredthBps: 22 }) - expect(bondData.validatorVoteAccount).toEqual(validatorVoteAccount) + expect(bondData.cpmpe).toEqual(22) + expect(bondData.voteAccount).toEqual(voteAccount) // Ensure the event listener was called await event.then(e => { expect(e.authority).toEqual(bondAuthority) expect(e.bondBump).toEqual(bondBump) expect(e.configAddress).toEqual(configAccount) - expect(e.revenueShare).toEqual({ hundredthBps: 22 }) - expect(e.validatorVoteAccount).toEqual(validatorVoteAccount) + expect(e.cpmpe).toEqual(22) + expect(e.voteAccount).toEqual(voteAccount) expect(e.validatorIdentity).toEqual(validatorIdentity.publicKey) }) }) @@ -112,20 +114,22 @@ describe('Validator Bonds init bond', () => { const voteAccounts: [PublicKey, Keypair][] = [] for (let i = 1; i <= numberOfBonds; i++) { - const { voteAccount: validatorVoteAccount } = - await createVoteAccountWithIdentity(provider, validatorIdentity) - voteAccounts.push([validatorVoteAccount, validatorIdentity]) + const { voteAccount: voteAccount } = await createVoteAccountWithIdentity( + provider, + validatorIdentity + ) + voteAccounts.push([voteAccount, validatorIdentity]) signers.push(signer(validatorIdentity)) } for (let i = 1; i <= numberOfBonds; i++) { - const [validatorVoteAccount, nodeIdentity] = voteAccounts[i - 1] + const [voteAccount, nodeIdentity] = voteAccounts[i - 1] const { instruction } = await initBondInstruction({ program, configAccount, bondAuthority: bondAuthority, - revenueShareHundredthBps: 100, - validatorVoteAccount, + cpmpe: 100, + voteAccount, validatorIdentity: nodeIdentity, }) tx.add(instruction) @@ -145,10 +149,10 @@ describe('Validator Bonds init bond', () => { expect(bondDataFromList.length).toEqual(numberOfBonds) for (let i = 1; i <= numberOfBonds; i++) { - const [validatorVoteAccount] = voteAccounts[i - 1] + const [voteAccount] = voteAccounts[i - 1] bondDataFromList = await findBonds({ program, - validatorVoteAccount, + voteAccount, }) expect(bondDataFromList.length).toEqual(1) } diff --git a/packages/validator-bonds-sdk/__tests__/test-validator/initSettlement.spec.ts b/packages/validator-bonds-sdk/__tests__/test-validator/initSettlement.spec.ts new file mode 100644 index 00000000..2a8f544a --- /dev/null +++ b/packages/validator-bonds-sdk/__tests__/test-validator/initSettlement.spec.ts @@ -0,0 +1,207 @@ +import { Keypair, PublicKey, Signer } from '@solana/web3.js' +import { + INIT_SETTLEMENT_EVENT, + InitSettlementEvent, + ValidatorBondsProgram, + findSettlements, + getSettlement, + initSettlementInstruction, + settlementAddress, + settlementAuthority, +} from '../../src' +import { getValidatorInfo, initTest, waitForNextEpoch } from './testValidator' +import { + executeInitBondInstruction, + executeInitConfigInstruction, + executeInitSettlement, +} from '../utils/testTransactions' +import { ExtendedProvider } from '../utils/provider' +import { + transaction, + Wallet, + splitAndExecuteTx, +} from '@marinade.finance/web3js-common' +import { AnchorProvider } from '@coral-xyz/anchor' + +describe('Validator Bonds init settlement', () => { + let provider: ExtendedProvider + let program: ValidatorBondsProgram + let configAccount: PublicKey + let operatorAuthority: Keypair + let validatorIdentity: Keypair + let voteAccount: PublicKey + let bondAccount: PublicKey + + beforeAll(async () => { + ;({ provider, program } = await initTest()) + ;({ validatorIdentity } = await getValidatorInfo(provider.connection)) + }) + + afterAll(async () => { + // workaround: "Jest has detected the following 1 open handle", see `initConfig.spec.ts` + await new Promise(resolve => setTimeout(resolve, 500)) + }) + + beforeEach(async () => { + ;({ configAccount, operatorAuthority } = await executeInitConfigInstruction( + { + program, + provider, + } + )) + ;({ voteAccount, bondAccount } = await executeInitBondInstruction({ + config: configAccount, + program, + provider, + validatorIdentity, + })) + }) + + it('init settlement', async () => { + const event = new Promise(resolve => { + const listener = program.addEventListener( + INIT_SETTLEMENT_EVENT, + async event => { + await program.removeEventListener(listener) + resolve(event) + } + ) + }) + + const currentEpoch = (await program.provider.connection.getEpochInfo()) + .epoch + const merkleRoot = Buffer.alloc(32) + const { settlementAccount, rentCollector } = await executeInitSettlement({ + program, + provider, + config: configAccount, + operatorAuthority, + voteAccount, + currentEpoch, + merkleRoot, + maxMerkleNodes: 2, + maxTotalClaim: 100, + }) + + const settlementData = await getSettlement(program, settlementAccount) + + const [, bump] = settlementAddress( + bondAccount, + merkleRoot, + currentEpoch, + program.programId + ) + const [settlementAuth, authorityBump] = settlementAuthority( + settlementAccount, + program.programId + ) + + expect(settlementData.bond).toEqual(bondAccount) + expect(settlementData.bumps).toEqual({ + pda: bump, + authority: authorityBump, + }) + expect(settlementData.epochCreatedAt).toEqual(currentEpoch) + expect(settlementData.maxMerkleNodes).toEqual(2) + expect(settlementData.maxTotalClaim).toEqual(100) + expect(settlementData.merkleRoot).toEqual(Array.from(merkleRoot)) + expect(settlementData.rentCollector).toEqual(rentCollector) + expect(settlementData.authority).toEqual(settlementAuth) + expect(settlementData.lamportsFunded).toEqual(0) + expect(settlementData.lamportsClaimed).toEqual(0) + expect(settlementData.merkleNodesClaimed).toEqual(0) + expect(settlementData.splitRentAmount).toEqual(0) + expect(settlementData.splitRentCollector).toEqual(null) + + await event.then(e => { + expect(e.bond).toEqual(bondAccount) + expect(e.voteAccount).toEqual(voteAccount) + expect(e.bumps).toEqual({ pda: bump, authority: authorityBump }) + expect(e.epochCreatedAt).toEqual(currentEpoch) + expect(e.maxMerkleNodes).toEqual(2) + expect(e.maxTotalClaim).toEqual(100) + expect(e.merkleRoot).toEqual(Array.from(merkleRoot)) + expect(e.rentCollector).toEqual(rentCollector) + expect(e.authority).toEqual(settlementAuth) + }) + }) + + it('find settlement', async () => { + const tx = await transaction(provider) + const signers: (Signer | Wallet)[] = [ + (provider as unknown as AnchorProvider).wallet, + operatorAuthority, + ] + + const numberOfSettlements = 19 + + // we want to be at the beginning of the epoch + await waitForNextEpoch(provider.connection, 15) + const currentEpoch = (await program.provider.connection.getEpochInfo()) + .epoch + const buffers: Buffer[] = [] + for (let i = 1; i <= numberOfSettlements; i++) { + const buffer = Buffer.from( + Array.from({ length: 32 }, () => Math.floor(Math.random() * 256)) + ) + buffers.push(buffer) + const { instruction } = await initSettlementInstruction({ + program, + bondAccount, + operatorAuthority, + configAccount, + merkleRoot: buffer, + currentEpoch, + voteAccount, + maxTotalClaim: 1, + maxMerkleNodes: 11, + }) + tx.add(instruction) + } + expect(tx.instructions.length).toEqual(numberOfSettlements) + + await splitAndExecuteTx({ + connection: provider.connection, + transaction: tx, + signers, + errMessage: 'Failed to init bonds and withdraw requests', + }) + + let settlementList = await findSettlements({ + program, + epoch: currentEpoch, + }) + expect(settlementList.length).toBeGreaterThanOrEqual(numberOfSettlements) + + settlementList = await findSettlements({ + program, + bond: bondAccount, + }) + expect(settlementList.length).toEqual(numberOfSettlements) + + settlementList = await findSettlements({ + program, + bond: bondAccount, + epoch: currentEpoch, + }) + expect(settlementList.length).toEqual(numberOfSettlements) + + settlementList = await findSettlements({ program }) + expect(settlementList.length).toBeGreaterThanOrEqual(numberOfSettlements) + + for (let i = 0; i < numberOfSettlements; i++) { + settlementList = await findSettlements({ + program, + merkleRoot: buffers[i], + }) + expect(settlementList.length).toEqual(1) + settlementList = await findSettlements({ + program, + bond: bondAccount, + epoch: currentEpoch, + merkleRoot: buffers[i], + }) + expect(settlementList.length).toEqual(1) + } + }) +}) diff --git a/packages/validator-bonds-sdk/__tests__/test-validator/initWithdrawRequest.spec.ts b/packages/validator-bonds-sdk/__tests__/test-validator/initWithdrawRequest.spec.ts index 02ae23cb..aa2b9f29 100644 --- a/packages/validator-bonds-sdk/__tests__/test-validator/initWithdrawRequest.spec.ts +++ b/packages/validator-bonds-sdk/__tests__/test-validator/initWithdrawRequest.spec.ts @@ -1,19 +1,31 @@ -import { Keypair, LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js' +import { Keypair, LAMPORTS_PER_SOL, PublicKey, Signer } from '@solana/web3.js' import { INIT_WITHDRAW_REQUEST_EVENT, InitWithdrawRequestEvent, ValidatorBondsProgram, + findWithdrawRequests, getWithdrawRequest, + initBondInstruction, initWithdrawRequestInstruction, withdrawRequestAddress, } from '../../src' -import { initTest } from './testValidator' +import { getValidatorInfo, initTest, waitForNextEpoch } from './testValidator' import { executeInitBondInstruction, executeInitConfigInstruction, } from '../utils/testTransactions' import { ExtendedProvider } from '../utils/provider' -import { createVoteAccount } from '../utils/staking' +import { + createVoteAccount, + createVoteAccountWithIdentity, +} from '../utils/staking' +import { AnchorProvider } from '@coral-xyz/anchor' +import { + Wallet, + splitAndExecuteTx, + signer, + transaction, +} from '@marinade.finance/web3js-common' describe('Validator Bonds init withdraw request', () => { let provider: ExtendedProvider @@ -21,10 +33,12 @@ describe('Validator Bonds init withdraw request', () => { let configAccount: PublicKey let bondAccount: PublicKey let bondAuthority: Keypair - let validatorVoteAccount: PublicKey + let voteAccount: PublicKey + let validatorIdentity: Keypair beforeAll(async () => { ;({ provider, program } = await initTest()) + ;({ validatorIdentity } = await getValidatorInfo(provider.connection)) }) afterAll(async () => { @@ -37,16 +51,16 @@ describe('Validator Bonds init withdraw request', () => { program, provider, })) - const { voteAccount, validatorIdentity } = await createVoteAccount(provider) - validatorVoteAccount = voteAccount - ;({ bondAccount, bondAuthority } = await executeInitBondInstruction( + const { voteAccount: validatorVoteAccount, validatorIdentity } = + await createVoteAccount({ provider }) + voteAccount = validatorVoteAccount + ;({ bondAccount, bondAuthority } = await executeInitBondInstruction({ program, provider, - configAccount, - undefined, - validatorVoteAccount, - validatorIdentity - )) + config: configAccount, + voteAccount, + validatorIdentity, + })) }) it('init withdraw request', async () => { @@ -70,6 +84,12 @@ describe('Validator Bonds init withdraw request', () => { }) await provider.sendIx([bondAuthority], instruction) + const withdrawRequestList = await findWithdrawRequests({ + program, + voteAccount, + }) + expect(withdrawRequestList.length).toEqual(1) + const epoch = (await provider.connection.getEpochInfo()).epoch const [, bump] = withdrawRequestAddress(bondAccount, program.programId) const withdrawRequestData = await getWithdrawRequest( @@ -80,9 +100,7 @@ describe('Validator Bonds init withdraw request', () => { expect(withdrawRequestData.bump).toEqual(bump) expect(withdrawRequestData.epoch).toEqual(epoch) expect(withdrawRequestData.requestedAmount).toEqual(2 * LAMPORTS_PER_SOL) - expect(withdrawRequestData.validatorVoteAccount).toEqual( - validatorVoteAccount - ) + expect(withdrawRequestData.voteAccount).toEqual(voteAccount) expect(withdrawRequestData.withdrawnAmount).toEqual(0) await event.then(e => { @@ -91,7 +109,96 @@ describe('Validator Bonds init withdraw request', () => { expect(e.bump).toEqual(bump) expect(e.epoch).toEqual(epoch) expect(e.requestedAmount).toEqual(2 * LAMPORTS_PER_SOL) - expect(e.validatorVoteAccount).toEqual(validatorVoteAccount) + expect(e.voteAccount).toEqual(voteAccount) + }) + }) + + it('find withdraw request', async () => { + const tx = await transaction(provider) + const signers: (Signer | Wallet)[] = [ + (provider as unknown as AnchorProvider).wallet, + ] + + const numberOfBonds = 24 + + signers.push(signer(validatorIdentity)) + const voteAndBonds: [PublicKey, PublicKey][] = [] + for (let i = 1; i <= numberOfBonds; i++) { + const { voteAccount: voteAccount } = await createVoteAccountWithIdentity( + provider, + validatorIdentity + ) + voteAndBonds.push([voteAccount, PublicKey.default]) + } + + const bondAuthority = Keypair.generate() + signers.push(signer(bondAuthority)) + for (let i = 1; i <= numberOfBonds; i++) { + const [voteAccount] = voteAndBonds[i - 1] + const { instruction, bondAccount } = await initBondInstruction({ + program, + configAccount, + bondAuthority: bondAuthority.publicKey, + cpmpe: Math.floor(Math.random() * 100), + voteAccount, + validatorIdentity, + }) + tx.add(instruction) + voteAndBonds[i - 1][1] = bondAccount + } + await waitForNextEpoch(provider.connection, 15) + for (let i = 1; i <= numberOfBonds; i++) { + const [voteAccount, bondAccount] = voteAndBonds[i - 1] + const { instruction } = await initWithdrawRequestInstruction({ + program, + bondAccount, + configAccount, + authority: bondAuthority, + amount: 2 * LAMPORTS_PER_SOL, + voteAccount, + }) + tx.add(instruction) + } + expect(tx.instructions.length).toEqual(numberOfBonds * 2) + const currentEpoch = Number( + (await provider.connection.getEpochInfo()).epoch + ) + await splitAndExecuteTx({ + connection: provider.connection, + transaction: tx, + signers, + errMessage: 'Failed to init bonds and withdraw requests', + }) + + let withdrawRequestList = await findWithdrawRequests({ + program, + epoch: currentEpoch, }) + expect(withdrawRequestList.length).toEqual(numberOfBonds) + + withdrawRequestList = await findWithdrawRequests({ + program, + bond: voteAndBonds[0][1], + voteAccount: voteAndBonds[0][0], + epoch: currentEpoch, + }) + expect(withdrawRequestList.length).toEqual(1) + + withdrawRequestList = await findWithdrawRequests({ program }) + expect(withdrawRequestList.length).toBeGreaterThanOrEqual(numberOfBonds) + + for (let i = 1; i <= numberOfBonds; i++) { + const [voteAccount, bondAccount] = voteAndBonds[i - 1] + withdrawRequestList = await findWithdrawRequests({ + program, + bond: bondAccount, + }) + expect(withdrawRequestList.length).toEqual(1) + withdrawRequestList = await findWithdrawRequests({ + program, + voteAccount: voteAccount, + }) + expect(withdrawRequestList.length).toEqual(1) + } }) }) diff --git a/packages/validator-bonds-sdk/__tests__/test-validator/reset.spec.ts b/packages/validator-bonds-sdk/__tests__/test-validator/reset.spec.ts new file mode 100644 index 00000000..4c744b88 --- /dev/null +++ b/packages/validator-bonds-sdk/__tests__/test-validator/reset.spec.ts @@ -0,0 +1,116 @@ +import { Keypair, LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js' +import { + RESET_EVENT, + ResetEvent, + U64_MAX, + ValidatorBondsProgram, + getStakeAccount, + resetInstruction, + settlementAuthority, + withdrawerAuthority, +} from '../../src' +import { initTest } from './testValidator' +import { + executeInitBondInstruction, + executeInitConfigInstruction, +} from '../utils/testTransactions' +import { ExtendedProvider } from '../utils/provider' +import { + createSettlementFundedStakeAccount, + createVoteAccount, +} from '../utils/staking' + +describe('Validator Bonds reset settlement stake account', () => { + let provider: ExtendedProvider + let program: ValidatorBondsProgram + let configAccount: PublicKey + let voteAccount: PublicKey + let bondAccount: PublicKey + + beforeAll(async () => { + ;({ provider, program } = await initTest()) + }) + + afterAll(async () => { + // workaround: "Jest has detected the following 1 open handle", see `initConfig.spec.ts` + await new Promise(resolve => setTimeout(resolve, 500)) + }) + + beforeEach(async () => { + ;({ configAccount } = await executeInitConfigInstruction({ + program, + provider, + })) + const { voteAccount: validatorVoteAccount, validatorIdentity } = + await createVoteAccount({ provider }) + voteAccount = validatorVoteAccount + ;({ bondAccount } = await executeInitBondInstruction({ + program, + provider, + config: configAccount, + voteAccount, + validatorIdentity, + })) + }) + + it('reset', async () => { + const event = new Promise(resolve => { + const listener = program.addEventListener(RESET_EVENT, async event => { + await program.removeEventListener(listener) + resolve(event) + }) + }) + + const fakeSettlement = Keypair.generate().publicKey + const stakeAccount = await createSettlementFundedStakeAccount({ + program, + provider, + config: configAccount, + settlement: fakeSettlement, + voteAccount, + lamports: LAMPORTS_PER_SOL * 54, + }) + + const [bondWithdrawer] = withdrawerAuthority( + configAccount, + program.programId + ) + const [settlementAuth] = settlementAuthority( + fakeSettlement, + program.programId + ) + + let stakeAccountData = await getStakeAccount(provider, stakeAccount) + expect(stakeAccountData.staker).toEqual(settlementAuth) + expect(stakeAccountData.withdrawer).toEqual(bondWithdrawer) + + const { instruction } = await resetInstruction({ + program, + configAccount, + stakeAccount, + voteAccount, + settlementAccount: fakeSettlement, + }) + await provider.sendIx([], instruction) + + stakeAccountData = await getStakeAccount(provider, stakeAccount) + expect(stakeAccountData.staker).toEqual(bondWithdrawer) + expect(stakeAccountData.withdrawer).toEqual(bondWithdrawer) + expect(stakeAccountData.voter).toEqual(voteAccount) + expect(stakeAccountData.deactivationEpoch).toEqual(U64_MAX) + expect(stakeAccountData.activationEpoch).toEqual( + (await provider.connection.getEpochInfo()).epoch + ) + expect(stakeAccountData.isCoolingDown).toEqual(false) + expect(stakeAccountData.isLockedUp).toBeFalsy() + + await event.then(e => { + expect(e.bond).toEqual(bondAccount) + expect(e.stakeAccount).toEqual(stakeAccount) + expect(e.config).toEqual(configAccount) + expect(e.settlement).toEqual(fakeSettlement) + expect(e.settlementAuthority).toEqual(settlementAuth) + expect(e.voteAccount).toEqual(voteAccount) + }) + }) +}) diff --git a/packages/validator-bonds-sdk/__tests__/test-validator/web3js.utils.spec.ts b/packages/validator-bonds-sdk/__tests__/test-validator/web3js.utils.spec.ts new file mode 100644 index 00000000..210d565e --- /dev/null +++ b/packages/validator-bonds-sdk/__tests__/test-validator/web3js.utils.spec.ts @@ -0,0 +1,2 @@ +// TODO: add tests for web3.js/stakeAccount.ts +// need to test if findStakeAccount works correctly diff --git a/packages/validator-bonds-sdk/__tests__/utils/helpers.ts b/packages/validator-bonds-sdk/__tests__/utils/helpers.ts index 6861e0a5..c096fb60 100644 --- a/packages/validator-bonds-sdk/__tests__/utils/helpers.ts +++ b/packages/validator-bonds-sdk/__tests__/utils/helpers.ts @@ -1,12 +1,14 @@ import { Wallet as WalletInterface } from '@coral-xyz/anchor/dist/cjs/provider' import { ExtendedProvider } from './provider' import { + PublicKey, Signer, Transaction, TransactionInstruction, TransactionInstructionCtorFields, } from '@solana/web3.js' import { checkErrorMessage } from '@marinade.finance/anchor-common' +import assert from 'assert' export async function verifyErrorMessage( provider: ExtendedProvider, @@ -34,3 +36,14 @@ export async function verifyErrorMessage( } } } + +export async function getRentExempt( + provider: ExtendedProvider, + account: PublicKey +): Promise { + const accountInfo = await provider.connection.getAccountInfo(account) + assert(accountInfo !== null) + return await provider.connection.getMinimumBalanceForRentExemption( + accountInfo.data.length + ) +} diff --git a/packages/validator-bonds-sdk/__tests__/utils/merkleTreeTestData.ts b/packages/validator-bonds-sdk/__tests__/utils/merkleTreeTestData.ts new file mode 100644 index 00000000..410e33f9 --- /dev/null +++ b/packages/validator-bonds-sdk/__tests__/utils/merkleTreeTestData.ts @@ -0,0 +1,262 @@ +import { bs58 } from '@coral-xyz/anchor/dist/cjs/utils/bytes' +import { MerkleTreeNode, withdrawerAuthority } from '../../src' +import { Keypair, PublicKey } from '@solana/web3.js' +import BN from 'bn.js' + +export const MERKLE_PROOF = 'CJWSpJD2yeL1JPUH9pyfAefFetdiSuvPNCqq5LfQ71je' +export const MERKLE_ROOT_BUF = bs58.decode(MERKLE_PROOF) + +export const configAccount = new PublicKey( + '4wQELTA1RMEM3cKN7gjbiNN247e3GY9Sga7MKpNV38kL' +) +export const configAccountKeypair = Keypair.fromSecretKey( + new Uint8Array([ + 195, 59, 42, 183, 63, 138, 218, 169, 10, 100, 131, 107, 2, 115, 249, 203, + 208, 118, 243, 242, 24, 147, 123, 88, 139, 227, 106, 207, 94, 218, 99, 100, + 58, 130, 176, 204, 178, 57, 15, 228, 92, 42, 250, 174, 237, 156, 164, 110, + 140, 9, 134, 240, 11, 218, 244, 246, 119, 158, 226, 206, 102, 189, 44, 189, + ]) +) +export const [stakeAuthority] = withdrawerAuthority(configAccount) + +export const voteAccount1 = new PublicKey( + 'FHUuZcuLB3ZLWZhKoY7metTEJ2Y2Xton99TTuDmzFmgW' +) +export const voteAccount1Keypair = Keypair.fromSecretKey( + new Uint8Array([ + 237, 246, 189, 191, 50, 152, 232, 64, 134, 120, 210, 214, 194, 111, 53, 133, + 170, 199, 146, 119, 157, 49, 109, 243, 195, 101, 77, 247, 84, 24, 140, 91, + 212, 60, 118, 175, 30, 52, 179, 95, 71, 227, 218, 208, 181, 105, 0, 118, + 215, 81, 90, 129, 131, 7, 0, 112, 16, 195, 54, 165, 197, 132, 148, 99, + ]) +) +export const voteAccount2 = new PublicKey( + '9D6EuvndvhgDBLRzpxNjHdvLWicJE1WvZrdTbapjhKR6' +) +export const voteAccount2Keypair = Keypair.fromSecretKey( + new Uint8Array([ + 158, 19, 28, 228, 253, 204, 120, 137, 23, 230, 13, 29, 237, 102, 35, 165, + 229, 88, 46, 52, 155, 70, 76, 191, 107, 215, 89, 254, 81, 194, 210, 246, + 121, 246, 99, 205, 241, 99, 163, 208, 21, 194, 189, 10, 12, 150, 243, 133, + 109, 226, 97, 167, 38, 231, 184, 41, 76, 143, 181, 153, 145, 234, 174, 125, + ]) +) + +export const withdrawer1 = new PublicKey( + '3vGstFWWyQbDknu9WKr9vbTn2Kw5qgorP7UkRXVrfe9t' +) +export const withdrawer1Keypair = Keypair.fromSecretKey( + new Uint8Array([ + 24, 43, 11, 179, 150, 224, 217, 74, 162, 155, 151, 213, 201, 83, 185, 19, + 246, 232, 231, 211, 169, 98, 182, 164, 121, 32, 13, 149, 173, 20, 162, 79, + 43, 93, 27, 248, 91, 110, 139, 170, 254, 199, 133, 92, 39, 0, 152, 214, 250, + 62, 25, 69, 251, 157, 144, 190, 219, 23, 97, 15, 224, 80, 64, 55, + ]) +) +export const withdrawer2 = new PublicKey( + 'DBnWKq1Ln9y8HtGwYxFMqMWLY1Ld9xpB28ayKfHejiTs' +) +export const withdrawer2Keypair = Keypair.fromSecretKey( + new Uint8Array([ + 203, 169, 131, 90, 255, 189, 179, 151, 246, 221, 4, 202, 168, 89, 103, 56, + 157, 52, 187, 22, 120, 178, 211, 8, 225, 71, 217, 211, 169, 238, 96, 10, + 181, 15, 129, 42, 37, 41, 183, 202, 199, 50, 186, 123, 22, 52, 73, 23, 52, + 93, 14, 155, 96, 140, 165, 205, 167, 146, 16, 93, 55, 109, 137, 58, + ]) +) +export const withdrawer3 = new PublicKey( + 'CgoqXy3e1hsnuNw6bJ8iuzqZwr93CA4jsRa1AnsseJ53' +) +export const withdrawer3Keypair = Keypair.fromSecretKey( + new Uint8Array([ + 229, 228, 121, 248, 83, 69, 46, 5, 231, 40, 199, 127, 48, 139, 100, 228, 69, + 221, 133, 64, 199, 252, 158, 244, 226, 80, 66, 188, 168, 164, 93, 248, 173, + 163, 42, 144, 216, 187, 230, 250, 231, 216, 255, 149, 48, 250, 11, 4, 144, + 101, 205, 13, 212, 139, 234, 174, 137, 193, 203, 120, 62, 72, 48, 54, + ]) +) + +// TODO: delete me +// new PublicKey(9bvgE5dz7cKi2774nrEAW26b8r9YecH3Xzk7ZJFinBgi +// ) +// Keypair.fromSecretKey(new Uint8Array([205,234,126,103,105,119,64,125,57,229,49,149,89,113,100,223,223,177,205,41,234,133,82,48,87,135,73,64,178,229,174,135,127,207,239,86,25,196,108,213,181,212,252,208,208,204,247,56,222,91,180,18,20,249,217,138,84,102,21,135,66,73,221,255])) +// new PublicKey(DVamVtXcmLMJEGb81HkLcgdppGBuN5GqjL5PWGnWAN4t +// ) +// Keypair.fromSecretKey(new Uint8Array([14,213,177,117,226,3,88,196,31,5,84,164,111,252,169,144,70,142,160,201,184,18,245,59,6,182,43,207,158,194,125,154,185,158,175,188,200,211,91,132,140,157,16,95,36,251,31,216,65,16,30,227,146,221,222,205,173,210,154,59,39,97,208,109])) +// new PublicKey(9hiL6DJDzYZHvPGgvNGT14XwwwdkV7fhTuUBZoKimQWD +// ) +// Keypair.fromSecretKey(new Uint8Array([221,116,216,250,193,38,81,209,120,48,2,1,210,47,73,190,244,125,94,160,69,251,53,115,229,17,164,105,173,171,236,26,129,75,117,114,94,94,130,230,188,184,76,171,189,237,43,232,198,89,239,254,114,64,69,17,2,104,251,222,43,147,40,10])) +// new PublicKey(G4C91n221dSr7Pip591hhMp9vXDbobX98PT8uAxhM4kB +// ) +// Keypair.fromSecretKey(new Uint8Array([238,106,132,88,200,237,6,169,64,232,243,104,168,64,238,231,111,255,218,30,226,30,79,75,2,190,87,123,245,220,103,68,223,176,163,233,136,43,221,240,98,54,247,113,189,34,250,224,58,157,91,232,100,232,6,95,158,100,36,209,105,149,1,116])) +// new PublicKey(5vGBkuJSpdaCdrZkTrhdwWZJESu14X5w3Naq6dPkm6Az +// ) +// Keypair.fromSecretKey(new Uint8Array([165,72,173,177,206,64,255,239,247,137,231,194,227,101,25,217,35,82,2,55,229,130,110,32,241,36,104,132,245,67,211,214,73,19,179,106,157,62,20,215,18,17,143,217,8,24,111,202,241,21,94,23,38,25,182,76,86,4,161,145,92,251,51,199])) +// new PublicKey(FZryDKqbsZRYXjvrz16N28eHaWBJGGk8DZyAGxJjJ1R1 +// ) +// export const configAccount2 = new PublicKey('FmS9SKrALCkuxfEDhwk52Fi9614xhScF2PHSrQoZmmEY') +// export const configAccount2Keypair = Keypair.fromSecretKey(new Uint8Array([128,56,10,62,22,237,67,32,128,93,175,52,51,181,21,157,163,21,108,146,70,89,249,98,54,60,166,14,18,135,222,182,219,101,154,201,2,129,96,1,202,71,74,207,63,104,143,129,128,235,44,246,89,173,189,55,102,107,241,202,182,150,52,33])) +// export const stakeAuthority2 = withdrawerAuthority(configAccount2) + +export const ITEMS: { treeNode: MerkleTreeNode; proof: number[][] }[] = [ + { + // tree node hash: CUgEsyuML4P22hjkvmU6MRHi5jHvpPaB7UvD5t1V4uFc + treeNode: new MerkleTreeNode({ + withdrawAuthority: withdrawer1.toBase58(), + stakeAuthority: stakeAuthority.toBase58(), + voteAccount: voteAccount1.toBase58(), + claim: 1234, + }), + proof: [ + [ + 131, 152, 159, 242, 119, 184, 154, 81, 208, 199, 41, 49, 231, 168, 233, + 237, 3, 179, 126, 106, 23, 135, 213, 74, 127, 83, 255, 105, 86, 250, 63, + 23, + ], + [ + 113, 143, 128, 112, 238, 209, 97, 96, 248, 220, 18, 78, 229, 222, 253, + 207, 114, 129, 14, 161, 181, 23, 254, 81, 84, 119, 196, 162, 86, 45, + 205, 54, + ], + [ + 147, 52, 104, 182, 174, 190, 248, 228, 27, 240, 240, 245, 6, 218, 13, + 196, 53, 63, 242, 117, 208, 239, 15, 106, 255, 30, 248, 47, 107, 170, + 233, 94, + ], + ], + }, + { + // tree node hash: 3pwY1X2iV6vofeswACq53ywUE55Ux8BBY5EwxXApi3rA + treeNode: new MerkleTreeNode({ + withdrawAuthority: withdrawer2.toBase58(), + stakeAuthority: stakeAuthority.toBase58(), + voteAccount: voteAccount1.toBase58(), + claim: 99999, + }), + proof: [ + [ + 230, 150, 49, 236, 10, 41, 122, 129, 64, 55, 46, 243, 120, 207, 147, + 226, 10, 100, 14, 160, 68, 85, 238, 179, 252, 103, 90, 71, 63, 137, 115, + 100, + ], + [ + 113, 143, 128, 112, 238, 209, 97, 96, 248, 220, 18, 78, 229, 222, 253, + 207, 114, 129, 14, 161, 181, 23, 254, 81, 84, 119, 196, 162, 86, 45, + 205, 54, + ], + [ + 147, 52, 104, 182, 174, 190, 248, 228, 27, 240, 240, 245, 6, 218, 13, + 196, 53, 63, 242, 117, 208, 239, 15, 106, 255, 30, 248, 47, 107, 170, + 233, 94, + ], + ], + }, + { + // tree node hash: EtS2pCv7w21NaRUdgd11ECSbrLrYezNgJqPbki6GC3hj + treeNode: new MerkleTreeNode({ + withdrawAuthority: withdrawer3.toBase58(), + stakeAuthority: stakeAuthority.toBase58(), + voteAccount: voteAccount1.toBase58(), + claim: 212121, + }), + proof: [ + [ + 56, 7, 110, 103, 58, 212, 164, 78, 150, 160, 80, 24, 209, 221, 112, 197, + 170, 62, 83, 209, 7, 111, 140, 113, 52, 73, 3, 38, 135, 169, 19, 181, + ], + [ + 190, 99, 233, 8, 249, 68, 135, 70, 128, 15, 2, 169, 47, 194, 102, 12, + 200, 64, 213, 103, 134, 64, 112, 215, 201, 36, 212, 236, 32, 93, 76, + 106, + ], + [ + 147, 52, 104, 182, 174, 190, 248, 228, 27, 240, 240, 245, 6, 218, 13, + 196, 53, 63, 242, 117, 208, 239, 15, 106, 255, 30, 248, 47, 107, 170, + 233, 94, + ], + ], + }, + { + // tree node hash: C94ftStYh3afdysMEnf4KGMvyQZVcMY6P16UkEJGkYbU + treeNode: new MerkleTreeNode({ + withdrawAuthority: withdrawer1.toBase58(), + stakeAuthority: stakeAuthority.toBase58(), + voteAccount: voteAccount2.toBase58(), + claim: 69, + }), + proof: [ + [ + 217, 141, 69, 36, 65, 205, 32, 76, 165, 35, 197, 94, 188, 141, 93, 158, + 129, 239, 253, 174, 42, 156, 151, 29, 197, 253, 160, 116, 10, 112, 12, + 10, + ], + [ + 190, 99, 233, 8, 249, 68, 135, 70, 128, 15, 2, 169, 47, 194, 102, 12, + 200, 64, 213, 103, 134, 64, 112, 215, 201, 36, 212, 236, 32, 93, 76, + 106, + ], + [ + 147, 52, 104, 182, 174, 190, 248, 228, 27, 240, 240, 245, 6, 218, 13, + 196, 53, 63, 242, 117, 208, 239, 15, 106, 255, 30, 248, 47, 107, 170, + 233, 94, + ], + ], + }, + { + // tree node hash: C6NjywmM6srH8Cseo54Z8QAEy9AiLieZfjoY3T3Whs5u + treeNode: new MerkleTreeNode({ + withdrawAuthority: withdrawer2.toBase58(), + stakeAuthority: stakeAuthority.toBase58(), + voteAccount: voteAccount2.toBase58(), + claim: 111111, + }), + proof: [ + [ + 11, 90, 12, 238, 182, 194, 37, 113, 192, 160, 195, 72, 154, 15, 243, + 131, 143, 156, 169, 22, 83, 184, 141, 22, 119, 27, 143, 77, 156, 9, 216, + 244, + ], + [ + 41, 234, 24, 0, 236, 27, 21, 81, 222, 105, 251, 199, 172, 210, 12, 100, + 139, 85, 239, 237, 217, 227, 55, 20, 103, 35, 170, 84, 91, 184, 224, + 200, + ], + [ + 44, 180, 56, 187, 76, 83, 100, 85, 154, 245, 73, 252, 42, 103, 30, 96, + 53, 35, 46, 47, 17, 67, 81, 88, 202, 55, 44, 61, 55, 49, 144, 165, + ], + ], + }, +] + +export const treeNodesVoteAccount1 = ITEMS.filter( + item => item.treeNode.data.voteAccount === voteAccount1.toBase58() +) +export const totalClaimVoteAccount1 = treeNodesVoteAccount1.reduce( + (acc, item) => acc.add(item.treeNode.data.claim), + new BN(0) +) +export const treeNodesVoteAccount2 = ITEMS.filter( + item => item.treeNode.data.voteAccount === voteAccount2.toBase58() +) +export const totalClaimVoteAccount2 = treeNodesVoteAccount2.reduce( + (acc, item) => acc.add(item.treeNode.data.claim), + new BN(0) +) + +export function treeNodeBy( + voteAccount: PublicKey, + withdrawer: PublicKey +): { + treeNode: MerkleTreeNode + proof: number[][] +} { + const treeNodesFiltered = ITEMS.filter( + item => item.treeNode.data.voteAccount === voteAccount.toBase58() + ) + const treeNodesByWithdrawer = treeNodesFiltered.filter( + r => r.treeNode.data.withdrawAuthority === withdrawer.toBase58() + ) + expect(treeNodesByWithdrawer.length).toBe(1) + return treeNodesByWithdrawer[0] +} diff --git a/packages/validator-bonds-sdk/__tests__/utils/staking.ts b/packages/validator-bonds-sdk/__tests__/utils/staking.ts index 6ebbb97c..e9375056 100644 --- a/packages/validator-bonds-sdk/__tests__/utils/staking.ts +++ b/packages/validator-bonds-sdk/__tests__/utils/staking.ts @@ -21,6 +21,11 @@ import { } from '@marinade.finance/marinade-ts-sdk/dist/src/marinade-state/borsh/stake-state' import assert from 'assert' import { pubkey } from '@marinade.finance/web3js-common' +import { + ValidatorBondsProgram, + settlementAuthority, + withdrawerAuthority, +} from '../../src' // Depending if new vote account feature-set is gated on. // It can be 3762 or 3736 @@ -186,25 +191,28 @@ export async function createVoteAccountWithIdentity( provider: ExtendedProvider, validatorIdentity: Keypair ): Promise { - return await createVoteAccount( + return await createVoteAccount({ provider, - undefined, - undefined, - undefined, - validatorIdentity - ) + validatorIdentity, + }) } -export async function createVoteAccount( - provider: ExtendedProvider, - rentExempt?: number, - authorizedVoter?: Keypair, - authorizedWithdrawer?: Keypair, +export async function createVoteAccount({ + provider, + rentExempt, + authorizedVoter, + authorizedWithdrawer, + validatorIdentity, + voteAccount = Keypair.generate(), +}: { + provider: ExtendedProvider + rentExempt?: number + authorizedVoter?: Keypair + authorizedWithdrawer?: Keypair validatorIdentity?: Keypair -): Promise { + voteAccount?: Keypair +}): Promise { rentExempt = await getRentExemptVote(provider, rentExempt) - - const voteAccount = Keypair.generate() validatorIdentity = validatorIdentity ?? Keypair.generate() authorizedVoter = authorizedVoter ?? Keypair.generate() authorizedWithdrawer = authorizedWithdrawer ?? Keypair.generate() @@ -307,7 +315,8 @@ export async function delegatedStakeAccount({ voteAccountToDelegate = voteAccountToDelegate ?? - (await createVoteAccount(provider, rentExemptVote)).voteAccount + (await createVoteAccount({ provider, rentExempt: rentExemptVote })) + .voteAccount const createStakeAccountIx = StakeProgram.createAccount({ fromPubkey: provider.walletPubkey, @@ -337,6 +346,84 @@ export async function delegatedStakeAccount({ } } +export async function createBondsFundedStakeAccount({ + program, + provider, + config, + lamports, + voteAccount, +}: { + program: ValidatorBondsProgram + provider: ExtendedProvider + config: PublicKey + lamports: number + voteAccount: PublicKey +}): Promise { + const [bondsAuth] = withdrawerAuthority(config, program.programId) + return await createStakeAccount({ + provider, + voteAccount, + lamports, + newWithdrawerAuthority: bondsAuth, + newStakerAuthority: bondsAuth, + }) +} + +export async function createSettlementFundedStakeAccount({ + program, + provider, + config, + settlement, + voteAccount, + lamports, +}: { + program: ValidatorBondsProgram + provider: ExtendedProvider + config: PublicKey + settlement: PublicKey + voteAccount: PublicKey + lamports: number +}): Promise { + const [bondsAuth] = withdrawerAuthority(config, program.programId) + const [settlementAuth] = settlementAuthority(settlement, program.programId) + return await createStakeAccount({ + provider, + voteAccount, + lamports, + newWithdrawerAuthority: bondsAuth, + newStakerAuthority: settlementAuth, + }) +} + +export async function createStakeAccount({ + provider, + lamports, + voteAccount, + newWithdrawerAuthority, + newStakerAuthority, +}: { + provider: ExtendedProvider + lamports: number + voteAccount: PublicKey + newWithdrawerAuthority: PublicKey + newStakerAuthority: PublicKey +}): Promise { + const { stakeAccount, withdrawer: initWithdrawer } = + await delegatedStakeAccount({ + provider, + lamports, + voteAccountToDelegate: voteAccount, + }) + await authorizeStakeAccount({ + provider, + authority: initWithdrawer, + stakeAccount: stakeAccount, + withdrawer: newWithdrawerAuthority, + staker: newStakerAuthority, + }) + return stakeAccount +} + export async function nonInitializedStakeAccount( provider: ExtendedProvider, rentExempt?: number diff --git a/packages/validator-bonds-sdk/__tests__/utils/testTransactions.ts b/packages/validator-bonds-sdk/__tests__/utils/testTransactions.ts index 449c3da8..7f8f3045 100644 --- a/packages/validator-bonds-sdk/__tests__/utils/testTransactions.ts +++ b/packages/validator-bonds-sdk/__tests__/utils/testTransactions.ts @@ -5,10 +5,12 @@ import { getBond, initBondInstruction, initConfigInstruction, + initSettlementInstruction, initWithdrawRequestInstruction, withdrawerAuthority, } from '../../src' import { + ComputeBudgetProgram, Keypair, LAMPORTS_PER_SOL, PublicKey, @@ -16,7 +18,7 @@ import { SystemProgram, } from '@solana/web3.js' import { ExtendedProvider } from './provider' -import { createVoteAccount } from './staking' +import { createVoteAccount, createVoteAccountWithIdentity } from './staking' import BN from 'bn.js' import assert from 'assert' import { pubkey, signer } from '@marinade.finance/web3js-common' @@ -24,18 +26,20 @@ import { pubkey, signer } from '@marinade.finance/web3js-common' export async function createUserAndFund( provider: ExtendedProvider, lamports = LAMPORTS_PER_SOL, - user: Keypair = Keypair.generate() -): Promise { + user: Keypair | PublicKey = Keypair.generate() +): Promise { const instruction = SystemProgram.transfer({ fromPubkey: provider.walletPubkey, - toPubkey: user.publicKey, + toPubkey: pubkey(user), lamports, }) try { await provider.sendIx([], instruction) } catch (e) { console.error( - `createUserAndFund: to fund ${user.publicKey.toBase58()} with ${lamports} lamports`, + `createUserAndFund: to fund ${pubkey( + user + ).toBase58()} with ${lamports} lamports`, e ) throw e @@ -132,15 +136,23 @@ export async function executeInitConfigInstruction({ } } -export async function executeInitBondInstruction( - program: ValidatorBondsProgram, - provider: ExtendedProvider, - config: PublicKey, - bondAuthority?: Keypair, - voteAccount?: PublicKey, - validatorIdentity?: Keypair, - revenueShareHundredthBps: BN | number = Math.floor(Math.random() * 100) + 1 -): Promise<{ +export async function executeInitBondInstruction({ + program, + provider, + config, + bondAuthority, + voteAccount, + validatorIdentity, + cpmpe = Math.floor(Math.random() * 100) + 1, +}: { + program: ValidatorBondsProgram + provider: ExtendedProvider + config: PublicKey + bondAuthority?: Keypair + voteAccount?: PublicKey + validatorIdentity?: Keypair + cpmpe?: BN | number +}): Promise<{ bondAccount: PublicKey bondAuthority: Keypair voteAccount: PublicKey @@ -148,7 +160,16 @@ export async function executeInitBondInstruction( }> { bondAuthority = bondAuthority ?? Keypair.generate() if (!voteAccount) { - ;({ voteAccount, validatorIdentity } = await createVoteAccount(provider)) + if (validatorIdentity !== undefined) { + ;({ voteAccount } = await createVoteAccountWithIdentity( + provider, + validatorIdentity + )) + } else { + ;({ validatorIdentity, voteAccount } = await createVoteAccount({ + provider, + })) + } } if (validatorIdentity === undefined) { throw new Error( @@ -159,12 +180,15 @@ export async function executeInitBondInstruction( program, configAccount: config, bondAuthority: bondAuthority.publicKey, - revenueShareHundredthBps, - validatorVoteAccount: voteAccount, + cpmpe, + voteAccount, validatorIdentity: validatorIdentity.publicKey, }) try { await provider.sendIx([validatorIdentity], instruction) + expect( + provider.connection.getAccountInfo(bondAccount) + ).resolves.not.toBeNull() } catch (e) { console.error( `executeInitBondInstruction: bond account ${pubkey( @@ -217,11 +241,11 @@ export async function executeFundBondInstruction({ })) } ;({ bondAccount, bondAuthority, voteAccount } = - await executeInitBondInstruction(program, provider, config)) + await executeInitBondInstruction({ program, provider, config })) } else { const bondData = await getBond(program, bondAccount) bondAuthority = bondData.authority - voteAccount = bondData.validatorVoteAccount + voteAccount = bondData.voteAccount config = bondData.config } @@ -234,7 +258,7 @@ export async function executeFundBondInstruction({ program, configAccount: config, bondAccount, - validatorVoteAccount: voteAccount, + voteAccount: voteAccount, stakeAccount, stakeAccountAuthority, }) @@ -296,12 +320,16 @@ export async function executeInitWithdrawRequestInstruction({ })) } ;({ bondAccount, validatorIdentity, bondAuthority, voteAccount } = - await executeInitBondInstruction(program, provider, configAccount)) + await executeInitBondInstruction({ + program, + provider, + config: configAccount, + })) } else { const bondData = await getBond(program, bondAccount) bondAuthority = bondData.authority configAccount = configAccount ?? bondData.config - voteAccount = bondData.validatorVoteAccount + voteAccount = bondData.voteAccount } assert(bondAccount) let authority = validatorIdentity @@ -413,3 +441,71 @@ export async function executeCancelWithdrawRequestInstruction( throw e } } + +export async function executeInitSettlement({ + program, + provider, + config, + bondAccount, + voteAccount, + operatorAuthority, + currentEpoch, + merkleRoot = Buffer.from( + Array.from({ length: 32 }, () => Math.floor(Math.random() * 256)) + ), + rentCollector = Keypair.generate().publicKey, + maxMerkleNodes = Math.floor(Math.random() * 100) + 1, + maxTotalClaim = Math.floor(Math.random() * 100) + 1, +}: { + program: ValidatorBondsProgram + provider: ExtendedProvider + config: PublicKey + voteAccount?: PublicKey + bondAccount?: PublicKey + operatorAuthority: Keypair + currentEpoch?: number + rentCollector?: PublicKey + merkleRoot?: number[] | Uint8Array | Buffer + maxMerkleNodes?: number | BN + maxTotalClaim?: number | BN +}): Promise<{ + settlementAccount: PublicKey + epoch: BN + rentCollector: PublicKey + merkleRoot: number[] | Uint8Array | Buffer + maxMerkleNodes: number + maxTotalClaim: number +}> { + const { + instruction, + settlementAccount, + epoch: settlementEpoch, + } = await initSettlementInstruction({ + program, + configAccount: config, + operatorAuthority, + merkleRoot, + maxMerkleNodes, + maxTotalClaim, + voteAccount, + bondAccount, + currentEpoch, + rentCollector, + }) + await provider.sendIx([operatorAuthority], instruction) + expect( + provider.connection.getAccountInfo(settlementAccount) + ).resolves.not.toBeNull() + return { + settlementAccount, + epoch: settlementEpoch, + rentCollector, + merkleRoot, + maxMerkleNodes: new BN(maxMerkleNodes).toNumber(), + maxTotalClaim: new BN(maxTotalClaim).toNumber(), + } +} + +export const computeUnitIx = ComputeBudgetProgram.setComputeUnitLimit({ + units: 1_000_000, +}) diff --git a/packages/validator-bonds-sdk/generated/validator_bonds.ts b/packages/validator-bonds-sdk/generated/validator_bonds.ts index ea795eac..b011ea05 100644 --- a/packages/validator-bonds-sdk/generated/validator_bonds.ts +++ b/packages/validator-bonds-sdk/generated/validator_bonds.ts @@ -1,5 +1,5 @@ export type ValidatorBonds = { - "version": "0.1.0", + "version": "1.1.0", "name": "validator_bonds", "constants": [ { @@ -117,7 +117,7 @@ export type ValidatorBonds = { ] }, { - "name": "validatorVoteAccount", + "name": "voteAccount", "isMut": false, "isSigner": false }, @@ -151,7 +151,7 @@ export type ValidatorBonds = { { "kind": "account", "type": "publicKey", - "path": "validator_vote_account" + "path": "vote_account" } ] } @@ -202,12 +202,12 @@ export type ValidatorBonds = { { "kind": "account", "type": "publicKey", - "path": "validator_vote_account" + "path": "vote_account" } ] }, "relations": [ - "validator_vote_account" + "vote_account" ] }, { @@ -219,7 +219,7 @@ export type ValidatorBonds = { ] }, { - "name": "validatorVoteAccount", + "name": "voteAccount", "isMut": false, "isSigner": false } @@ -265,7 +265,7 @@ export type ValidatorBonds = { "kind": "account", "type": "publicKey", "account": "Bond", - "path": "bond.validator_vote_account" + "path": "bond.vote_account" } ] }, @@ -361,17 +361,17 @@ export type ValidatorBonds = { { "kind": "account", "type": "publicKey", - "path": "validator_vote_account" + "path": "vote_account" } ] }, "relations": [ "config", - "validator_vote_account" + "vote_account" ] }, { - "name": "validatorVoteAccount", + "name": "voteAccount", "isMut": false, "isSigner": false }, @@ -454,16 +454,16 @@ export type ValidatorBonds = { { "kind": "account", "type": "publicKey", - "path": "validator_vote_account" + "path": "vote_account" } ] }, "relations": [ - "validator_vote_account" + "vote_account" ] }, { - "name": "validatorVoteAccount", + "name": "voteAccount", "isMut": false, "isSigner": false }, @@ -537,17 +537,17 @@ export type ValidatorBonds = { { "kind": "account", "type": "publicKey", - "path": "validator_vote_account" + "path": "vote_account" } ] }, "relations": [ "config", - "validator_vote_account" + "vote_account" ] }, { - "name": "validatorVoteAccount", + "name": "voteAccount", "isMut": false, "isSigner": false }, @@ -579,7 +579,7 @@ export type ValidatorBonds = { ] }, "relations": [ - "validator_vote_account", + "vote_account", "bond" ] }, @@ -690,11 +690,10 @@ export type ValidatorBonds = { "path": "config" }, { - "kind": "arg", - "type": { - "defined": "InitSettlementArgs" - }, - "path": "params.vote_account" + "kind": "account", + "type": "publicKey", + "account": "Bond", + "path": "bond.vote_account" } ] }, @@ -772,7 +771,7 @@ export type ValidatorBonds = { "kind": "account", "type": "publicKey", "account": "Bond", - "path": "bond.validator_vote_account" + "path": "bond.vote_account" } ] }, @@ -824,16 +823,6 @@ export type ValidatorBonds = { "rent_collector" ] }, - { - "name": "rentCollector", - "isMut": true, - "isSigner": false - }, - { - "name": "splitRentCollector", - "isMut": true, - "isSigner": false - }, { "name": "bondsWithdrawerAuthority", "isMut": false, @@ -855,11 +844,23 @@ export type ValidatorBonds = { } }, { - "name": "stakeAccount", + "name": "rentCollector", + "isMut": true, + "isSigner": false + }, + { + "name": "splitRentCollector", + "isMut": true, + "isSigner": false + }, + { + "name": "splitRentRefundAccount", "isMut": true, "isSigner": false, "docs": [ - "a stake account to be used to return back the split rent exempt fee" + "a stake account that was funded to the settlement credited to bond's validator vote account", + "lamports of the stake accounts are used to pay back rent exempt of the split_stake_account", + "that can be created on funding the settlement" ] }, { @@ -912,7 +913,7 @@ export type ValidatorBonds = { "kind": "account", "type": "publicKey", "account": "Bond", - "path": "bond.validator_vote_account" + "path": "bond.vote_account" } ] }, @@ -957,8 +958,7 @@ export type ValidatorBonds = { ] }, "relations": [ - "bond", - "settlement_authority" + "bond" ] }, { @@ -1030,7 +1030,10 @@ export type ValidatorBonds = { "isMut": true, "isSigner": true, "docs": [ - "a split stake account is needed when the provided stake_account is bigger than the settlement" + "an account that does not exist, it will be initialized as a stake account (the signature needed)", + "the split_stake_account is needed when the provided stake_account is consists of more lamports", + "than the amount needed to fund the settlement, the left-over lamports from the stake account is split", + "into the new split_stake_account; when the split_stake_account is not needed, the rent payer is refunded" ] }, { @@ -1038,10 +1041,10 @@ export type ValidatorBonds = { "isMut": true, "isSigner": true, "docs": [ - "This is an account used to prefund the split stake account.", - "If a split stake account is not needed then rent payer is fully refunded at the end of the transaction.", - "If a split stake account is created for the settlement, the payer needs to manually close the claim_settlement", - "instruction to get the rent back (success only when the stake account is already deactivated)." + "rent exempt payer of the split_stake_account creation", + "if the split_stake_account is not needed (no left-over lamports on funding) then rent payer is refunded", + "it the split_stake_account is needed to spill out over funding of the settlement", + "then the rent payer is refunded when the settlement is closed" ] }, { @@ -1126,11 +1129,10 @@ export type ValidatorBonds = { "path": "config" }, { - "kind": "arg", - "type": { - "defined": "ClaimSettlementArgs" - }, - "path": "params.vote_account" + "kind": "account", + "type": "publicKey", + "account": "Bond", + "path": "bond.vote_account" } ] }, @@ -1184,50 +1186,7 @@ export type ValidatorBonds = { "isSigner": false, "docs": [ "deduplication, one amount cannot be claimed twice" - ], - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "claim_account" - }, - { - "kind": "account", - "type": "publicKey", - "account": "Settlement", - "path": "settlement" - }, - { - "kind": "arg", - "type": { - "defined": "ClaimSettlementArgs" - }, - "path": "params.staker" - }, - { - "kind": "arg", - "type": { - "defined": "ClaimSettlementArgs" - }, - "path": "params.withdrawer" - }, - { - "kind": "arg", - "type": { - "defined": "ClaimSettlementArgs" - }, - "path": "params.vote_account" - }, - { - "kind": "arg", - "type": { - "defined": "ClaimSettlementArgs" - }, - "path": "params.claim" - } - ] - } + ] }, { "name": "stakeAccount", @@ -1238,7 +1197,7 @@ export type ValidatorBonds = { ] }, { - "name": "withdrawerAuthority", + "name": "withdrawAuthority", "isMut": true, "isSigner": false, "docs": [ @@ -1392,19 +1351,22 @@ export type ValidatorBonds = { { "kind": "account", "type": "publicKey", - "account": "Bond", - "path": "bond.validator_vote_account" + "path": "vote_account" } ] }, "relations": [ - "config" + "config", + "vote_account" ] }, { "name": "settlement", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "settlment account used to derive settlement authority which cannot exists" + ] }, { "name": "stakeAccount", @@ -1414,17 +1376,13 @@ export type ValidatorBonds = { "stake account belonging to authority of the settlement" ] }, - { - "name": "settlementAuthority", - "isMut": false, - "isSigner": false - }, { "name": "bondsWithdrawerAuthority", "isMut": false, "isSigner": false, "docs": [ - "authority that owns (withdrawer authority) all stakes account under the bonds program" + "bonds withdrawer authority", + "to cancel settlement funding of the stake account changing staker authority to address" ], "pda": { "seeds": [ @@ -1443,7 +1401,7 @@ export type ValidatorBonds = { } }, { - "name": "validatorVoteAccount", + "name": "voteAccount", "isMut": false, "isSigner": false }, @@ -1469,6 +1427,30 @@ export type ValidatorBonds = { } ], "args": [] + }, + { + "name": "migrateBondCpmpe", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false, + "docs": [ + "config root account that will be configured" + ] + }, + { + "name": "bond", + "isMut": true, + "isSigner": false + }, + { + "name": "adminAuthority", + "isMut": false, + "isSigner": true + } + ], + "args": [] } ], "accounts": [ @@ -1489,12 +1471,12 @@ export type ValidatorBonds = { "type": "publicKey" }, { - "name": "validatorVoteAccount", + "name": "voteAccount", "docs": [ "Validator vote address that this bond account is crated for", "INVARIANTS:", "- one bond account per validator vote address", - "- bond program does not change received stake account delegation voter_pubkey to any other validator vote" + "- this program does NOT change stake account delegation voter_pubkey to any other validator vote account" ], "type": "publicKey" }, @@ -1507,13 +1489,11 @@ export type ValidatorBonds = { "type": "publicKey" }, { - "name": "revenueShare", + "name": "cpmpe", "docs": [ - "Revenue that is distributed from the bond (from validator) to the protocol" + "Cost per mille per epoch" ], - "type": { - "defined": "HundredthBasisPoint" - } + "type": "u64" }, { "name": "bump", @@ -1527,6 +1507,43 @@ export type ValidatorBonds = { "docs": [ "reserve space for future extensions" ], + "type": { + "array": [ + "u8", + 142 + ] + } + } + ] + } + }, + { + "name": "bondWithRevenueShare", + "type": { + "kind": "struct", + "fields": [ + { + "name": "config", + "type": "publicKey" + }, + { + "name": "voteAccount", + "type": "publicKey" + }, + { + "name": "authority", + "type": "publicKey" + }, + { + "name": "revenueShare", + "type": "u32" + }, + { + "name": "bump", + "type": "u8" + }, + { + "name": "reserved", "type": { "defined": "Reserved150" } @@ -1616,14 +1633,14 @@ export type ValidatorBonds = { "type": "publicKey" }, { - "name": "stakerAuthority", + "name": "stakeAuthority", "docs": [ - "staker authority as part of the merkle proof for this claim" + "stake authority as part of the merkle proof for this claim" ], "type": "publicKey" }, { - "name": "withdrawerAuthority", + "name": "withdrawAuthority", "docs": [ "withdrawer authority that has got permission to withdraw the claim" ], @@ -1637,7 +1654,7 @@ export type ValidatorBonds = { "type": "publicKey" }, { - "name": "claim", + "name": "amount", "docs": [ "claim amount" ], @@ -1686,9 +1703,10 @@ export type ValidatorBonds = { "type": "publicKey" }, { - "name": "settlementAuthority", + "name": "authority", "docs": [ - "stake account authority that manages the funded stake accounts" + "settlement authority used as the 'staker' stake account authority", + "of stake accounts funded to this settlement" ], "type": "publicKey" }, @@ -1712,28 +1730,28 @@ export type ValidatorBonds = { "type": "u64" }, { - "name": "maxNumNodes", + "name": "maxMerkleNodes", "docs": [ - "maximum number of nodes that can ever be claimed from this [Settlement]" + "maximum number of merkle tree nodes that can ever be claimed from this [Settlement]" ], "type": "u64" }, { - "name": "totalFunded", + "name": "lamportsFunded", "docs": [ - "total funds that have been deposited to this [Settlement]" + "total lamports funded to this [Settlement]" ], "type": "u64" }, { - "name": "totalFundsClaimed", + "name": "lamportsClaimed", "docs": [ - "total funds that have been claimed from this [Settlement]" + "total lamports that have been claimed from this [Settlement]" ], "type": "u64" }, { - "name": "numNodesClaimed", + "name": "merkleNodesClaimed", "docs": [ "number of nodes that have been claimed from this [Settlement]" ], @@ -1756,7 +1774,7 @@ export type ValidatorBonds = { { "name": "splitRentCollector", "docs": [ - "address that may claim the rent exempt for creation of \"split stake account\"" + "address claiming the rent exempt for \"split stake account\" created on funding settlement" ], "type": { "option": "publicKey" @@ -1796,16 +1814,16 @@ export type ValidatorBonds = { "kind": "struct", "fields": [ { - "name": "validatorVoteAccount", + "name": "voteAccount", "docs": [ - "Validator that requested the withdraw" + "Validator vote account that requested the withdraw" ], "type": "publicKey" }, { "name": "bond", "docs": [ - "Bond account that the withdraw request is for" + "Bond account that the withdraw request is for (has to match with vote_account)" ], "type": "publicKey" }, @@ -1883,26 +1901,6 @@ export type ValidatorBonds = { ] } }, - { - "name": "HundrethBasisPointChange", - "type": { - "kind": "struct", - "fields": [ - { - "name": "old", - "type": { - "defined": "HundredthBasisPoint" - } - }, - { - "name": "new", - "type": { - "defined": "HundredthBasisPoint" - } - } - ] - } - }, { "name": "DelegationInfo", "type": { @@ -1967,11 +1965,9 @@ export type ValidatorBonds = { } }, { - "name": "revenueShare", + "name": "cpmpe", "type": { - "option": { - "defined": "HundredthBasisPoint" - } + "option": "u64" } } ] @@ -1987,10 +1983,8 @@ export type ValidatorBonds = { "type": "publicKey" }, { - "name": "revenueShare", - "type": { - "defined": "HundredthBasisPoint" - } + "name": "cpmpe", + "type": "u64" } ] } @@ -2062,10 +2056,6 @@ export type ValidatorBonds = { "type": { "kind": "struct", "fields": [ - { - "name": "amount", - "type": "u64" - }, { "name": "proof", "type": { @@ -2078,22 +2068,10 @@ export type ValidatorBonds = { } }, { - "name": "staker", - "type": "publicKey" - }, - { - "name": "withdrawer", + "name": "claim", "docs": [ - "claim holder, withdrawer_authority" + "claim amount; merkle root verification" ], - "type": "publicKey" - }, - { - "name": "voteAccount", - "type": "publicKey" - }, - { - "name": "claim", "type": "u64" } ] @@ -2106,6 +2084,10 @@ export type ValidatorBonds = { "fields": [ { "name": "merkleRoot", + "docs": [ + "merkle root for this settlement, multiple settlements can be created with the same merkle root,", + "settlements will be distinguished by the vote_account" + ], "type": { "array": [ "u8", @@ -2114,19 +2096,24 @@ export type ValidatorBonds = { } }, { - "name": "voteAccount", - "type": "publicKey" - }, - { - "name": "settlementTotalClaim", + "name": "maxTotalClaim", + "docs": [ + "maximal number of lamports that can be claimed from this settlement" + ], "type": "u64" }, { - "name": "settlementNumNodes", + "name": "maxMerkleNodes", + "docs": [ + "maximal number of merkle tree nodes that can be claimed from this settlement" + ], "type": "u64" }, { "name": "rentCollector", + "docs": [ + "collects the rent exempt from the settlement account when closed" + ], "type": "publicKey" } ] @@ -2188,23 +2175,6 @@ export type ValidatorBonds = { } ] } - }, - { - "name": "HundredthBasisPoint", - "docs": [ - "It's a smaller unit of a basis point (basis point = 1/100%), we calculate 1/10_000% here instead.", - "The max value is 1_000_000 (100%).", - "1 HundredthBasisPoint = 0.0001%, 10_000 HundredthBasisPoint = 1%, 1_000_000 HundredthBasisPoint = 100%." - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "hundredthBps", - "type": "u32" - } - ] - } } ], "events": [ @@ -2217,7 +2187,7 @@ export type ValidatorBonds = { "index": false }, { - "name": "validatorVoteAccount", + "name": "voteAccount", "type": "publicKey", "index": false }, @@ -2232,10 +2202,8 @@ export type ValidatorBonds = { "index": false }, { - "name": "revenueShare", - "type": { - "defined": "HundredthBasisPoint" - }, + "name": "cpmpe", + "type": "u64", "index": false }, { @@ -2258,10 +2226,10 @@ export type ValidatorBonds = { "index": false }, { - "name": "revenueShare", + "name": "cpmpe", "type": { "option": { - "defined": "HundrethBasisPointChange" + "defined": "U64ValueChange" } }, "index": false @@ -2277,7 +2245,7 @@ export type ValidatorBonds = { "index": false }, { - "name": "validatorVoteAccount", + "name": "voteAccount", "type": "publicKey", "index": false }, @@ -2287,10 +2255,8 @@ export type ValidatorBonds = { "index": false }, { - "name": "revenueShare", - "type": { - "defined": "HundredthBasisPoint" - }, + "name": "cpmpe", + "type": "u64", "index": false }, { @@ -2309,7 +2275,7 @@ export type ValidatorBonds = { "index": false }, { - "name": "validatorVote", + "name": "voteAccount", "type": "publicKey", "index": false }, @@ -2434,12 +2400,17 @@ export type ValidatorBonds = { "index": false }, { - "name": "stakerAuthority", - "type": "publicKey", + "name": "settlementLamportsClaimed", + "type": "u64", + "index": false + }, + { + "name": "settlementMerkleNodesClaimed", + "type": "u64", "index": false }, { - "name": "withdrawerAuthority", + "name": "withdrawAuthority", "type": "publicKey", "index": false }, @@ -2449,7 +2420,7 @@ export type ValidatorBonds = { "index": false }, { - "name": "claim", + "name": "amount", "type": "u64", "index": false }, @@ -2494,7 +2465,7 @@ export type ValidatorBonds = { "index": false }, { - "name": "settlementAuthority", + "name": "authority", "type": "publicKey", "index": false }, @@ -2514,12 +2485,12 @@ export type ValidatorBonds = { "index": false }, { - "name": "maxNumNodes", + "name": "maxMerkleNodes", "type": "u64", "index": false }, { - "name": "epoch", + "name": "epochCreatedAt", "type": "u64", "index": false }, @@ -2566,22 +2537,22 @@ export type ValidatorBonds = { "index": false }, { - "name": "maxNumNodes", + "name": "maxMerkleNodes", "type": "u64", "index": false }, { - "name": "totalFunded", + "name": "lamportsFunded", "type": "u64", "index": false }, { - "name": "totalFundsClaimed", + "name": "lamportsClaimed", "type": "u64", "index": false }, { - "name": "numNodesClaimed", + "name": "merkleNodesClaimed", "type": "u64", "index": false }, @@ -2592,6 +2563,11 @@ export type ValidatorBonds = { }, "index": false }, + { + "name": "splitRentRefundAccount", + "type": "publicKey", + "index": false + }, { "name": "rentCollector", "type": "publicKey", @@ -2628,17 +2604,17 @@ export type ValidatorBonds = { "index": false }, { - "name": "totalFunded", + "name": "lamportsFunded", "type": "u64", "index": false }, { - "name": "totalFundsClaimed", + "name": "lamportsClaimed", "type": "u64", "index": false }, { - "name": "numNodesClaimed", + "name": "merkleNodesClaimed", "type": "u64", "index": false }, @@ -2742,7 +2718,7 @@ export type ValidatorBonds = { "index": false }, { - "name": "validatorVoteAcount", + "name": "voteAccount", "type": "publicKey", "index": false }, @@ -2750,11 +2726,6 @@ export type ValidatorBonds = { "name": "settlementAuthority", "type": "publicKey", "index": false - }, - { - "name": "bondsWithdrawerAuthority", - "type": "publicKey", - "index": false } ] }, @@ -2772,7 +2743,7 @@ export type ValidatorBonds = { "index": false }, { - "name": "validatorVoteAccount", + "name": "voteAccount", "type": "publicKey", "index": false }, @@ -2837,7 +2808,7 @@ export type ValidatorBonds = { "index": false }, { - "name": "validatorVoteAccount", + "name": "voteAccount", "type": "publicKey", "index": false }, @@ -2903,229 +2874,239 @@ export type ValidatorBonds = { }, { "code": 6005, + "name": "InvalidStakeAccountState", + "msg": "Provided vote account is trouble to deserialize" + }, + { + "code": 6006, + "name": "InvalidStakeAccountProgramId", + "msg": "Provided stake account is not owned by the stake account program" + }, + { + "code": 6007, "name": "InvalidSettlementAddress", "msg": "Fail to create account address for Settlement" }, { - "code": 6006, + "code": 6008, "name": "InvalidSettlementAuthorityAddress", "msg": "Fail to create PDA address for Settlement Authority" }, { - "code": 6007, + "code": 6009, "name": "InvalidBondsWithdrawerAuthorityAddress", "msg": "Fail to create PDA address for Bonds Withdrawer Authority" }, { - "code": 6008, + "code": 6010, "name": "InvalidSettlementClaimAddress", "msg": "Fail to create program address for SettlementClaim" }, { - "code": 6009, + "code": 6011, "name": "InvalidBondAddress", "msg": "Fail to create program address for Bond" }, { - "code": 6010, + "code": 6012, "name": "WrongStakeAccountWithdrawer", "msg": "Wrong withdrawer authority of the stake account" }, { - "code": 6011, + "code": 6013, "name": "InvalidWithdrawRequestAddress", "msg": "Fail to create program address for WithdrawRequest" }, { - "code": 6012, + "code": 6014, "name": "HundredthBasisPointsOverflow", "msg": "Value of hundredth basis points is too big" }, { - "code": 6013, + "code": 6015, "name": "HundredthBasisPointsCalculation", "msg": "Hundredth basis points calculation failure" }, { - "code": 6014, + "code": 6016, "name": "HundredthBasisPointsParse", "msg": "Hundredth basis points failure to parse the value" }, { - "code": 6015, + "code": 6017, "name": "FailedToDeserializeVoteAccount", "msg": "Cannot deserialize validator vote account data" }, { - "code": 6016, + "code": 6018, "name": "BondChangeNotPermitted", "msg": "Wrong authority for changing the validator bond account" }, { - "code": 6017, + "code": 6019, "name": "StakeNotDelegated", "msg": "Provided stake cannot be used for bonds, it's not delegated" }, { - "code": 6018, + "code": 6020, "name": "BondStakeWrongDelegation", "msg": "Provided stake is delegated to a wrong validator vote account" }, { - "code": 6019, + "code": 6021, "name": "WithdrawRequestNotReady", "msg": "Withdraw request has not elapsed the epoch lockup period yet" }, { - "code": 6020, + "code": 6022, "name": "SettlementNotExpired", "msg": "Settlement has not expired yet" }, { - "code": 6021, + "code": 6023, "name": "SettlementExpired", "msg": "Settlement has already expired" }, { - "code": 6022, + "code": 6024, "name": "UninitializedStake", "msg": "Stake is not initialized" }, { - "code": 6023, + "code": 6025, "name": "NoStakeOrNotFullyActivated", "msg": "Stake account is not fully activated" }, { - "code": 6024, + "code": 6026, "name": "UnexpectedRemainingAccounts", "msg": "Instruction context was provided with unexpected set of remaining accounts" }, { - "code": 6025, + "code": 6027, "name": "SettlementNotClosed", - "msg": "Closing SettlementClaim requires the settlement being closed" + "msg": "Required settlement to be closed" }, { - "code": 6026, + "code": 6028, "name": "StakeAccountIsFundedToSettlement", "msg": "Provided stake account has been already funded to a settlement" }, { - "code": 6027, + "code": 6029, "name": "ClaimSettlementProofFailed", "msg": "Settlement claim proof failed" }, { - "code": 6028, + "code": 6030, "name": "StakeLockedUp", "msg": "Provided stake account is locked-up" }, { - "code": 6029, + "code": 6031, "name": "StakeAccountNotBigEnoughToSplit", "msg": "Stake account is not big enough to be split" }, { - "code": 6030, + "code": 6032, "name": "ClaimAmountExceedsMaxTotalClaim", "msg": "Claiming bigger amount than the max total claim" }, { - "code": 6031, - "name": "ClaimCountExceedsMaxNumNodes", + "code": 6033, + "name": "ClaimCountExceedsMaxMerkleNodes", "msg": "Claim exceeded number of claimable nodes in the merkle tree" }, { - "code": 6032, + "code": 6034, "name": "EmptySettlementMerkleTree", "msg": "Empty merkle tree, nothing to be claimed" }, { - "code": 6033, + "code": 6035, "name": "ClaimingStakeAccountLamportsInsufficient", "msg": "Provided stake account has not enough lamports to cover the claim" }, { - "code": 6034, - "name": "StakeAccountNotFunded", - "msg": "Provided stake account is not funded under a settlement" + "code": 6036, + "name": "StakeAccountNotFundedToSettlement", + "msg": "Provided stake account is not funded under the settlement" }, { - "code": 6035, + "code": 6037, "name": "VoteAccountValidatorIdentityMismatch", "msg": "Validator vote account does not match to provided validator identity signature" }, { - "code": 6036, + "code": 6038, "name": "VoteAccountMismatch", "msg": "Bond vote account address does not match with the provided validator vote account" }, { - "code": 6037, + "code": 6039, "name": "ConfigAccountMismatch", "msg": "Bond config address does not match with the provided config account" }, { - "code": 6038, + "code": 6040, "name": "WithdrawRequestVoteAccountMismatch", "msg": "Withdraw request vote account address does not match with the provided validator vote account" }, { - "code": 6039, + "code": 6041, "name": "BondAccountMismatch", "msg": "Bond account address does not match with the stored one" }, { - "code": 6040, + "code": 6042, "name": "SettlementAccountMismatch", "msg": "Settlement account address does not match with the stored one" }, { - "code": 6041, + "code": 6043, "name": "RentCollectorMismatch", "msg": "Rent collector address does not match permitted rent collector" }, { - "code": 6042, + "code": 6044, "name": "StakerAuthorityMismatch", "msg": "Stake account's staker does not match with the provided authority" }, { - "code": 6043, + "code": 6045, "name": "NonBondStakeAuthorities", "msg": "One or both stake authorities does not belong to bonds program" }, { - "code": 6044, + "code": 6046, "name": "SettlementAuthorityMismatch", - "msg": "Settlement stake account authority does not match with the provided stake account authority" + "msg": "Stake account staker authority mismatches with the settlement authority" }, { - "code": 6045, + "code": 6047, "name": "StakeDelegationMismatch", "msg": "Delegation of provided stake account mismatches" }, { - "code": 6046, + "code": 6048, "name": "WithdrawRequestAmountTooSmall", "msg": "Too small non-withdrawn withdraw request amount, cancel and init new one" }, { - "code": 6047, + "code": 6049, "name": "WithdrawRequestAlreadyFulfilled", "msg": "Withdraw request has been already fulfilled" }, { - "code": 6048, - "name": "NotYetImplemented", - "msg": "Not yet implemented" + "code": 6050, + "name": "ClaimSettlementMerkleTreeNodeMismatch", + "msg": "Claim settlement merkle tree node mismatch" } ] }; export const IDL: ValidatorBonds = { - "version": "0.1.0", + "version": "1.1.0", "name": "validator_bonds", "constants": [ { @@ -3243,7 +3224,7 @@ export const IDL: ValidatorBonds = { ] }, { - "name": "validatorVoteAccount", + "name": "voteAccount", "isMut": false, "isSigner": false }, @@ -3277,7 +3258,7 @@ export const IDL: ValidatorBonds = { { "kind": "account", "type": "publicKey", - "path": "validator_vote_account" + "path": "vote_account" } ] } @@ -3328,12 +3309,12 @@ export const IDL: ValidatorBonds = { { "kind": "account", "type": "publicKey", - "path": "validator_vote_account" + "path": "vote_account" } ] }, "relations": [ - "validator_vote_account" + "vote_account" ] }, { @@ -3345,7 +3326,7 @@ export const IDL: ValidatorBonds = { ] }, { - "name": "validatorVoteAccount", + "name": "voteAccount", "isMut": false, "isSigner": false } @@ -3391,7 +3372,7 @@ export const IDL: ValidatorBonds = { "kind": "account", "type": "publicKey", "account": "Bond", - "path": "bond.validator_vote_account" + "path": "bond.vote_account" } ] }, @@ -3487,17 +3468,17 @@ export const IDL: ValidatorBonds = { { "kind": "account", "type": "publicKey", - "path": "validator_vote_account" + "path": "vote_account" } ] }, "relations": [ "config", - "validator_vote_account" + "vote_account" ] }, { - "name": "validatorVoteAccount", + "name": "voteAccount", "isMut": false, "isSigner": false }, @@ -3580,16 +3561,16 @@ export const IDL: ValidatorBonds = { { "kind": "account", "type": "publicKey", - "path": "validator_vote_account" + "path": "vote_account" } ] }, "relations": [ - "validator_vote_account" + "vote_account" ] }, { - "name": "validatorVoteAccount", + "name": "voteAccount", "isMut": false, "isSigner": false }, @@ -3663,17 +3644,17 @@ export const IDL: ValidatorBonds = { { "kind": "account", "type": "publicKey", - "path": "validator_vote_account" + "path": "vote_account" } ] }, "relations": [ "config", - "validator_vote_account" + "vote_account" ] }, { - "name": "validatorVoteAccount", + "name": "voteAccount", "isMut": false, "isSigner": false }, @@ -3705,7 +3686,7 @@ export const IDL: ValidatorBonds = { ] }, "relations": [ - "validator_vote_account", + "vote_account", "bond" ] }, @@ -3816,11 +3797,10 @@ export const IDL: ValidatorBonds = { "path": "config" }, { - "kind": "arg", - "type": { - "defined": "InitSettlementArgs" - }, - "path": "params.vote_account" + "kind": "account", + "type": "publicKey", + "account": "Bond", + "path": "bond.vote_account" } ] }, @@ -3898,7 +3878,7 @@ export const IDL: ValidatorBonds = { "kind": "account", "type": "publicKey", "account": "Bond", - "path": "bond.validator_vote_account" + "path": "bond.vote_account" } ] }, @@ -3950,16 +3930,6 @@ export const IDL: ValidatorBonds = { "rent_collector" ] }, - { - "name": "rentCollector", - "isMut": true, - "isSigner": false - }, - { - "name": "splitRentCollector", - "isMut": true, - "isSigner": false - }, { "name": "bondsWithdrawerAuthority", "isMut": false, @@ -3981,11 +3951,23 @@ export const IDL: ValidatorBonds = { } }, { - "name": "stakeAccount", + "name": "rentCollector", + "isMut": true, + "isSigner": false + }, + { + "name": "splitRentCollector", + "isMut": true, + "isSigner": false + }, + { + "name": "splitRentRefundAccount", "isMut": true, "isSigner": false, "docs": [ - "a stake account to be used to return back the split rent exempt fee" + "a stake account that was funded to the settlement credited to bond's validator vote account", + "lamports of the stake accounts are used to pay back rent exempt of the split_stake_account", + "that can be created on funding the settlement" ] }, { @@ -4038,7 +4020,7 @@ export const IDL: ValidatorBonds = { "kind": "account", "type": "publicKey", "account": "Bond", - "path": "bond.validator_vote_account" + "path": "bond.vote_account" } ] }, @@ -4083,8 +4065,7 @@ export const IDL: ValidatorBonds = { ] }, "relations": [ - "bond", - "settlement_authority" + "bond" ] }, { @@ -4156,7 +4137,10 @@ export const IDL: ValidatorBonds = { "isMut": true, "isSigner": true, "docs": [ - "a split stake account is needed when the provided stake_account is bigger than the settlement" + "an account that does not exist, it will be initialized as a stake account (the signature needed)", + "the split_stake_account is needed when the provided stake_account is consists of more lamports", + "than the amount needed to fund the settlement, the left-over lamports from the stake account is split", + "into the new split_stake_account; when the split_stake_account is not needed, the rent payer is refunded" ] }, { @@ -4164,10 +4148,10 @@ export const IDL: ValidatorBonds = { "isMut": true, "isSigner": true, "docs": [ - "This is an account used to prefund the split stake account.", - "If a split stake account is not needed then rent payer is fully refunded at the end of the transaction.", - "If a split stake account is created for the settlement, the payer needs to manually close the claim_settlement", - "instruction to get the rent back (success only when the stake account is already deactivated)." + "rent exempt payer of the split_stake_account creation", + "if the split_stake_account is not needed (no left-over lamports on funding) then rent payer is refunded", + "it the split_stake_account is needed to spill out over funding of the settlement", + "then the rent payer is refunded when the settlement is closed" ] }, { @@ -4252,11 +4236,10 @@ export const IDL: ValidatorBonds = { "path": "config" }, { - "kind": "arg", - "type": { - "defined": "ClaimSettlementArgs" - }, - "path": "params.vote_account" + "kind": "account", + "type": "publicKey", + "account": "Bond", + "path": "bond.vote_account" } ] }, @@ -4310,50 +4293,7 @@ export const IDL: ValidatorBonds = { "isSigner": false, "docs": [ "deduplication, one amount cannot be claimed twice" - ], - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "claim_account" - }, - { - "kind": "account", - "type": "publicKey", - "account": "Settlement", - "path": "settlement" - }, - { - "kind": "arg", - "type": { - "defined": "ClaimSettlementArgs" - }, - "path": "params.staker" - }, - { - "kind": "arg", - "type": { - "defined": "ClaimSettlementArgs" - }, - "path": "params.withdrawer" - }, - { - "kind": "arg", - "type": { - "defined": "ClaimSettlementArgs" - }, - "path": "params.vote_account" - }, - { - "kind": "arg", - "type": { - "defined": "ClaimSettlementArgs" - }, - "path": "params.claim" - } - ] - } + ] }, { "name": "stakeAccount", @@ -4364,7 +4304,7 @@ export const IDL: ValidatorBonds = { ] }, { - "name": "withdrawerAuthority", + "name": "withdrawAuthority", "isMut": true, "isSigner": false, "docs": [ @@ -4518,19 +4458,22 @@ export const IDL: ValidatorBonds = { { "kind": "account", "type": "publicKey", - "account": "Bond", - "path": "bond.validator_vote_account" + "path": "vote_account" } ] }, "relations": [ - "config" + "config", + "vote_account" ] }, { "name": "settlement", "isMut": false, - "isSigner": false + "isSigner": false, + "docs": [ + "settlment account used to derive settlement authority which cannot exists" + ] }, { "name": "stakeAccount", @@ -4540,17 +4483,13 @@ export const IDL: ValidatorBonds = { "stake account belonging to authority of the settlement" ] }, - { - "name": "settlementAuthority", - "isMut": false, - "isSigner": false - }, { "name": "bondsWithdrawerAuthority", "isMut": false, "isSigner": false, "docs": [ - "authority that owns (withdrawer authority) all stakes account under the bonds program" + "bonds withdrawer authority", + "to cancel settlement funding of the stake account changing staker authority to address" ], "pda": { "seeds": [ @@ -4569,7 +4508,7 @@ export const IDL: ValidatorBonds = { } }, { - "name": "validatorVoteAccount", + "name": "voteAccount", "isMut": false, "isSigner": false }, @@ -4595,6 +4534,30 @@ export const IDL: ValidatorBonds = { } ], "args": [] + }, + { + "name": "migrateBondCpmpe", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false, + "docs": [ + "config root account that will be configured" + ] + }, + { + "name": "bond", + "isMut": true, + "isSigner": false + }, + { + "name": "adminAuthority", + "isMut": false, + "isSigner": true + } + ], + "args": [] } ], "accounts": [ @@ -4615,12 +4578,12 @@ export const IDL: ValidatorBonds = { "type": "publicKey" }, { - "name": "validatorVoteAccount", + "name": "voteAccount", "docs": [ "Validator vote address that this bond account is crated for", "INVARIANTS:", "- one bond account per validator vote address", - "- bond program does not change received stake account delegation voter_pubkey to any other validator vote" + "- this program does NOT change stake account delegation voter_pubkey to any other validator vote account" ], "type": "publicKey" }, @@ -4633,13 +4596,11 @@ export const IDL: ValidatorBonds = { "type": "publicKey" }, { - "name": "revenueShare", + "name": "cpmpe", "docs": [ - "Revenue that is distributed from the bond (from validator) to the protocol" + "Cost per mille per epoch" ], - "type": { - "defined": "HundredthBasisPoint" - } + "type": "u64" }, { "name": "bump", @@ -4653,6 +4614,43 @@ export const IDL: ValidatorBonds = { "docs": [ "reserve space for future extensions" ], + "type": { + "array": [ + "u8", + 142 + ] + } + } + ] + } + }, + { + "name": "bondWithRevenueShare", + "type": { + "kind": "struct", + "fields": [ + { + "name": "config", + "type": "publicKey" + }, + { + "name": "voteAccount", + "type": "publicKey" + }, + { + "name": "authority", + "type": "publicKey" + }, + { + "name": "revenueShare", + "type": "u32" + }, + { + "name": "bump", + "type": "u8" + }, + { + "name": "reserved", "type": { "defined": "Reserved150" } @@ -4742,14 +4740,14 @@ export const IDL: ValidatorBonds = { "type": "publicKey" }, { - "name": "stakerAuthority", + "name": "stakeAuthority", "docs": [ - "staker authority as part of the merkle proof for this claim" + "stake authority as part of the merkle proof for this claim" ], "type": "publicKey" }, { - "name": "withdrawerAuthority", + "name": "withdrawAuthority", "docs": [ "withdrawer authority that has got permission to withdraw the claim" ], @@ -4763,7 +4761,7 @@ export const IDL: ValidatorBonds = { "type": "publicKey" }, { - "name": "claim", + "name": "amount", "docs": [ "claim amount" ], @@ -4812,9 +4810,10 @@ export const IDL: ValidatorBonds = { "type": "publicKey" }, { - "name": "settlementAuthority", + "name": "authority", "docs": [ - "stake account authority that manages the funded stake accounts" + "settlement authority used as the 'staker' stake account authority", + "of stake accounts funded to this settlement" ], "type": "publicKey" }, @@ -4838,28 +4837,28 @@ export const IDL: ValidatorBonds = { "type": "u64" }, { - "name": "maxNumNodes", + "name": "maxMerkleNodes", "docs": [ - "maximum number of nodes that can ever be claimed from this [Settlement]" + "maximum number of merkle tree nodes that can ever be claimed from this [Settlement]" ], "type": "u64" }, { - "name": "totalFunded", + "name": "lamportsFunded", "docs": [ - "total funds that have been deposited to this [Settlement]" + "total lamports funded to this [Settlement]" ], "type": "u64" }, { - "name": "totalFundsClaimed", + "name": "lamportsClaimed", "docs": [ - "total funds that have been claimed from this [Settlement]" + "total lamports that have been claimed from this [Settlement]" ], "type": "u64" }, { - "name": "numNodesClaimed", + "name": "merkleNodesClaimed", "docs": [ "number of nodes that have been claimed from this [Settlement]" ], @@ -4882,7 +4881,7 @@ export const IDL: ValidatorBonds = { { "name": "splitRentCollector", "docs": [ - "address that may claim the rent exempt for creation of \"split stake account\"" + "address claiming the rent exempt for \"split stake account\" created on funding settlement" ], "type": { "option": "publicKey" @@ -4922,16 +4921,16 @@ export const IDL: ValidatorBonds = { "kind": "struct", "fields": [ { - "name": "validatorVoteAccount", + "name": "voteAccount", "docs": [ - "Validator that requested the withdraw" + "Validator vote account that requested the withdraw" ], "type": "publicKey" }, { "name": "bond", "docs": [ - "Bond account that the withdraw request is for" + "Bond account that the withdraw request is for (has to match with vote_account)" ], "type": "publicKey" }, @@ -5009,26 +5008,6 @@ export const IDL: ValidatorBonds = { ] } }, - { - "name": "HundrethBasisPointChange", - "type": { - "kind": "struct", - "fields": [ - { - "name": "old", - "type": { - "defined": "HundredthBasisPoint" - } - }, - { - "name": "new", - "type": { - "defined": "HundredthBasisPoint" - } - } - ] - } - }, { "name": "DelegationInfo", "type": { @@ -5093,11 +5072,9 @@ export const IDL: ValidatorBonds = { } }, { - "name": "revenueShare", + "name": "cpmpe", "type": { - "option": { - "defined": "HundredthBasisPoint" - } + "option": "u64" } } ] @@ -5113,10 +5090,8 @@ export const IDL: ValidatorBonds = { "type": "publicKey" }, { - "name": "revenueShare", - "type": { - "defined": "HundredthBasisPoint" - } + "name": "cpmpe", + "type": "u64" } ] } @@ -5188,10 +5163,6 @@ export const IDL: ValidatorBonds = { "type": { "kind": "struct", "fields": [ - { - "name": "amount", - "type": "u64" - }, { "name": "proof", "type": { @@ -5204,22 +5175,10 @@ export const IDL: ValidatorBonds = { } }, { - "name": "staker", - "type": "publicKey" - }, - { - "name": "withdrawer", + "name": "claim", "docs": [ - "claim holder, withdrawer_authority" + "claim amount; merkle root verification" ], - "type": "publicKey" - }, - { - "name": "voteAccount", - "type": "publicKey" - }, - { - "name": "claim", "type": "u64" } ] @@ -5232,6 +5191,10 @@ export const IDL: ValidatorBonds = { "fields": [ { "name": "merkleRoot", + "docs": [ + "merkle root for this settlement, multiple settlements can be created with the same merkle root,", + "settlements will be distinguished by the vote_account" + ], "type": { "array": [ "u8", @@ -5240,19 +5203,24 @@ export const IDL: ValidatorBonds = { } }, { - "name": "voteAccount", - "type": "publicKey" - }, - { - "name": "settlementTotalClaim", + "name": "maxTotalClaim", + "docs": [ + "maximal number of lamports that can be claimed from this settlement" + ], "type": "u64" }, { - "name": "settlementNumNodes", + "name": "maxMerkleNodes", + "docs": [ + "maximal number of merkle tree nodes that can be claimed from this settlement" + ], "type": "u64" }, { "name": "rentCollector", + "docs": [ + "collects the rent exempt from the settlement account when closed" + ], "type": "publicKey" } ] @@ -5314,23 +5282,6 @@ export const IDL: ValidatorBonds = { } ] } - }, - { - "name": "HundredthBasisPoint", - "docs": [ - "It's a smaller unit of a basis point (basis point = 1/100%), we calculate 1/10_000% here instead.", - "The max value is 1_000_000 (100%).", - "1 HundredthBasisPoint = 0.0001%, 10_000 HundredthBasisPoint = 1%, 1_000_000 HundredthBasisPoint = 100%." - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "hundredthBps", - "type": "u32" - } - ] - } } ], "events": [ @@ -5343,7 +5294,7 @@ export const IDL: ValidatorBonds = { "index": false }, { - "name": "validatorVoteAccount", + "name": "voteAccount", "type": "publicKey", "index": false }, @@ -5358,10 +5309,8 @@ export const IDL: ValidatorBonds = { "index": false }, { - "name": "revenueShare", - "type": { - "defined": "HundredthBasisPoint" - }, + "name": "cpmpe", + "type": "u64", "index": false }, { @@ -5384,10 +5333,10 @@ export const IDL: ValidatorBonds = { "index": false }, { - "name": "revenueShare", + "name": "cpmpe", "type": { "option": { - "defined": "HundrethBasisPointChange" + "defined": "U64ValueChange" } }, "index": false @@ -5403,7 +5352,7 @@ export const IDL: ValidatorBonds = { "index": false }, { - "name": "validatorVoteAccount", + "name": "voteAccount", "type": "publicKey", "index": false }, @@ -5413,10 +5362,8 @@ export const IDL: ValidatorBonds = { "index": false }, { - "name": "revenueShare", - "type": { - "defined": "HundredthBasisPoint" - }, + "name": "cpmpe", + "type": "u64", "index": false }, { @@ -5435,7 +5382,7 @@ export const IDL: ValidatorBonds = { "index": false }, { - "name": "validatorVote", + "name": "voteAccount", "type": "publicKey", "index": false }, @@ -5560,12 +5507,17 @@ export const IDL: ValidatorBonds = { "index": false }, { - "name": "stakerAuthority", - "type": "publicKey", + "name": "settlementLamportsClaimed", + "type": "u64", + "index": false + }, + { + "name": "settlementMerkleNodesClaimed", + "type": "u64", "index": false }, { - "name": "withdrawerAuthority", + "name": "withdrawAuthority", "type": "publicKey", "index": false }, @@ -5575,7 +5527,7 @@ export const IDL: ValidatorBonds = { "index": false }, { - "name": "claim", + "name": "amount", "type": "u64", "index": false }, @@ -5620,7 +5572,7 @@ export const IDL: ValidatorBonds = { "index": false }, { - "name": "settlementAuthority", + "name": "authority", "type": "publicKey", "index": false }, @@ -5640,12 +5592,12 @@ export const IDL: ValidatorBonds = { "index": false }, { - "name": "maxNumNodes", + "name": "maxMerkleNodes", "type": "u64", "index": false }, { - "name": "epoch", + "name": "epochCreatedAt", "type": "u64", "index": false }, @@ -5692,22 +5644,22 @@ export const IDL: ValidatorBonds = { "index": false }, { - "name": "maxNumNodes", + "name": "maxMerkleNodes", "type": "u64", "index": false }, { - "name": "totalFunded", + "name": "lamportsFunded", "type": "u64", "index": false }, { - "name": "totalFundsClaimed", + "name": "lamportsClaimed", "type": "u64", "index": false }, { - "name": "numNodesClaimed", + "name": "merkleNodesClaimed", "type": "u64", "index": false }, @@ -5718,6 +5670,11 @@ export const IDL: ValidatorBonds = { }, "index": false }, + { + "name": "splitRentRefundAccount", + "type": "publicKey", + "index": false + }, { "name": "rentCollector", "type": "publicKey", @@ -5754,17 +5711,17 @@ export const IDL: ValidatorBonds = { "index": false }, { - "name": "totalFunded", + "name": "lamportsFunded", "type": "u64", "index": false }, { - "name": "totalFundsClaimed", + "name": "lamportsClaimed", "type": "u64", "index": false }, { - "name": "numNodesClaimed", + "name": "merkleNodesClaimed", "type": "u64", "index": false }, @@ -5868,7 +5825,7 @@ export const IDL: ValidatorBonds = { "index": false }, { - "name": "validatorVoteAcount", + "name": "voteAccount", "type": "publicKey", "index": false }, @@ -5876,11 +5833,6 @@ export const IDL: ValidatorBonds = { "name": "settlementAuthority", "type": "publicKey", "index": false - }, - { - "name": "bondsWithdrawerAuthority", - "type": "publicKey", - "index": false } ] }, @@ -5898,7 +5850,7 @@ export const IDL: ValidatorBonds = { "index": false }, { - "name": "validatorVoteAccount", + "name": "voteAccount", "type": "publicKey", "index": false }, @@ -5963,7 +5915,7 @@ export const IDL: ValidatorBonds = { "index": false }, { - "name": "validatorVoteAccount", + "name": "voteAccount", "type": "publicKey", "index": false }, @@ -6029,223 +5981,233 @@ export const IDL: ValidatorBonds = { }, { "code": 6005, + "name": "InvalidStakeAccountState", + "msg": "Provided vote account is trouble to deserialize" + }, + { + "code": 6006, + "name": "InvalidStakeAccountProgramId", + "msg": "Provided stake account is not owned by the stake account program" + }, + { + "code": 6007, "name": "InvalidSettlementAddress", "msg": "Fail to create account address for Settlement" }, { - "code": 6006, + "code": 6008, "name": "InvalidSettlementAuthorityAddress", "msg": "Fail to create PDA address for Settlement Authority" }, { - "code": 6007, + "code": 6009, "name": "InvalidBondsWithdrawerAuthorityAddress", "msg": "Fail to create PDA address for Bonds Withdrawer Authority" }, { - "code": 6008, + "code": 6010, "name": "InvalidSettlementClaimAddress", "msg": "Fail to create program address for SettlementClaim" }, { - "code": 6009, + "code": 6011, "name": "InvalidBondAddress", "msg": "Fail to create program address for Bond" }, { - "code": 6010, + "code": 6012, "name": "WrongStakeAccountWithdrawer", "msg": "Wrong withdrawer authority of the stake account" }, { - "code": 6011, + "code": 6013, "name": "InvalidWithdrawRequestAddress", "msg": "Fail to create program address for WithdrawRequest" }, { - "code": 6012, + "code": 6014, "name": "HundredthBasisPointsOverflow", "msg": "Value of hundredth basis points is too big" }, { - "code": 6013, + "code": 6015, "name": "HundredthBasisPointsCalculation", "msg": "Hundredth basis points calculation failure" }, { - "code": 6014, + "code": 6016, "name": "HundredthBasisPointsParse", "msg": "Hundredth basis points failure to parse the value" }, { - "code": 6015, + "code": 6017, "name": "FailedToDeserializeVoteAccount", "msg": "Cannot deserialize validator vote account data" }, { - "code": 6016, + "code": 6018, "name": "BondChangeNotPermitted", "msg": "Wrong authority for changing the validator bond account" }, { - "code": 6017, + "code": 6019, "name": "StakeNotDelegated", "msg": "Provided stake cannot be used for bonds, it's not delegated" }, { - "code": 6018, + "code": 6020, "name": "BondStakeWrongDelegation", "msg": "Provided stake is delegated to a wrong validator vote account" }, { - "code": 6019, + "code": 6021, "name": "WithdrawRequestNotReady", "msg": "Withdraw request has not elapsed the epoch lockup period yet" }, { - "code": 6020, + "code": 6022, "name": "SettlementNotExpired", "msg": "Settlement has not expired yet" }, { - "code": 6021, + "code": 6023, "name": "SettlementExpired", "msg": "Settlement has already expired" }, { - "code": 6022, + "code": 6024, "name": "UninitializedStake", "msg": "Stake is not initialized" }, { - "code": 6023, + "code": 6025, "name": "NoStakeOrNotFullyActivated", "msg": "Stake account is not fully activated" }, { - "code": 6024, + "code": 6026, "name": "UnexpectedRemainingAccounts", "msg": "Instruction context was provided with unexpected set of remaining accounts" }, { - "code": 6025, + "code": 6027, "name": "SettlementNotClosed", - "msg": "Closing SettlementClaim requires the settlement being closed" + "msg": "Required settlement to be closed" }, { - "code": 6026, + "code": 6028, "name": "StakeAccountIsFundedToSettlement", "msg": "Provided stake account has been already funded to a settlement" }, { - "code": 6027, + "code": 6029, "name": "ClaimSettlementProofFailed", "msg": "Settlement claim proof failed" }, { - "code": 6028, + "code": 6030, "name": "StakeLockedUp", "msg": "Provided stake account is locked-up" }, { - "code": 6029, + "code": 6031, "name": "StakeAccountNotBigEnoughToSplit", "msg": "Stake account is not big enough to be split" }, { - "code": 6030, + "code": 6032, "name": "ClaimAmountExceedsMaxTotalClaim", "msg": "Claiming bigger amount than the max total claim" }, { - "code": 6031, - "name": "ClaimCountExceedsMaxNumNodes", + "code": 6033, + "name": "ClaimCountExceedsMaxMerkleNodes", "msg": "Claim exceeded number of claimable nodes in the merkle tree" }, { - "code": 6032, + "code": 6034, "name": "EmptySettlementMerkleTree", "msg": "Empty merkle tree, nothing to be claimed" }, { - "code": 6033, + "code": 6035, "name": "ClaimingStakeAccountLamportsInsufficient", "msg": "Provided stake account has not enough lamports to cover the claim" }, { - "code": 6034, - "name": "StakeAccountNotFunded", - "msg": "Provided stake account is not funded under a settlement" + "code": 6036, + "name": "StakeAccountNotFundedToSettlement", + "msg": "Provided stake account is not funded under the settlement" }, { - "code": 6035, + "code": 6037, "name": "VoteAccountValidatorIdentityMismatch", "msg": "Validator vote account does not match to provided validator identity signature" }, { - "code": 6036, + "code": 6038, "name": "VoteAccountMismatch", "msg": "Bond vote account address does not match with the provided validator vote account" }, { - "code": 6037, + "code": 6039, "name": "ConfigAccountMismatch", "msg": "Bond config address does not match with the provided config account" }, { - "code": 6038, + "code": 6040, "name": "WithdrawRequestVoteAccountMismatch", "msg": "Withdraw request vote account address does not match with the provided validator vote account" }, { - "code": 6039, + "code": 6041, "name": "BondAccountMismatch", "msg": "Bond account address does not match with the stored one" }, { - "code": 6040, + "code": 6042, "name": "SettlementAccountMismatch", "msg": "Settlement account address does not match with the stored one" }, { - "code": 6041, + "code": 6043, "name": "RentCollectorMismatch", "msg": "Rent collector address does not match permitted rent collector" }, { - "code": 6042, + "code": 6044, "name": "StakerAuthorityMismatch", "msg": "Stake account's staker does not match with the provided authority" }, { - "code": 6043, + "code": 6045, "name": "NonBondStakeAuthorities", "msg": "One or both stake authorities does not belong to bonds program" }, { - "code": 6044, + "code": 6046, "name": "SettlementAuthorityMismatch", - "msg": "Settlement stake account authority does not match with the provided stake account authority" + "msg": "Stake account staker authority mismatches with the settlement authority" }, { - "code": 6045, + "code": 6047, "name": "StakeDelegationMismatch", "msg": "Delegation of provided stake account mismatches" }, { - "code": 6046, + "code": 6048, "name": "WithdrawRequestAmountTooSmall", "msg": "Too small non-withdrawn withdraw request amount, cancel and init new one" }, { - "code": 6047, + "code": 6049, "name": "WithdrawRequestAlreadyFulfilled", "msg": "Withdraw request has been already fulfilled" }, { - "code": 6048, - "name": "NotYetImplemented", - "msg": "Not yet implemented" + "code": 6050, + "name": "ClaimSettlementMerkleTreeNodeMismatch", + "msg": "Claim settlement merkle tree node mismatch" } ] }; diff --git a/packages/validator-bonds-sdk/package.json b/packages/validator-bonds-sdk/package.json index b5e59237..ec9bb303 100644 --- a/packages/validator-bonds-sdk/package.json +++ b/packages/validator-bonds-sdk/package.json @@ -29,22 +29,24 @@ "devDependencies": { "@coral-xyz/anchor": "^0.29.0", "@marinade.finance/marinade-ts-sdk": "^5.0.7", - "@marinade.finance/anchor-common": "^2.2.1", - "@marinade.finance/ts-common": "^2.2.1", - "@marinade.finance/web3js-common": "^2.2.1", + "@marinade.finance/anchor-common": "^2.2.2", + "@marinade.finance/ts-common": "^2.2.2", + "@marinade.finance/web3js-common": "^2.2.2", "@solana/buffer-layout": "^4.0.1", - "@solana/web3.js": "^1.87.6", + "@solana/web3.js": "^1.89.1", + "@types/crypto-js": "^4.2.1", "anchor-bankrun": "^0.3.0", - "solana-bankrun": "^0.2.0", "bn.js": "^5.2.1", "borsh": "^0.7.0", "jsbi": "^4.3.0", - "solana-spl-token-modern": "npm:@solana/spl-token@^0.3.8" + "solana-bankrun": "^0.2.0", + "solana-spl-token-modern": "npm:@solana/spl-token@^0.3.11" }, "peerDependencies": { "@coral-xyz/anchor": "^0.29.0", - "@solana/web3.js": "^1.87.3", "@solana/buffer-layout": "^4.0.1", + "@solana/web3.js": "^1.89.1", + "crypto-js": "^4.2.0", "bn.js": "^5.2.1", "bs58": "^5.0.0", "jsbi": "^4.3.0" diff --git a/packages/validator-bonds-sdk/src/api.ts b/packages/validator-bonds-sdk/src/api.ts index ce990ede..17efc1a6 100644 --- a/packages/validator-bonds-sdk/src/api.ts +++ b/packages/validator-bonds-sdk/src/api.ts @@ -1,5 +1,5 @@ import { ProgramAccount } from '@coral-xyz/anchor' -import { PublicKey } from '@solana/web3.js' +import { EpochInfo, PublicKey } from '@solana/web3.js' import { ValidatorBondsProgram, Config, @@ -7,6 +7,9 @@ import { WithdrawRequest, bondAddress, withdrawRequestAddress, + settlementAddress, + Settlement, + SettlementClaim, } from './sdk' import BN from 'bn.js' import { bs58 } from '@coral-xyz/anchor/dist/cjs/utils/bytes' @@ -62,20 +65,16 @@ export async function getBond( export async function findBonds({ program, config, - validatorVoteAccount, + voteAccount, bondAuthority, }: { program: ValidatorBondsProgram config?: PublicKey - validatorVoteAccount?: PublicKey + voteAccount?: PublicKey bondAuthority?: PublicKey }): Promise[]> { - if (config && validatorVoteAccount) { - const [bondAccount] = bondAddress( - config, - validatorVoteAccount, - program.programId - ) + if (config && voteAccount) { + const [bondAccount] = bondAddress(config, voteAccount, program.programId) const bondData = await getBond(program, bondAccount) return [{ publicKey: bondAccount, account: bondData }] } @@ -89,10 +88,10 @@ export async function findBonds({ }, }) } - if (validatorVoteAccount) { + if (voteAccount) { filters.push({ memcmp: { - bytes: validatorVoteAccount.toBase58(), + bytes: voteAccount.toBase58(), // 8 anchor offset + first data 32B config pubkey offset: 40, }, @@ -119,12 +118,12 @@ export async function getWithdrawRequest( export async function findWithdrawRequests({ program, - validatorVoteAccount, + voteAccount, bond, epoch, }: { program: ValidatorBondsProgram - validatorVoteAccount?: PublicKey + voteAccount?: PublicKey bond?: PublicKey epoch?: number | BN }): Promise[]> { @@ -140,10 +139,10 @@ export async function findWithdrawRequests({ return [{ publicKey: withdrawRequestAccount, account: withdrawRequestData }] } const filters = [] - if (validatorVoteAccount) { + if (voteAccount) { filters.push({ memcmp: { - bytes: validatorVoteAccount.toBase58(), + bytes: voteAccount.toBase58(), // 8 anchor offset offset: 8, }, @@ -152,7 +151,7 @@ export async function findWithdrawRequests({ if (epoch) { filters.push({ memcmp: { - bytes: bs58.encode(new BN(epoch).toArray('le', 8)), + bytes: bs58.encode(new BN(epoch).toArray('le', 8)), // TODO: consider to use the same number-to-bytes as in settlement address // 8 anchor offset + 32B validator vote pubkey + 32B bond pubkey offset: 72, }, @@ -160,3 +159,113 @@ export async function findWithdrawRequests({ } return await program.account.withdrawRequest.all(filters) } + +export async function getSettlement( + program: ValidatorBondsProgram, + address: PublicKey +): Promise { + return program.account.settlement.fetch(address) +} + +export async function findSettlements({ + program, + bond, + merkleRoot, + epoch, +}: { + program: ValidatorBondsProgram + bond?: PublicKey + merkleRoot?: Uint8Array | Buffer | number[] + epoch?: number | BN | EpochInfo +}): Promise[]> { + if (bond && merkleRoot && epoch) { + const [settlementAccount] = settlementAddress( + bond, + merkleRoot, + epoch, + program.programId + ) + const settlementData = await getSettlement(program, settlementAccount) + return [{ publicKey: settlementAccount, account: settlementData }] + } + const filters = [] + if (bond) { + filters.push({ + memcmp: { + bytes: bond.toBase58(), + // 8 anchor offset + offset: 8, + }, + }) + } + if (merkleRoot) { + filters.push({ + memcmp: { + bytes: bs58.encode(merkleRoot), + // 8 anchor offset + 32B bond pubkey + 32B settlement authority + offset: 72, + }, + }) + } + return await program.account.settlement.all(filters) +} + +export async function getSettlementClaim( + program: ValidatorBondsProgram, + address: PublicKey +): Promise { + return program.account.settlementClaim.fetch(address) +} + +export async function findSettlementClaims({ + program, + settlement, + stakeAuthority, + withdrawAuthority, + voteAccount, +}: { + program: ValidatorBondsProgram + settlement?: PublicKey + stakeAuthority?: PublicKey + withdrawAuthority?: PublicKey + voteAccount?: PublicKey +}): Promise[]> { + const filters = [] + if (settlement) { + filters.push({ + memcmp: { + bytes: settlement.toBase58(), + // 8 anchor offset + offset: 8, + }, + }) + } + if (stakeAuthority) { + filters.push({ + memcmp: { + bytes: stakeAuthority.toBase58(), + // 8 anchor offset + settlement 32B + offset: 40, + }, + }) + } + if (withdrawAuthority) { + filters.push({ + memcmp: { + bytes: withdrawAuthority.toBase58(), + // 8 anchor offset + 32B settlement + 32B stake authority + offset: 72, + }, + }) + } + if (voteAccount) { + filters.push({ + memcmp: { + bytes: voteAccount.toBase58(), + // 8 anchor offset + 32B settlement + 32B stake authority + withdraw authority 32B + offset: 104, + }, + }) + } + return await program.account.settlementClaim.all(filters) +} diff --git a/packages/validator-bonds-sdk/src/index.ts b/packages/validator-bonds-sdk/src/index.ts index a1772ebe..b0018d72 100644 --- a/packages/validator-bonds-sdk/src/index.ts +++ b/packages/validator-bonds-sdk/src/index.ts @@ -4,3 +4,4 @@ export * from './instructions' export * from './orchestrators' export * from './web3.js' export * from './utils' +export * from './merkleTree' diff --git a/packages/validator-bonds-sdk/src/instructions/cancelWithdrawRequest.ts b/packages/validator-bonds-sdk/src/instructions/cancelWithdrawRequest.ts index a4e8ba2c..0b586166 100644 --- a/packages/validator-bonds-sdk/src/instructions/cancelWithdrawRequest.ts +++ b/packages/validator-bonds-sdk/src/instructions/cancelWithdrawRequest.ts @@ -19,7 +19,7 @@ export async function cancelWithdrawRequestInstruction({ withdrawRequestAccount, bondAccount, configAccount, - validatorVoteAccount, + voteAccount, authority, rentCollector = anchorProgramWalletPubkey(program), }: { @@ -27,7 +27,7 @@ export async function cancelWithdrawRequestInstruction({ withdrawRequestAccount?: PublicKey bondAccount?: PublicKey configAccount?: PublicKey - validatorVoteAccount?: PublicKey + voteAccount?: PublicKey authority?: PublicKey | Keypair | Signer | WalletInterface // signer rentCollector?: PublicKey }): Promise<{ @@ -36,26 +36,21 @@ export async function cancelWithdrawRequestInstruction({ let withdrawRequestData: WithdrawRequest | undefined if ( withdrawRequestAccount !== undefined && - (bondAccount === undefined || validatorVoteAccount === undefined) + (bondAccount === undefined || voteAccount === undefined) ) { withdrawRequestData = await getWithdrawRequest( program, withdrawRequestAccount ) bondAccount = bondAccount ?? withdrawRequestData.bond - validatorVoteAccount = - validatorVoteAccount ?? withdrawRequestData.validatorVoteAccount + voteAccount = voteAccount ?? withdrawRequestData.voteAccount } if ( configAccount !== undefined && - validatorVoteAccount !== undefined && + voteAccount !== undefined && bondAccount === undefined ) { - bondAccount = bondAddress( - configAccount, - validatorVoteAccount, - program.programId - )[0] + bondAccount = bondAddress(configAccount, voteAccount, program.programId)[0] } if (bondAccount !== undefined && withdrawRequestAccount === undefined) { withdrawRequestAccount = withdrawRequestAddress( @@ -65,10 +60,10 @@ export async function cancelWithdrawRequestInstruction({ } if ( bondAccount !== undefined && - (validatorVoteAccount === undefined || authority === undefined) + (voteAccount === undefined || authority === undefined) ) { const bondData = await program.account.bond.fetch(bondAccount) - validatorVoteAccount = bondData.validatorVoteAccount + voteAccount = bondData.voteAccount authority = authority ?? bondData.authority } authority = authority ?? anchorProgramWalletPubkey(program) @@ -84,7 +79,7 @@ export async function cancelWithdrawRequestInstruction({ .cancelWithdrawRequest() .accounts({ bond: bondAccount, - validatorVoteAccount, + voteAccount, authority, withdrawRequest: withdrawRequestAccount, rentCollector, diff --git a/packages/validator-bonds-sdk/src/instructions/claimSettlement.ts b/packages/validator-bonds-sdk/src/instructions/claimSettlement.ts new file mode 100644 index 00000000..3595b51d --- /dev/null +++ b/packages/validator-bonds-sdk/src/instructions/claimSettlement.ts @@ -0,0 +1,146 @@ +import { + EpochInfo, + Keypair, + PublicKey, + SYSVAR_CLOCK_PUBKEY, + SYSVAR_STAKE_HISTORY_PUBKEY, + Signer, + StakeProgram, + SystemProgram, + TransactionInstruction, +} from '@solana/web3.js' +import { + Settlement, + ValidatorBondsProgram, + bondAddress, + settlementAddress, + settlementClaimAddress, + withdrawerAuthority, +} from '../sdk' +import { anchorProgramWalletPubkey } from '../utils' +import BN from 'bn.js' +import { Wallet as WalletInterface } from '@coral-xyz/anchor/dist/cjs/provider' +import { getBond, getSettlement } from '../api' + +export async function claimSettlementInstruction({ + program, + claimAmount, + merkleProof, + withdrawer, + stakeAccount, + settlementAccount, + settlementMerkleRoot, + settlementEpoch, + configAccount, + bondAccount, + voteAccount, + rentPayer = anchorProgramWalletPubkey(program), +}: { + program: ValidatorBondsProgram + claimAmount: number | BN + merkleProof: (number[] | Uint8Array | Buffer)[] + withdrawer: PublicKey + stakeAccount: PublicKey + settlementAccount?: PublicKey + settlementMerkleRoot?: number[] | Uint8Array | Buffer + settlementEpoch?: number | BN | EpochInfo + configAccount?: PublicKey + bondAccount?: PublicKey + voteAccount?: PublicKey + rentPayer?: PublicKey | Keypair | Signer | WalletInterface // signer +}): Promise<{ + instruction: TransactionInstruction + settlementClaimAccount: PublicKey + settlementAccount: PublicKey +}> { + const renPayerPubkey = + rentPayer instanceof PublicKey ? rentPayer : rentPayer.publicKey + + let settlementData: undefined | Settlement + if (settlementAccount !== undefined) { + settlementData = await getSettlement(program, settlementAccount) + bondAccount = bondAccount || settlementData.bond + } + + if ( + voteAccount !== undefined && + configAccount !== undefined && + bondAccount === undefined + ) { + ;[bondAccount] = bondAddress(configAccount, voteAccount, program.programId) + } + if (bondAccount === undefined) { + throw new Error('Either voteAccount or bondAccount must be provided') + } + + if (configAccount === undefined || voteAccount === undefined) { + const bondData = await getBond(program, bondAccount) + configAccount = configAccount || bondData.config + voteAccount = voteAccount || bondData.voteAccount + } + + if ( + settlementAccount === undefined && + settlementMerkleRoot !== undefined && + settlementEpoch !== undefined + ) { + ;[settlementAccount] = settlementAddress( + bondAccount, + settlementMerkleRoot, + settlementEpoch, + program.programId + ) + } + if (settlementAccount === undefined) { + throw new Error( + '[settlementAccount] must be provided or needed to have [bondAccount, merkleProof] to derive the address' + ) + } + + const merkleProofNumbers = merkleProof.map(proofPathRecord => { + if (Array.isArray(proofPathRecord)) { + return proofPathRecord + } else { + return Array.from(proofPathRecord) + } + }) + const [bondsWithdrawerAuthority] = withdrawerAuthority( + configAccount, + program.programId + ) + const [settlementClaimAccount] = settlementClaimAddress( + { + settlement: settlementAccount, + stakeAuthority: bondsWithdrawerAuthority, + voteAccount, + withdrawAuthority: withdrawer, + claim: claimAmount, + }, + program.programId + ) + + const instruction = await program.methods + .claimSettlement({ + proof: merkleProofNumbers, + claim: new BN(claimAmount), + }) + .accounts({ + withdrawAuthority: withdrawer, + config: configAccount, + bond: bondAccount, + settlement: settlementAccount, + settlementClaim: settlementClaimAccount, + stakeAccount, + rentPayer: renPayerPubkey, + systemProgram: SystemProgram.programId, + stakeHistory: SYSVAR_STAKE_HISTORY_PUBKEY, + clock: SYSVAR_CLOCK_PUBKEY, + stakeProgram: StakeProgram.programId, + }) + .instruction() + return { + instruction, + settlementClaimAccount, + settlementAccount, + } +} diff --git a/packages/validator-bonds-sdk/src/instructions/claimWithdrawRequest.ts b/packages/validator-bonds-sdk/src/instructions/claimWithdrawRequest.ts index 74ec534f..85dafd67 100644 --- a/packages/validator-bonds-sdk/src/instructions/claimWithdrawRequest.ts +++ b/packages/validator-bonds-sdk/src/instructions/claimWithdrawRequest.ts @@ -22,7 +22,7 @@ export async function claimWithdrawRequestInstruction({ withdrawRequestAccount, bondAccount, configAccount, - validatorVoteAccount, + voteAccount, stakeAccount, authority = anchorProgramWalletPubkey(program), splitStakeRentPayer = anchorProgramWalletPubkey(program), @@ -32,7 +32,7 @@ export async function claimWithdrawRequestInstruction({ withdrawRequestAccount?: PublicKey bondAccount?: PublicKey configAccount?: PublicKey - validatorVoteAccount?: PublicKey + voteAccount?: PublicKey stakeAccount: PublicKey authority?: PublicKey | Keypair | Signer | WalletInterface // signer splitStakeRentPayer?: PublicKey | Keypair | Signer | WalletInterface // signer @@ -43,27 +43,22 @@ export async function claimWithdrawRequestInstruction({ }> { if ( configAccount !== undefined && - validatorVoteAccount !== undefined && + voteAccount !== undefined && bondAccount === undefined ) { - bondAccount = bondAddress( - configAccount, - validatorVoteAccount, - program.programId - )[0] + bondAccount = bondAddress(configAccount, voteAccount, program.programId)[0] } let withdrawRequestData: WithdrawRequest | undefined if ( withdrawRequestAccount !== undefined && - (bondAccount === undefined || validatorVoteAccount === undefined) + (bondAccount === undefined || voteAccount === undefined) ) { withdrawRequestData = await getWithdrawRequest( program, withdrawRequestAccount ) bondAccount = bondAccount ?? withdrawRequestData.bond - validatorVoteAccount = - validatorVoteAccount ?? withdrawRequestData.validatorVoteAccount + voteAccount = voteAccount ?? withdrawRequestData.voteAccount } if (bondAccount !== undefined && withdrawRequestAccount === undefined) { withdrawRequestAccount = withdrawRequestAddress( @@ -73,10 +68,10 @@ export async function claimWithdrawRequestInstruction({ } if ( bondAccount !== undefined && - (validatorVoteAccount === undefined || configAccount === undefined) + (voteAccount === undefined || configAccount === undefined) ) { const bondData = await program.account.bond.fetch(bondAccount) - validatorVoteAccount = validatorVoteAccount ?? bondData.validatorVoteAccount + voteAccount = voteAccount ?? bondData.voteAccount configAccount = configAccount ?? bondData.config } @@ -92,7 +87,7 @@ export async function claimWithdrawRequestInstruction({ (await getWithdrawRequest(program, withdrawRequestAccount)) const voteAccountData = await getVoteAccount( program, - withdrawRequestData.validatorVoteAccount + withdrawRequestData.voteAccount ) withdrawer = voteAccountData.account.data.nodePubkey } @@ -109,7 +104,7 @@ export async function claimWithdrawRequestInstruction({ .accounts({ config: configAccount, bond: bondAccount, - validatorVoteAccount, + voteAccount, withdrawRequest: withdrawRequestAccount, stakeAccount, withdrawer, diff --git a/packages/validator-bonds-sdk/src/instructions/closeSettlement.ts b/packages/validator-bonds-sdk/src/instructions/closeSettlement.ts new file mode 100644 index 00000000..90b53282 --- /dev/null +++ b/packages/validator-bonds-sdk/src/instructions/closeSettlement.ts @@ -0,0 +1,112 @@ +import { + PublicKey, + TransactionInstruction, + StakeProgram, + SYSVAR_STAKE_HISTORY_PUBKEY, + SYSVAR_CLOCK_PUBKEY, + Keypair, +} from '@solana/web3.js' +import { + ValidatorBondsProgram, + bondAddress, + settlementAuthority, + withdrawerAuthority, +} from '../sdk' +import { getBond, getSettlement } from '../api' +import { findStakeAccount } from '../web3.js' + +export async function closeSettlementInstruction({ + program, + settlementAccount, + configAccount, + bondAccount, + voteAccount, + rentCollector, + splitRentCollector, + splitRentRefundAccount, +}: { + program: ValidatorBondsProgram + settlementAccount: PublicKey + configAccount?: PublicKey + bondAccount?: PublicKey + voteAccount?: PublicKey + rentCollector?: PublicKey + splitRentCollector?: PublicKey | null + splitRentRefundAccount?: PublicKey +}): Promise<{ + instruction: TransactionInstruction +}> { + if ( + voteAccount !== undefined && + configAccount !== undefined && + bondAccount === undefined + ) { + ;[bondAccount] = bondAddress(configAccount, voteAccount, program.programId) + } + if ( + bondAccount === undefined || + rentCollector === undefined || + splitRentCollector === undefined + ) { + const settlementData = await getSettlement(program, settlementAccount) + bondAccount = settlementData.bond + rentCollector = rentCollector || settlementData.rentCollector + splitRentCollector = splitRentCollector || settlementData.splitRentCollector + } + + if ( + configAccount === undefined || + (voteAccount === undefined && splitRentRefundAccount === undefined) + ) { + const bondData = await getBond(program, bondAccount) + configAccount = configAccount || bondData.config + voteAccount = bondData.voteAccount + } + + const [bondsAuth] = withdrawerAuthority(configAccount, program.programId) + + // when split rent collector is null then there was no need for creation split stake account + // in such case there is no splitRentCollector set and both, the splitRentCollector and stakeAccount + // can be set to an arbitrary address + if (splitRentCollector === null) { + splitRentCollector = Keypair.generate().publicKey + splitRentRefundAccount = splitRentRefundAccount || splitRentCollector + } else if (splitRentRefundAccount === undefined) { + const [settlementAuth] = settlementAuthority( + settlementAccount, + program.programId + ) + const stakeAccounts = await findStakeAccount({ + connection: program, + staker: settlementAuth, + withdrawer: bondsAuth, + voter: voteAccount, + }) + if (stakeAccounts.length === 0) { + throw new Error( + 'Cannot find any stake account usable to close settlement: ' + + `${settlementAccount.toBase58()} of vote account ${voteAccount?.toBase58()}` + ) + } + splitRentRefundAccount = stakeAccounts[0].publicKey + } + + const instruction = await program.methods + .closeSettlement() + .accounts({ + config: configAccount, + bond: bondAccount, + settlement: settlementAccount, + rentCollector, + splitRentCollector, + bondsWithdrawerAuthority: bondsAuth, + splitRentRefundAccount, + stakeProgram: StakeProgram.programId, + stakeHistory: SYSVAR_STAKE_HISTORY_PUBKEY, + clock: SYSVAR_CLOCK_PUBKEY, + }) + .instruction() + return { + instruction, + } +} diff --git a/packages/validator-bonds-sdk/src/instructions/closeSettlementClaim.ts b/packages/validator-bonds-sdk/src/instructions/closeSettlementClaim.ts new file mode 100644 index 00000000..dd7f0f39 --- /dev/null +++ b/packages/validator-bonds-sdk/src/instructions/closeSettlementClaim.ts @@ -0,0 +1,78 @@ +import { PublicKey, TransactionInstruction } from '@solana/web3.js' +import { + ValidatorBondsProgram, + settlementClaimAddress, + withdrawerAuthority, +} from '../sdk' +import { getSettlementClaim } from '../api' + +export async function closeSettlementClaimInstruction({ + program, + settlementAccount, + settlementClaimAccount, + rentCollector, + configAccount, + voteAccount, + withdrawer, + claimAmount, +}: { + program: ValidatorBondsProgram + settlementAccount: PublicKey + settlementClaimAccount?: PublicKey + rentCollector?: PublicKey + configAccount?: PublicKey + voteAccount?: PublicKey + withdrawer?: PublicKey + claimAmount?: number +}): Promise<{ + instruction: TransactionInstruction +}> { + if ( + settlementClaimAccount === undefined && + configAccount && + voteAccount && + withdrawer && + claimAmount + ) { + const [bondsWithdrawerAuthority] = withdrawerAuthority( + configAccount, + program.programId + ) + settlementClaimAccount = settlementClaimAddress( + { + settlement: settlementAccount, + stakeAuthority: bondsWithdrawerAuthority, + voteAccount, + withdrawAuthority: withdrawer, + claim: claimAmount, + }, + program.programId + )[0] + } + + if (!settlementClaimAccount) { + throw new Error( + 'settlementClaimAccount is required, provide address or parameters to derive it' + ) + } + + if (!rentCollector) { + const settlementClaimData = await getSettlementClaim( + program, + settlementClaimAccount + ) + rentCollector = settlementClaimData.rentCollector + } + + const instruction = await program.methods + .closeSettlementClaim() + .accounts({ + settlement: settlementAccount, + settlementClaim: settlementClaimAccount, + rentCollector, + }) + .instruction() + return { + instruction, + } +} diff --git a/packages/validator-bonds-sdk/src/instructions/configureBond.ts b/packages/validator-bonds-sdk/src/instructions/configureBond.ts index 1fddf009..83be1a74 100644 --- a/packages/validator-bonds-sdk/src/instructions/configureBond.ts +++ b/packages/validator-bonds-sdk/src/instructions/configureBond.ts @@ -4,7 +4,7 @@ import { Signer, TransactionInstruction, } from '@solana/web3.js' -import { CONFIG_ADDRESS, ValidatorBondsProgram } from '../sdk' +import { ValidatorBondsProgram } from '../sdk' import { checkAndGetBondAddress, anchorProgramWalletPubkey } from '../utils' import BN from 'bn.js' import { getBond } from '../api' @@ -13,19 +13,19 @@ import { Wallet as WalletInterface } from '@coral-xyz/anchor/dist/cjs/provider' export async function configureBondInstruction({ program, bondAccount, - configAccount = CONFIG_ADDRESS, - validatorVoteAccount, + configAccount, + voteAccount, authority = anchorProgramWalletPubkey(program), newBondAuthority, - newRevenueShareHundredthBps, + newCpmpe, }: { program: ValidatorBondsProgram bondAccount?: PublicKey configAccount?: PublicKey - validatorVoteAccount?: PublicKey + voteAccount?: PublicKey authority?: PublicKey | Keypair | Signer | WalletInterface | WalletInterface // signer newBondAuthority?: PublicKey - newRevenueShareHundredthBps?: BN | number + newCpmpe?: BN | number }): Promise<{ bondAccount: PublicKey instruction: TransactionInstruction @@ -33,34 +33,24 @@ export async function configureBondInstruction({ bondAccount = checkAndGetBondAddress( bondAccount, configAccount, - validatorVoteAccount, + voteAccount, program.programId ) - if (validatorVoteAccount === undefined) { + if (voteAccount === undefined) { const bondData = await getBond(program, bondAccount) - validatorVoteAccount = bondData.validatorVoteAccount + voteAccount = bondData.voteAccount } authority = authority instanceof PublicKey ? authority : authority.publicKey - if (newRevenueShareHundredthBps !== undefined) { - newRevenueShareHundredthBps = - newRevenueShareHundredthBps instanceof BN - ? newRevenueShareHundredthBps.toNumber() - : newRevenueShareHundredthBps - } - const instruction = await program.methods .configureBond({ bondAuthority: newBondAuthority === undefined ? null : newBondAuthority, - revenueShare: - newRevenueShareHundredthBps === undefined - ? null - : { hundredthBps: newRevenueShareHundredthBps }, + cpmpe: newCpmpe === undefined ? null : new BN(newCpmpe), }) .accounts({ bond: bondAccount, authority, - validatorVoteAccount, + voteAccount, }) .instruction() return { diff --git a/packages/validator-bonds-sdk/src/instructions/fundBond.ts b/packages/validator-bonds-sdk/src/instructions/fundBond.ts index acb8198a..9fb86c6b 100644 --- a/packages/validator-bonds-sdk/src/instructions/fundBond.ts +++ b/packages/validator-bonds-sdk/src/instructions/fundBond.ts @@ -17,14 +17,14 @@ export async function fundBondInstruction({ stakeAccount, stakeAccountAuthority = anchorProgramWalletPubkey(program), configAccount, - validatorVoteAccount, + voteAccount, }: { program: ValidatorBondsProgram bondAccount?: PublicKey stakeAccount: PublicKey stakeAccountAuthority?: PublicKey | Keypair | Signer | WalletInterface // signer configAccount?: PublicKey - validatorVoteAccount?: PublicKey + voteAccount?: PublicKey }): Promise<{ instruction: TransactionInstruction bondAccount: PublicKey @@ -32,7 +32,7 @@ export async function fundBondInstruction({ bondAccount = checkAndGetBondAddress( bondAccount, configAccount, - validatorVoteAccount, + voteAccount, program.programId ) if (configAccount === undefined) { diff --git a/packages/validator-bonds-sdk/src/instructions/fundSettlement.ts b/packages/validator-bonds-sdk/src/instructions/fundSettlement.ts new file mode 100644 index 00000000..afc0b0e5 --- /dev/null +++ b/packages/validator-bonds-sdk/src/instructions/fundSettlement.ts @@ -0,0 +1,97 @@ +import { + PublicKey, + TransactionInstruction, + StakeProgram, + SYSVAR_STAKE_HISTORY_PUBKEY, + SYSVAR_CLOCK_PUBKEY, + Keypair, + Signer, + SYSVAR_RENT_PUBKEY, + SystemProgram, +} from '@solana/web3.js' +import { ValidatorBondsProgram, bondAddress } from '../sdk' +import { getBond, getConfig, getSettlement } from '../api' +import { anchorProgramWalletPubkey } from '../utils' +import { Wallet as WalletInterface } from '@coral-xyz/anchor/dist/cjs/provider' + +export async function fundSettlementInstruction({ + program, + settlementAccount, + stakeAccount, + configAccount, + bondAccount, + voteAccount, + operatorAuthority, + splitStakeAccount = Keypair.generate(), + splitStakeRentPayer = anchorProgramWalletPubkey(program), +}: { + program: ValidatorBondsProgram + settlementAccount: PublicKey + stakeAccount: PublicKey + configAccount?: PublicKey + bondAccount?: PublicKey + voteAccount?: PublicKey + operatorAuthority?: PublicKey | Keypair | Signer | WalletInterface // signer + splitStakeAccount?: PublicKey | Keypair | Signer | WalletInterface // signer + splitStakeRentPayer?: PublicKey | Keypair | Signer | WalletInterface // signer +}): Promise<{ + instruction: TransactionInstruction + splitStakeAccount: PublicKey | Keypair | Signer | WalletInterface +}> { + if ( + voteAccount !== undefined && + configAccount !== undefined && + bondAccount === undefined + ) { + ;[bondAccount] = bondAddress(configAccount, voteAccount, program.programId) + } + if (bondAccount === undefined) { + const settlementData = await getSettlement(program, settlementAccount) + bondAccount = settlementData.bond + } + + if (configAccount === undefined) { + const bondData = await getBond(program, bondAccount) + configAccount = bondData.config + } + + if (operatorAuthority === undefined) { + const configData = await getConfig(program, configAccount) + operatorAuthority = configData.operatorAuthority + } + const operatorAuthorityPubkey = + operatorAuthority instanceof PublicKey + ? operatorAuthority + : operatorAuthority.publicKey + + const splitStakeAccountPubkey = + splitStakeAccount instanceof PublicKey + ? splitStakeAccount + : splitStakeAccount.publicKey + const splitStakeRentPayerPubkey = + splitStakeRentPayer instanceof PublicKey + ? splitStakeRentPayer + : splitStakeRentPayer.publicKey + + const instruction = await program.methods + .fundSettlement() + .accounts({ + config: configAccount, + bond: bondAccount, + settlement: settlementAccount, + operatorAuthority: operatorAuthorityPubkey, + stakeAccount, + splitStakeAccount: splitStakeAccountPubkey, + splitStakeRentPayer: splitStakeRentPayerPubkey, + systemProgram: SystemProgram.programId, + stakeHistory: SYSVAR_STAKE_HISTORY_PUBKEY, + rent: SYSVAR_RENT_PUBKEY, + clock: SYSVAR_CLOCK_PUBKEY, + stakeProgram: StakeProgram.programId, + }) + .instruction() + return { + instruction, + splitStakeAccount, + } +} diff --git a/packages/validator-bonds-sdk/src/instructions/index.ts b/packages/validator-bonds-sdk/src/instructions/index.ts index 34d1a295..1b5651fa 100644 --- a/packages/validator-bonds-sdk/src/instructions/index.ts +++ b/packages/validator-bonds-sdk/src/instructions/index.ts @@ -6,3 +6,9 @@ export * from './initBond' export * from './initConfig' export * from './initWithdrawRequest' export * from './merge' +export * from './reset' +export * from './initSettlement' +export * from './closeSettlement' +export * from './fundSettlement' +export * from './claimSettlement' +export * from './closeSettlementClaim' diff --git a/packages/validator-bonds-sdk/src/instructions/initBond.ts b/packages/validator-bonds-sdk/src/instructions/initBond.ts index cb5598ad..c7fb47bb 100644 --- a/packages/validator-bonds-sdk/src/instructions/initBond.ts +++ b/packages/validator-bonds-sdk/src/instructions/initBond.ts @@ -12,18 +12,18 @@ import { Wallet as WalletInterface } from '@coral-xyz/anchor/dist/cjs/provider' export async function initBondInstruction({ program, configAccount = CONFIG_ADDRESS, - validatorVoteAccount, + voteAccount, validatorIdentity, bondAuthority = anchorProgramWalletPubkey(program), - revenueShareHundredthBps = 0, + cpmpe = 0, rentPayer = anchorProgramWalletPubkey(program), }: { program: ValidatorBondsProgram - configAccount?: PublicKey - validatorVoteAccount: PublicKey + configAccount: PublicKey + voteAccount: PublicKey validatorIdentity?: PublicKey | Keypair | Signer | WalletInterface // signer bondAuthority?: PublicKey - revenueShareHundredthBps?: BN | number + cpmpe?: BN | number rentPayer?: PublicKey | Keypair | Signer | WalletInterface // signer }): Promise<{ instruction: TransactionInstruction @@ -39,23 +39,19 @@ export async function initBondInstruction({ rentPayer instanceof PublicKey ? rentPayer : rentPayer.publicKey const [bondAccount] = bondAddress( configAccount, - validatorVoteAccount, + voteAccount, program.programId ) - const revenueShare = - revenueShareHundredthBps instanceof BN - ? revenueShareHundredthBps.toNumber() - : revenueShareHundredthBps const instruction = await program.methods .initBond({ bondAuthority, - revenueShare: { hundredthBps: revenueShare }, + cpmpe: new BN(cpmpe), }) .accounts({ config: configAccount, bond: bondAccount, - validatorVoteAccount, + voteAccount, validatorIdentity: validatorIdentity ?? null, rentPayer: renPayerPubkey, }) diff --git a/packages/validator-bonds-sdk/src/instructions/initSettlement.ts b/packages/validator-bonds-sdk/src/instructions/initSettlement.ts new file mode 100644 index 00000000..3babd816 --- /dev/null +++ b/packages/validator-bonds-sdk/src/instructions/initSettlement.ts @@ -0,0 +1,106 @@ +import { + EpochInfo, + Keypair, + PublicKey, + Signer, + TransactionInstruction, +} from '@solana/web3.js' +import { ValidatorBondsProgram, bondAddress, settlementAddress } from '../sdk' +import { anchorProgramWalletPubkey } from '../utils' +import BN from 'bn.js' +import { Wallet as WalletInterface } from '@coral-xyz/anchor/dist/cjs/provider' +import { getBond, getConfig } from '../api' + +export async function initSettlementInstruction({ + program, + merkleRoot, + configAccount, + bondAccount, + voteAccount, + currentEpoch, + maxTotalClaim, + maxMerkleNodes, + operatorAuthority, + rentCollector, + rentPayer = anchorProgramWalletPubkey(program), +}: { + program: ValidatorBondsProgram + merkleRoot: number[] | Uint8Array | Buffer + configAccount?: PublicKey + bondAccount?: PublicKey + voteAccount?: PublicKey + currentEpoch?: EpochInfo | number | BN | bigint + maxTotalClaim: number | BN + maxMerkleNodes: number | BN + operatorAuthority?: PublicKey | Keypair | Signer | WalletInterface // signer + rentCollector?: PublicKey + rentPayer?: PublicKey | Keypair | Signer | WalletInterface // signer +}): Promise<{ + instruction: TransactionInstruction + settlementAccount: PublicKey + epoch: BN +}> { + const renPayerPubkey = + rentPayer instanceof PublicKey ? rentPayer : rentPayer.publicKey + rentCollector = rentCollector || renPayerPubkey + if (currentEpoch === undefined) { + currentEpoch = (await program.provider.connection.getEpochInfo()).epoch + } + + if ( + voteAccount !== undefined && + configAccount !== undefined && + bondAccount === undefined + ) { + ;[bondAccount] = bondAddress(configAccount, voteAccount, program.programId) + } + if (bondAccount === undefined) { + throw new Error('Either voteAccount or bondAccount must be provided') + } + + if (configAccount === undefined) { + const bondData = await getBond(program, bondAccount) + configAccount = bondData.config + } + + if (operatorAuthority === undefined) { + const configData = await getConfig(program, configAccount) + operatorAuthority = configData.operatorAuthority + } + const operatorAuthorityPubkey = + operatorAuthority instanceof PublicKey + ? operatorAuthority + : operatorAuthority.publicKey + + const [settlementAccount] = settlementAddress( + bondAccount, + merkleRoot, + currentEpoch, + program.programId + ) + + merkleRoot = Array.isArray(merkleRoot) ? merkleRoot : Array.from(merkleRoot) + const instruction = await program.methods + .initSettlement({ + merkleRoot, + maxTotalClaim: new BN(maxTotalClaim), + maxMerkleNodes: new BN(maxMerkleNodes), + rentCollector, + }) + .accounts({ + config: configAccount, + bond: bondAccount, + settlement: settlementAccount, + operatorAuthority: operatorAuthorityPubkey, + rentPayer: renPayerPubkey, + }) + .instruction() + return { + settlementAccount, + instruction, + epoch: + typeof currentEpoch === 'object' && 'epoch' in currentEpoch + ? new BN(currentEpoch.epoch) + : new BN(currentEpoch.toString()), + } +} diff --git a/packages/validator-bonds-sdk/src/instructions/initWithdrawRequest.ts b/packages/validator-bonds-sdk/src/instructions/initWithdrawRequest.ts index 889188d9..a56e5029 100644 --- a/packages/validator-bonds-sdk/src/instructions/initWithdrawRequest.ts +++ b/packages/validator-bonds-sdk/src/instructions/initWithdrawRequest.ts @@ -13,7 +13,7 @@ export async function initWithdrawRequestInstruction({ program, bondAccount, configAccount, - validatorVoteAccount, + voteAccount, authority = anchorProgramWalletPubkey(program), rentPayer = anchorProgramWalletPubkey(program), amount, @@ -21,7 +21,7 @@ export async function initWithdrawRequestInstruction({ program: ValidatorBondsProgram bondAccount?: PublicKey configAccount?: PublicKey - validatorVoteAccount?: PublicKey + voteAccount?: PublicKey authority?: PublicKey | Keypair | Signer | WalletInterface // signer rentPayer?: PublicKey | Keypair | Signer | WalletInterface // signer amount: BN | number @@ -32,12 +32,12 @@ export async function initWithdrawRequestInstruction({ bondAccount = checkAndGetBondAddress( bondAccount, configAccount, - validatorVoteAccount, + voteAccount, program.programId ) - if (!validatorVoteAccount || !configAccount) { + if (!voteAccount || !configAccount) { const bondData = await program.account.bond.fetch(bondAccount) - validatorVoteAccount = validatorVoteAccount ?? bondData.validatorVoteAccount + voteAccount = voteAccount ?? bondData.voteAccount configAccount = configAccount ?? bondData.config } @@ -55,7 +55,7 @@ export async function initWithdrawRequestInstruction({ .accounts({ config: configAccount, bond: bondAccount, - validatorVoteAccount, + voteAccount, withdrawRequest, authority, rentPayer, diff --git a/packages/validator-bonds-sdk/src/instructions/merge.ts b/packages/validator-bonds-sdk/src/instructions/merge.ts index 6ad45d75..3bdac227 100644 --- a/packages/validator-bonds-sdk/src/instructions/merge.ts +++ b/packages/validator-bonds-sdk/src/instructions/merge.ts @@ -4,21 +4,17 @@ import { StakeProgram, TransactionInstruction, } from '@solana/web3.js' -import { - CONFIG_ADDRESS, - ValidatorBondsProgram, - withdrawerAuthority, -} from '../sdk' +import { ValidatorBondsProgram, withdrawerAuthority } from '../sdk' export async function mergeInstruction({ program, - configAccount = CONFIG_ADDRESS, + configAccount, sourceStakeAccount, destinationStakeAccount, settlementAccount = PublicKey.default, }: { program: ValidatorBondsProgram - configAccount?: PublicKey + configAccount: PublicKey sourceStakeAccount: PublicKey destinationStakeAccount: PublicKey settlementAccount?: PublicKey diff --git a/packages/validator-bonds-sdk/src/instructions/reset.ts b/packages/validator-bonds-sdk/src/instructions/reset.ts new file mode 100644 index 00000000..24fc5fbb --- /dev/null +++ b/packages/validator-bonds-sdk/src/instructions/reset.ts @@ -0,0 +1,68 @@ +import { + PublicKey, + SYSVAR_CLOCK_PUBKEY, + SYSVAR_STAKE_HISTORY_PUBKEY, + StakeProgram, + TransactionInstruction, + STAKE_CONFIG_ID, +} from '@solana/web3.js' +import { ValidatorBondsProgram } from '../sdk' +import { checkAndGetBondAddress } from '../utils' +import { getBond } from '../api' +import { getStakeAccount } from '../web3.js' + +export async function resetInstruction({ + program, + stakeAccount, + settlementAccount, + bondAccount, + configAccount, + voteAccount, +}: { + program: ValidatorBondsProgram + stakeAccount: PublicKey + settlementAccount: PublicKey + bondAccount?: PublicKey + configAccount?: PublicKey + voteAccount?: PublicKey +}): Promise<{ + instruction: TransactionInstruction +}> { + if (voteAccount === undefined) { + const stakeAccountData = await getStakeAccount(program, stakeAccount) + if (stakeAccountData.voter === null) { + throw new Error( + `Cannot load vote account address from stake account ${stakeAccount.toBase58()}` + ) + } + voteAccount = stakeAccountData.voter + } + bondAccount = checkAndGetBondAddress( + bondAccount, + configAccount, + voteAccount, + program.programId + ) + if (configAccount === undefined) { + const bondData = await getBond(program, bondAccount) + configAccount = bondData.config + } + + const instruction = await program.methods + .reset() + .accounts({ + config: configAccount, + bond: bondAccount, + settlement: settlementAccount, + stakeAccount, + voteAccount, + stakeHistory: SYSVAR_STAKE_HISTORY_PUBKEY, + stakeConfig: STAKE_CONFIG_ID, + clock: SYSVAR_CLOCK_PUBKEY, + stakeProgram: StakeProgram.programId, + }) + .instruction() + return { + instruction, + } +} diff --git a/packages/validator-bonds-sdk/src/merkleTree.ts b/packages/validator-bonds-sdk/src/merkleTree.ts new file mode 100644 index 00000000..8791b2d0 --- /dev/null +++ b/packages/validator-bonds-sdk/src/merkleTree.ts @@ -0,0 +1,110 @@ +import { base64, bs58 } from '@coral-xyz/anchor/dist/cjs/utils/bytes' +import BN from 'bn.js' +import CryptoJS from 'crypto-js' + +export const LEAF_NODE_PREFIX = new BN(0) +export const LEAF_NODE_PREFIX_BUF = Buffer.alloc( + 1, + LEAF_NODE_PREFIX.toBuffer('le', 1) +) +export const INTERMEDIATE_NODE_PREFIX = new BN(1) +export const INTERMEDIATE_NODE_PREFIX_BUF = Buffer.alloc( + 1, + INTERMEDIATE_NODE_PREFIX.toBuffer('le', 1) +) + +export type MerkleTreeNodeEncoded = { + base58: string + base64: string + buffer: Buffer + words: number[] + wordArray: CryptoJS.lib.WordArray +} + +type MerkleTreeNodeDataInput = { + stakeAuthority: string + withdrawAuthority: string + voteAccount: string + claim: BN | number +} + +export type MerkleTreeNodeData = MerkleTreeNodeDataInput & + Omit & { claim: BN } + +// see insurance_engine/src/merkle_tree_collection.rs +export class MerkleTreeNode { + public data: MerkleTreeNodeData + constructor(data: MerkleTreeNodeDataInput) { + this.data = { + ...data, + claim: new BN(data.claim), + } + } + + public hash(): MerkleTreeNodeEncoded { + return MerkleTreeNode.hash(this.data) + } + + public leafNodeHash(): MerkleTreeNodeEncoded { + return MerkleTreeNode.leafNodeHash(this.data) + } + + public static hash({ + stakeAuthority, + withdrawAuthority, + voteAccount, + claim, + }: MerkleTreeNodeDataInput): MerkleTreeNodeEncoded { + const sha256 = CryptoJS.algo.SHA256.create() + sha256.update(stakeAuthority) + sha256.update(withdrawAuthority) + sha256.update(voteAccount) + claim = new BN(claim) + sha256.update( + CryptoJS.enc.Hex.parse(claim.toBuffer('le', 8).toString('hex')) + ) + const wordArray = sha256.finalize() + return MerkleTreeNode.toEncodings(wordArray) + } + + public static leafNodeHash({ + stakeAuthority, + withdrawAuthority, + voteAccount, + claim, + }: MerkleTreeNodeDataInput): MerkleTreeNodeEncoded { + const resultHash = MerkleTreeNode.hash({ + stakeAuthority, + withdrawAuthority, + voteAccount, + claim, + }) + return MerkleTreeNode.treeNodeFromHash(resultHash) + } + + public static treeNodeFromHash({ + base58, + }: MerkleTreeNodeEncoded): MerkleTreeNodeEncoded { + const buffer: Buffer = bs58.decode(base58) + const prolongedBuffer = Buffer.concat([LEAF_NODE_PREFIX_BUF, buffer]) + const sha256 = CryptoJS.algo.SHA256.create() + sha256.update(CryptoJS.enc.Hex.parse(prolongedBuffer.toString('hex'))) + const wordArray = sha256.finalize() + return MerkleTreeNode.toEncodings(wordArray) + } + + private static toEncodings( + wordArray: CryptoJS.lib.WordArray + ): MerkleTreeNodeEncoded { + const base64Hash = CryptoJS.enc.Base64.stringify(wordArray) + const buffer = base64.decode(base64Hash) + const base58Hash = bs58.encode(buffer) + return { + base58: base58Hash, + base64: base64Hash, + buffer, + words: Array.from(buffer), + wordArray, + } + } +} diff --git a/packages/validator-bonds-sdk/src/orchestrators/orchestrateWithdrawRequest.ts b/packages/validator-bonds-sdk/src/orchestrators/orchestrateWithdrawRequest.ts index 4f0a4528..8007d72c 100644 --- a/packages/validator-bonds-sdk/src/orchestrators/orchestrateWithdrawRequest.ts +++ b/packages/validator-bonds-sdk/src/orchestrators/orchestrateWithdrawRequest.ts @@ -12,10 +12,7 @@ import { } from '../sdk' import { getBond, getWithdrawRequest } from '../api' import assert from 'assert' -import { - StakeAccountParsed, - findStakeAccountAccount, -} from '../web3.js/stakeAccount' +import { StakeAccountParsed, findStakeAccount } from '../web3.js/stakeAccount' import BN from 'bn.js' import { mergeInstruction } from '../instructions/merge' import { claimWithdrawRequestInstruction } from '../instructions/claimWithdrawRequest' @@ -79,7 +76,7 @@ export async function orchestrateWithdrawDeposit({ const bondData = await getBond(program, bondAccount) const voteAccountData = await getVoteAccount( program, - withdrawRequestData.validatorVoteAccount + withdrawRequestData.voteAccount ) const withdrawer = voteAccountData.account.data.nodePubkey const configAccount = bondData.config @@ -90,7 +87,7 @@ export async function orchestrateWithdrawDeposit({ amountToWithdraw = amountToWithdraw <= new BN(0) ? new BN(0) : amountToWithdraw const stakeAccountsToWithdraw = ( - await findStakeAccountAccount({ + await findStakeAccount({ connection: program, staker: withdrawer, withdrawer, @@ -142,7 +139,7 @@ export async function orchestrateWithdrawDeposit({ withdrawRequestAccount, bondAccount, stakeAccount: destinationStakeAccount, - validatorVoteAccount: withdrawRequestData.validatorVoteAccount, + voteAccount: withdrawRequestData.voteAccount, splitStakeRentPayer, withdrawer, }) diff --git a/packages/validator-bonds-sdk/src/sdk.ts b/packages/validator-bonds-sdk/src/sdk.ts index 34e20e6a..f2cc3e20 100644 --- a/packages/validator-bonds-sdk/src/sdk.ts +++ b/packages/validator-bonds-sdk/src/sdk.ts @@ -15,11 +15,13 @@ import { AccountInfo, ConfirmOptions, Connection, + EpochInfo, Keypair, ParsedAccountData, PublicKey, } from '@solana/web3.js' import BN from 'bn.js' +import { MerkleTreeNode } from './merkleTree' export const CONFIG_ADDRESS = new PublicKey( 'vbMaRfmTCg92HWGzmd53APkMNpPnGVGZTUHwUJQkXAU' @@ -46,8 +48,6 @@ export type InitConfigArgs = IdlTypes['InitConfigArgs'] export type ConfigureConfigArgs = IdlTypes['ConfigureConfigArgs'] export type InitBondArgs = IdlTypes['InitBondArgs'] -export type HundredthBasisPoint = - IdlTypes['HundredthBasisPoint'] // --- CONSTANTS --- function seedFromConstants(seedName: string): Uint8Array { @@ -91,6 +91,10 @@ export const CONFIGURE_CONFIG_EVENT = 'ConfigureConfigEvent' export type ConfigureConfigEvent = IdlEvents[typeof CONFIGURE_CONFIG_EVENT] +export const FUND_SETTLEMENT_EVENT = 'FundSettlementEvent' +export type FundSettlementEvent = + IdlEvents[typeof FUND_SETTLEMENT_EVENT] + export const CLAIM_SETTLEMENT_EVENT = 'ClaimSettlementEvent' export type ClaimSettlementEvent = IdlEvents[typeof CLAIM_SETTLEMENT_EVENT] @@ -209,11 +213,23 @@ export function withdrawerAuthority( export function settlementAddress( bond: PublicKey, - merkleRoot: Uint8Array, + merkleRoot: Uint8Array | Buffer | number[], + epoch: EpochInfo | number | BN | bigint, validatorBondsProgramId: PublicKey = VALIDATOR_BONDS_PROGRAM_ID ): [PublicKey, number] { + if (Array.isArray(merkleRoot)) { + merkleRoot = new Uint8Array(merkleRoot) + } + const epochLittleEndian = Buffer.alloc(8) + const epochBigint = + typeof epoch === 'number' || + epoch instanceof BN || + typeof epoch === 'bigint' + ? BigInt(epoch.toString()) + : BigInt(epoch.epoch) + epochLittleEndian.writeBigInt64LE(epochBigint) return PublicKey.findProgramAddressSync( - [SETTLEMENT_SEED, bond.toBytes(), merkleRoot], + [SETTLEMENT_SEED, bond.toBytes(), merkleRoot, epochLittleEndian], validatorBondsProgramId ) } @@ -229,21 +245,31 @@ export function settlementAuthority( } export function settlementClaimAddress( - settlement: PublicKey, - stakeAuthority: PublicKey, - withdrawAuthority: PublicKey, - voteAccount: PublicKey, - claim: BN, + { + settlement, + stakeAuthority, + withdrawAuthority, + voteAccount, + claim, + }: { + settlement: PublicKey + stakeAuthority: PublicKey + withdrawAuthority: PublicKey + voteAccount: PublicKey + claim: BN | number + }, validatorBondsProgramId: PublicKey = VALIDATOR_BONDS_PROGRAM_ID ): [PublicKey, number] { return PublicKey.findProgramAddressSync( [ SETTLEMENT_CLAIM_SEED, settlement.toBytes(), - stakeAuthority.toBytes(), - withdrawAuthority.toBytes(), - voteAccount.toBytes(), - claim.toArrayLike(Buffer, 'le', 8), + MerkleTreeNode.hash({ + stakeAuthority: stakeAuthority.toBase58(), + withdrawAuthority: withdrawAuthority.toBase58(), + voteAccount: voteAccount.toBase58(), + claim: claim, + }).buffer, ], validatorBondsProgramId ) diff --git a/packages/validator-bonds-sdk/src/utils.ts b/packages/validator-bonds-sdk/src/utils.ts index 9cb5e484..df9d4a4d 100644 --- a/packages/validator-bonds-sdk/src/utils.ts +++ b/packages/validator-bonds-sdk/src/utils.ts @@ -26,7 +26,7 @@ export function checkAndGetBondAddress( return sdkBondAddress(config, voteAccount, programId)[0] } else { throw new Error( - 'Either [bondAccount] or [validatorVoteAccount and configAccount] is required' + 'Either [bondAccount] or [voteAccount and configAccount] is required' ) } } diff --git a/packages/validator-bonds-sdk/src/web3.js/stakeAccount.ts b/packages/validator-bonds-sdk/src/web3.js/stakeAccount.ts index 879f3048..5be2a982 100644 --- a/packages/validator-bonds-sdk/src/web3.js/stakeAccount.ts +++ b/packages/validator-bonds-sdk/src/web3.js/stakeAccount.ts @@ -12,6 +12,8 @@ import BN from 'bn.js' import { ProgramAccountInfo, programAccountInfo } from '../sdk' import { getConnection } from '.' +export const U64_MAX = new BN('ffffffffffffffff', 16) + export type StakeAccountParsed = { address: PublicKey withdrawer: PublicKey | null @@ -90,9 +92,8 @@ export async function getStakeAccount( currentEpoch?: number ): Promise { connection = getConnection(connection) - const { value: stakeAccountInfo } = await connection.getParsedAccountInfo( - address - ) + const { value: stakeAccountInfo } = + await connection.getParsedAccountInfo(address) if (!stakeAccountInfo) { throw new Error( @@ -122,17 +123,22 @@ export async function getStakeAccount( ) } -const STAKER_OFFSET = 12 -const WITHDRAWER_OFFSET = 44 +// https://github.com/solana-labs/solana/blob/v1.17.15/sdk/program/src/stake/state.rs#L60 +const STAKER_OFFSET = 12 // 4 for enum, 8 rent exempt reserve +const WITHDRAWER_OFFSET = 44 // 4 + 8 + staker pubkey +// https://github.com/solana-labs/solana/blob/v1.17.15/sdk/program/src/stake/state.rs#L414 +const VOTER_PUBKEY_OFFSET = 124 // 4 for enum + 120 for Meta -export async function findStakeAccountAccount({ +export async function findStakeAccount({ connection, staker, withdrawer, + voter, }: { connection: Provider | Connection | Program staker?: PublicKey withdrawer?: PublicKey + voter?: PublicKey }): Promise[]> { const innerConnection = getConnection(connection) @@ -153,6 +159,14 @@ export async function findStakeAccountAccount({ }, }) } + if (voter) { + filters.push({ + memcmp: { + offset: VOTER_PUBKEY_OFFSET, + bytes: voter.toBase58(), + }, + }) + } const parsedStakeAccounts = await innerConnection.getParsedProgramAccounts( StakeProgram.programId, @@ -183,8 +197,6 @@ export async function findStakeAccountAccount({ return Promise.all(parsedPromises) } -const U64_MAX = new BN('ffffffffffffffff', 16) - function pubkeyOrNull( value?: ConstructorParameters[0] | null ): PublicKey | null { diff --git a/packages/validator-bonds-sdk/src/web3.js/voteAccount.ts b/packages/validator-bonds-sdk/src/web3.js/voteAccount.ts index bcb7f08e..ddcc9862 100644 --- a/packages/validator-bonds-sdk/src/web3.js/voteAccount.ts +++ b/packages/validator-bonds-sdk/src/web3.js/voteAccount.ts @@ -26,43 +26,55 @@ export async function getVoteAccount( ) } + const versionOffset = 4 + const version = VoteAccountVersionLayout.decode( + toBuffer(voteAccountInfo.data), + 0 + ) as VoteAccountVersion + let voteAccountData: VoteAccount - try { - voteAccountData = fromAccount0_14_11Data(voteAccountInfo.data) - } catch (err) { - voteAccountData = fromAccount0_23_5Data(voteAccountInfo.data) + if (version.V0_23_5) { + voteAccountData = fromAccount0_23_5Data(voteAccountInfo.data, versionOffset) + } else if (version.V1_14_11 || version.CURRENT) { + voteAccountData = fromAccount1_14_11Data( + voteAccountInfo.data, + versionOffset + ) + } else { + throw new Error( + `Unknown vote account version: ${JSON.stringify( + version + )} at ${address.toBase58()}` + ) } return programAccountInfo(address, voteAccountInfo, voteAccountData) } /** - * Deserialize VoteAccount 0.14.11 from the account data. + * Deserialize VoteAccount 1.14.11 from the account data. */ -function fromAccount0_14_11Data( - buffer: Buffer | Uint8Array | Array +function fromAccount1_14_11Data( + buffer: Buffer | Uint8Array | Array, + versionOffset: number ): VoteAccount { - const versionOffset = 4 - // console.log( - // 'fromAccount0_14_11Data: starting buffer length', - // buffer.length, - // versionOffset - // ) - const voteAccount01411 = VoteAccount0_14_11Layout.decode( + const voteAccount1_14_11 = VoteAccount1_14_11Layout.decode( toBuffer(buffer), versionOffset ) // eslint-disable-next-line @typescript-eslint/no-explicit-any return new (VoteAccount as any)({ - nodePubkey: new PublicKey(voteAccount01411.nodePubkey), - authorizedWithdrawer: new PublicKey(voteAccount01411.authorizedWithdrawer), - commission: voteAccount01411.commission, - votes: voteAccount01411.votes, - rootSlot: voteAccount01411.rootSlot, + nodePubkey: new PublicKey(voteAccount1_14_11.nodePubkey), + authorizedWithdrawer: new PublicKey( + voteAccount1_14_11.authorizedWithdrawer + ), + commission: voteAccount1_14_11.commission, + votes: voteAccount1_14_11.votes, + rootSlot: voteAccount1_14_11.rootSlot, authorizedVoters: - voteAccount01411.authorizedVoters.map(parseAuthorizedVoter), - priorVoters: getPriorVoters(voteAccount01411.priorVoters), - epochCredits: voteAccount01411.epochCredits, - lastTimestamp: voteAccount01411.lastTimestamp, + voteAccount1_14_11.authorizedVoters.map(parseAuthorizedVoter), + priorVoters: getPriorVoters(voteAccount1_14_11.priorVoters), + epochCredits: voteAccount1_14_11.epochCredits, + lastTimestamp: voteAccount1_14_11.lastTimestamp, }) as VoteAccount } @@ -70,34 +82,29 @@ function fromAccount0_14_11Data( * Deserialize VoteAccount 0.23.5 from the account data. */ function fromAccount0_23_5Data( - buffer: Buffer | Uint8Array | Array + buffer: Buffer | Uint8Array | Array, + versionOffset: number ): VoteAccount { - const versionOffset = 4 - // console.log( - // 'fromAccount0_23_5Data: starting buffer length', - // buffer.length, - // versionOffset - // ) - const voteAccount0235 = VoteAccount0_23_5Layout.decode( + const voteAccount0_23_5 = VoteAccount0_23_5Layout.decode( toBuffer(buffer), versionOffset ) // eslint-disable-next-line @typescript-eslint/no-explicit-any return new (VoteAccount as any)({ - nodePubkey: new PublicKey(voteAccount0235.nodePubkey), - authorizedWithdrawer: new PublicKey(voteAccount0235.authorizedWithdrawer), - commission: voteAccount0235.commission, - votes: voteAccount0235.votes, - rootSlot: voteAccount0235.rootSlot, + nodePubkey: new PublicKey(voteAccount0_23_5.nodePubkey), + authorizedWithdrawer: new PublicKey(voteAccount0_23_5.authorizedWithdrawer), + commission: voteAccount0_23_5.commission, + votes: voteAccount0_23_5.votes, + rootSlot: voteAccount0_23_5.rootSlot, authorizedVoters: [ parseAuthorizedVoter({ - authorizedVoter: voteAccount0235.authorizedVoter, - epoch: voteAccount0235.authorizedVoterEpoch, + authorizedVoter: voteAccount0_23_5.authorizedVoter, + epoch: voteAccount0_23_5.authorizedVoterEpoch, }), ], - priorVoters: getPriorVoters(voteAccount0235.priorVoters), - epochCredits: voteAccount0235.epochCredits, - lastTimestamp: voteAccount0235.lastTimestamp, + priorVoters: getPriorVoters(voteAccount0_23_5.priorVoters), + epochCredits: voteAccount0_23_5.epochCredits, + lastTimestamp: voteAccount0_23_5.lastTimestamp, }) as VoteAccount } @@ -179,6 +186,19 @@ class OptionLayout extends BufferLayout.Layout { } } +type VoteAccountVersions = 'V0_23_5' | 'V1_14_11' | 'CURRENT' | 'UNKNOWN' + +type VoteAccountVersion = Readonly> + +// https://github.com/solana-labs/solana/blob/v1.17/sdk/program/src/vote/state/vote_state_versions.rs#L4 +const VoteAccountVersionLayout = BufferLayout.union( + BufferLayout.u32(), + BufferLayout.struct([BufferLayout.u32()]) +) +VoteAccountVersionLayout.addVariant(0, BufferLayout.struct([]), 'V0_23_5') +VoteAccountVersionLayout.addVariant(1, BufferLayout.struct([]), 'V1_14_11') +VoteAccountVersionLayout.addVariant(2, BufferLayout.struct([]), 'CURRENT') + /** * See solana-labs/web3.js: * https://github.com/solana-labs/solana-web3.js/blob/v1.88.0/packages/library-legacy/src/vote-account.ts#L77 @@ -235,7 +255,7 @@ const VoteAccount0_23_5Layout = BufferLayout.struct([ ), ]) -const VoteAccount0_14_11Layout = BufferLayout.struct([ +const VoteAccount1_14_11Layout = BufferLayout.struct([ publicKey('nodePubkey'), publicKey('authorizedWithdrawer'), BufferLayout.u8('commission'), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d2996dd..63b648e7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: ^29.7.0 version: 29.7.0 '@marinade.finance/jest-utils': - specifier: ^2.2.1 - version: 2.2.1(@jest/globals@29.7.0)(@solana/web3.js@1.87.6)(bn.js@5.2.1)(jest-shell-matchers@1.0.2) + specifier: ^2.2.2 + version: 2.2.2(@jest/globals@29.7.0)(@solana/web3.js@1.89.1)(bn.js@5.2.1)(jest-shell-matchers@1.0.2) '@types/bn.js': specifier: ^5.1.3 version: 5.1.3 @@ -24,17 +24,17 @@ importers: specifier: ^18.11.9 version: 18.11.9 gts: - specifier: ^4.0.1 - version: 4.0.1(typescript@4.9.5) + specifier: ^5.2.0 + version: 5.2.0(typescript@4.9.5) jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@18.11.9)(ts-node@10.9.1) + version: 29.7.0(@types/node@18.11.9)(ts-node@10.9.2) ts-jest: - specifier: ^29.1.1 - version: 29.1.1(@babel/core@7.23.2)(jest@29.7.0)(typescript@4.9.5) + specifier: ^29.1.2 + version: 29.1.2(@babel/core@7.23.2)(jest@29.7.0)(typescript@4.9.5) ts-node: - specifier: ^10.9.1 - version: 10.9.1(@types/node@18.11.9)(typescript@4.9.5) + specifier: ^10.9.2 + version: 10.9.2(@types/node@18.11.9)(typescript@4.9.5) typescript: specifier: 4.9.5 version: 4.9.5 @@ -45,82 +45,88 @@ importers: specifier: ^0.29.0 version: 0.29.0 '@marinade.finance/anchor-common': - specifier: ^2.2.1 - version: 2.2.1(@coral-xyz/anchor@0.29.0)(@marinade.finance/ts-common@2.2.1)(@solana/web3.js@1.87.6)(bn.js@5.2.1) + specifier: ^2.2.2 + version: 2.2.2(@coral-xyz/anchor@0.29.0)(@marinade.finance/ts-common@2.2.2)(@solana/web3.js@1.89.1)(bn.js@5.2.1) '@marinade.finance/cli-common': - specifier: ^2.2.1 - version: 2.2.1(@marinade.finance/ledger-utils@3.0.1)(@marinade.finance/ts-common@2.2.1)(@marinade.finance/web3js-common@2.2.1)(@solana/web3.js@1.87.6)(bn.js@5.2.1)(borsh@0.7.0)(bs58@5.0.0)(expand-tilde@2.0.2)(pino@8.16.1)(yaml@2.3.3) + specifier: ^2.2.2 + version: 2.2.2(@marinade.finance/ledger-utils@3.0.1)(@marinade.finance/ts-common@2.2.2)(@marinade.finance/web3js-common@2.2.2)(@solana/web3.js@1.89.1)(bn.js@5.2.1)(borsh@0.7.0)(bs58@5.0.0)(expand-tilde@2.0.2)(pino@8.17.2)(yaml@2.3.4) '@marinade.finance/ledger-utils': specifier: ^3.0.1 - version: 3.0.1(@ledgerhq/errors@6.16.1)(@ledgerhq/hw-app-solana@7.1.2)(@ledgerhq/hw-transport-node-hid-noevents@6.29.2)(@marinade.finance/ts-common@2.2.1)(@solana/web3.js@1.87.6) + version: 3.0.1(@ledgerhq/errors@6.16.1)(@ledgerhq/hw-app-solana@7.1.2)(@ledgerhq/hw-transport-node-hid-noevents@6.29.2)(@marinade.finance/ts-common@2.2.2)(@solana/web3.js@1.89.1) '@marinade.finance/ts-common': - specifier: ^2.2.1 - version: 2.2.1 + specifier: ^2.2.2 + version: 2.2.2 '@marinade.finance/validator-bonds-sdk': specifier: ^1.1.10 version: link:../validator-bonds-sdk '@marinade.finance/web3js-common': - specifier: ^2.2.1 - version: 2.2.1(@marinade.finance/ts-common@2.2.1)(@solana/web3.js@1.87.6)(bn.js@5.2.1)(borsh@0.7.0)(bs58@5.0.0) + specifier: ^2.2.2 + version: 2.2.2(@marinade.finance/ts-common@2.2.2)(@solana/web3.js@1.89.1)(bn.js@5.2.1)(borsh@0.7.0)(bs58@5.0.0) '@solana/web3.js': - specifier: ^1.87.6 - version: 1.87.6 + specifier: ^1.89.1 + version: 1.89.1 bn.js: specifier: ^5.2.1 version: 5.2.1 commander: - specifier: ^9.5.0 - version: 9.5.0 + specifier: ^11.1.0 + version: 11.1.0 jsbi: specifier: ^4.3.0 version: 4.3.0 pino: - specifier: ^8.16.1 - version: 8.16.1 + specifier: ^8.17.2 + version: 8.17.2 pino-pretty: - specifier: ^10.2.3 - version: 10.2.3 + specifier: ^10.3.1 + version: 10.3.1 solana-spl-token-modern: - specifier: npm:@solana/spl-token@^0.3.8 - version: /@solana/spl-token@0.3.8(@solana/web3.js@1.87.6) + specifier: npm:@solana/spl-token@^0.3.11 + version: /@solana/spl-token@0.3.11(@solana/web3.js@1.89.1)(fastestsmallesttextencoderdecoder@1.0.22) yaml: - specifier: ^2.3.3 - version: 2.3.3 + specifier: ^2.3.4 + version: 2.3.4 devDependencies: '@marinade.finance/jest-utils': - specifier: ^2.2.1 - version: 2.2.1(@jest/globals@29.7.0)(@solana/web3.js@1.87.6)(bn.js@5.2.1)(jest-shell-matchers@1.0.2) + specifier: ^2.2.2 + version: 2.2.2(@jest/globals@29.7.0)(@solana/web3.js@1.89.1)(bn.js@5.2.1)(jest-shell-matchers@1.0.2) packages/validator-bonds-sdk: dependencies: bs58: specifier: ^5.0.0 version: 5.0.0 + crypto-js: + specifier: ^4.2.0 + version: 4.2.0 devDependencies: '@coral-xyz/anchor': specifier: ^0.29.0 version: 0.29.0 '@marinade.finance/anchor-common': - specifier: ^2.2.1 - version: 2.2.1(@coral-xyz/anchor@0.29.0)(@marinade.finance/ts-common@2.2.1)(@solana/web3.js@1.87.6)(bn.js@5.2.1) + specifier: ^2.2.2 + version: 2.2.2(@coral-xyz/anchor@0.29.0)(@marinade.finance/ts-common@2.2.2)(@solana/web3.js@1.89.1)(bn.js@5.2.1) '@marinade.finance/marinade-ts-sdk': specifier: ^5.0.7 - version: 5.0.7(@solana/web3.js@1.87.6)(bn.js@5.2.1)(jsbi@4.3.0) + version: 5.0.7(@solana/web3.js@1.89.1)(bn.js@5.2.1)(fastestsmallesttextencoderdecoder@1.0.22)(jsbi@4.3.0) '@marinade.finance/ts-common': - specifier: ^2.2.1 - version: 2.2.1 + specifier: ^2.2.2 + version: 2.2.2 '@marinade.finance/web3js-common': - specifier: ^2.2.1 - version: 2.2.1(@marinade.finance/ts-common@2.2.1)(@solana/web3.js@1.87.6)(bn.js@5.2.1)(borsh@0.7.0)(bs58@5.0.0) + specifier: ^2.2.2 + version: 2.2.2(@marinade.finance/ts-common@2.2.2)(@solana/web3.js@1.89.1)(bn.js@5.2.1)(borsh@0.7.0)(bs58@5.0.0) '@solana/buffer-layout': specifier: ^4.0.1 version: 4.0.1 '@solana/web3.js': - specifier: ^1.87.6 - version: 1.87.6 + specifier: ^1.89.1 + version: 1.89.1 + '@types/crypto-js': + specifier: ^4.2.1 + version: 4.2.1 anchor-bankrun: specifier: ^0.3.0 - version: 0.3.0(@coral-xyz/anchor@0.29.0)(@solana/web3.js@1.87.6)(solana-bankrun@0.2.0) + version: 0.3.0(@coral-xyz/anchor@0.29.0)(@solana/web3.js@1.89.1)(solana-bankrun@0.2.0) bn.js: specifier: ^5.2.1 version: 5.2.1 @@ -134,8 +140,8 @@ importers: specifier: ^0.2.0 version: 0.2.0 solana-spl-token-modern: - specifier: npm:@solana/spl-token@^0.3.8 - version: /@solana/spl-token@0.3.8(@solana/web3.js@1.87.6) + specifier: npm:@solana/spl-token@^0.3.11 + version: /@solana/spl-token@0.3.11(@solana/web3.js@1.89.1)(fastestsmallesttextencoderdecoder@1.0.22) packages: @@ -446,6 +452,13 @@ packages: engines: {node: '>=6.9.0'} dependencies: regenerator-runtime: 0.14.0 + dev: true + + /@babel/runtime@7.23.9: + resolution: {integrity: sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.0 /@babel/template@7.22.15: resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==} @@ -491,8 +504,8 @@ packages: resolution: {integrity: sha512-+P/vPdORawvg3A9Wj02iquxb4T0C5m4P6aZBVYysKl4Amk+r6aMPZkUhilBkD6E4Nuxnoajv3CFykUfkGE0n5g==} engines: {node: '>=11'} dependencies: - '@coral-xyz/borsh': 0.27.0(@solana/web3.js@1.87.6) - '@solana/web3.js': 1.87.6 + '@coral-xyz/borsh': 0.27.0(@solana/web3.js@1.89.1) + '@solana/web3.js': 1.89.1 base64-js: 1.5.1 bn.js: 5.2.1 bs58: 4.0.1 @@ -516,8 +529,8 @@ packages: resolution: {integrity: sha512-kQ02Hv2ZqxtWP30WN1d4xxT4QqlOXYDxmEd3k/bbneqhV3X5QMO4LAtoUFs7otxyivOgoqam5Il5qx81FuI4vw==} engines: {node: '>=11'} dependencies: - '@coral-xyz/borsh': 0.28.0(@solana/web3.js@1.87.6) - '@solana/web3.js': 1.87.6 + '@coral-xyz/borsh': 0.28.0(@solana/web3.js@1.89.1) + '@solana/web3.js': 1.89.1 base64-js: 1.5.1 bn.js: 5.2.1 bs58: 4.0.1 @@ -541,9 +554,9 @@ packages: resolution: {integrity: sha512-eny6QNG0WOwqV0zQ7cs/b1tIuzZGmP7U7EcH+ogt4Gdbl8HDmIYVMh/9aTmYZPaFWjtUaI8qSn73uYEXWfATdA==} engines: {node: '>=11'} dependencies: - '@coral-xyz/borsh': 0.29.0(@solana/web3.js@1.87.6) + '@coral-xyz/borsh': 0.29.0(@solana/web3.js@1.89.1) '@noble/hashes': 1.3.2 - '@solana/web3.js': 1.87.6 + '@solana/web3.js': 1.89.1 bn.js: 5.2.1 bs58: 4.0.1 buffer-layout: 1.2.2 @@ -560,35 +573,35 @@ packages: - encoding - utf-8-validate - /@coral-xyz/borsh@0.27.0(@solana/web3.js@1.87.6): + /@coral-xyz/borsh@0.27.0(@solana/web3.js@1.89.1): resolution: {integrity: sha512-tJKzhLukghTWPLy+n8K8iJKgBq1yLT/AxaNd10yJrX8mI56ao5+OFAKAqW/h0i79KCvb4BK0VGO5ECmmolFz9A==} engines: {node: '>=10'} peerDependencies: '@solana/web3.js': ^1.68.0 dependencies: - '@solana/web3.js': 1.87.6 + '@solana/web3.js': 1.89.1 bn.js: 5.2.1 buffer-layout: 1.2.2 dev: true - /@coral-xyz/borsh@0.28.0(@solana/web3.js@1.87.6): + /@coral-xyz/borsh@0.28.0(@solana/web3.js@1.89.1): resolution: {integrity: sha512-/u1VTzw7XooK7rqeD7JLUSwOyRSesPUk0U37BV9zK0axJc1q0nRbKFGFLYCQ16OtdOJTTwGfGp11Lx9B45bRCQ==} engines: {node: '>=10'} peerDependencies: '@solana/web3.js': ^1.68.0 dependencies: - '@solana/web3.js': 1.87.6 + '@solana/web3.js': 1.89.1 bn.js: 5.2.1 buffer-layout: 1.2.2 dev: true - /@coral-xyz/borsh@0.29.0(@solana/web3.js@1.87.6): + /@coral-xyz/borsh@0.29.0(@solana/web3.js@1.89.1): resolution: {integrity: sha512-s7VFVa3a0oqpkuRloWVPdCK7hMbAMY270geZOGfCnaqexrP5dTIpbEHL33req6IYPPJ0hYa71cdvJ1h6V55/oQ==} engines: {node: '>=10'} peerDependencies: '@solana/web3.js': ^1.68.0 dependencies: - '@solana/web3.js': 1.87.6 + '@solana/web3.js': 1.89.1 bn.js: 5.2.1 buffer-layout: 1.2.2 @@ -684,7 +697,7 @@ packages: slash: 3.0.0 dev: true - /@jest/core@29.7.0(ts-node@10.9.1): + /@jest/core@29.7.0(ts-node@10.9.2): resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: @@ -705,7 +718,7 @@ packages: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@18.11.9)(ts-node@10.9.1) + jest-config: 29.7.0(@types/node@18.11.9)(ts-node@10.9.2) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -967,26 +980,26 @@ packages: resolution: {integrity: sha512-ExDoj1QV5eC6TEbMdLUMMk9cfvNKhhv5gXol4SmULRVCx/3iyCPhJ74nsb3S0Vb+/f+XujBEj3vQn5+cwS0fNA==} dev: false - /@marinade.finance/anchor-common@2.2.1(@coral-xyz/anchor@0.29.0)(@marinade.finance/ts-common@2.2.1)(@solana/web3.js@1.87.6)(bn.js@5.2.1): - resolution: {integrity: sha512-nafxV7gAbcYozENk87SWJcgG0sCAgVR49CtptzalDD+bCXl/YmzQqCrVOSLliusx8JmmqAzQBUSWAygayCkjTA==} + /@marinade.finance/anchor-common@2.2.2(@coral-xyz/anchor@0.29.0)(@marinade.finance/ts-common@2.2.2)(@solana/web3.js@1.89.1)(bn.js@5.2.1): + resolution: {integrity: sha512-AxTt1x/DXazSjXvOzAqmohPJE4JJf0tiHhg9bRQTtPyAYW7YFME2DUTpR+uuC7nIftHAb9LRNyBQ+r7AQqyYmQ==} peerDependencies: '@coral-xyz/anchor': ^0.29.0 || 0.29 - '@marinade.finance/ts-common': ^2.2.1 + '@marinade.finance/ts-common': ^2.2.2 '@solana/web3.js': ^1.78.5 bn.js: ^5.2.1 dependencies: '@coral-xyz/anchor': 0.29.0 - '@marinade.finance/ts-common': 2.2.1 - '@solana/web3.js': 1.87.6 + '@marinade.finance/ts-common': 2.2.2 + '@solana/web3.js': 1.89.1 bn.js: 5.2.1 - /@marinade.finance/cli-common@2.2.1(@marinade.finance/ledger-utils@3.0.1)(@marinade.finance/ts-common@2.2.1)(@marinade.finance/web3js-common@2.2.1)(@solana/web3.js@1.87.6)(bn.js@5.2.1)(borsh@0.7.0)(bs58@5.0.0)(expand-tilde@2.0.2)(pino@8.16.1)(yaml@2.3.3): - resolution: {integrity: sha512-R+ZxslVcLGoGhLytvZ5SHY0RE10gbruDlCyrXea5l3EIKMVQ6kDLjOEhOwFtBZRVfPgNjL8AD/DI1hAeSINqCQ==} + /@marinade.finance/cli-common@2.2.2(@marinade.finance/ledger-utils@3.0.1)(@marinade.finance/ts-common@2.2.2)(@marinade.finance/web3js-common@2.2.2)(@solana/web3.js@1.89.1)(bn.js@5.2.1)(borsh@0.7.0)(bs58@5.0.0)(expand-tilde@2.0.2)(pino@8.17.2)(yaml@2.3.4): + resolution: {integrity: sha512-EDbzMNnxuRIMnn2QcgGXqnlxPahcmLBemiCzM4V8mBTqMj/+MI8jutCSSPewZECMuzcGEM3D2V7Fq9b2Y0VIqA==} engines: {node: '>=16.0.0'} peerDependencies: '@marinade.finance/ledger-utils': ^3.0.1 - '@marinade.finance/ts-common': ^2.2.1 - '@marinade.finance/web3js-common': ^2.2.1 + '@marinade.finance/ts-common': ^2.2.2 + '@marinade.finance/web3js-common': ^2.2.2 '@solana/web3.js': ^1.78.5 bn.js: ^5.2.1 borsh: ^0.7.0 @@ -995,19 +1008,19 @@ packages: pino: ^8.15.1 yaml: ^2.3.2 dependencies: - '@marinade.finance/ledger-utils': 3.0.1(@ledgerhq/errors@6.16.1)(@ledgerhq/hw-app-solana@7.1.2)(@ledgerhq/hw-transport-node-hid-noevents@6.29.2)(@marinade.finance/ts-common@2.2.1)(@solana/web3.js@1.87.6) - '@marinade.finance/ts-common': 2.2.1 - '@marinade.finance/web3js-common': 2.2.1(@marinade.finance/ts-common@2.2.1)(@solana/web3.js@1.87.6)(bn.js@5.2.1)(borsh@0.7.0)(bs58@5.0.0) - '@solana/web3.js': 1.87.6 + '@marinade.finance/ledger-utils': 3.0.1(@ledgerhq/errors@6.16.1)(@ledgerhq/hw-app-solana@7.1.2)(@ledgerhq/hw-transport-node-hid-noevents@6.29.2)(@marinade.finance/ts-common@2.2.2)(@solana/web3.js@1.89.1) + '@marinade.finance/ts-common': 2.2.2 + '@marinade.finance/web3js-common': 2.2.2(@marinade.finance/ts-common@2.2.2)(@solana/web3.js@1.89.1)(bn.js@5.2.1)(borsh@0.7.0)(bs58@5.0.0) + '@solana/web3.js': 1.89.1 bn.js: 5.2.1 borsh: 0.7.0 bs58: 5.0.0 expand-tilde: 2.0.2 - pino: 8.16.1 - yaml: 2.3.3 + pino: 8.17.2 + yaml: 2.3.4 dev: false - /@marinade.finance/directed-stake-sdk@0.0.4(@solana/web3.js@1.87.6)(bn.js@5.2.1)(jsbi@4.3.0): + /@marinade.finance/directed-stake-sdk@0.0.4(@solana/web3.js@1.89.1)(bn.js@5.2.1)(jsbi@4.3.0): resolution: {integrity: sha512-5WTBDRg8hbCHS54wUUhPUuXj9JiQE3Bc1+nmDI3JivQPHu7VsDaPB3w1SHRh0bfARnZQg9oKg8s6jVwNiScCsQ==} engines: {node: '>10'} peerDependencies: @@ -1016,7 +1029,7 @@ packages: jsbi: ^4.3.0 dependencies: '@coral-xyz/anchor': 0.27.0 - '@solana/web3.js': 1.87.6 + '@solana/web3.js': 1.89.1 bn.js: 5.2.1 bs58: 5.0.0 jsbi: 4.3.0 @@ -1026,8 +1039,8 @@ packages: - utf-8-validate dev: true - /@marinade.finance/jest-utils@2.2.1(@jest/globals@29.7.0)(@solana/web3.js@1.87.6)(bn.js@5.2.1)(jest-shell-matchers@1.0.2): - resolution: {integrity: sha512-TivYNmwXvC75agoeL3T7qvVVg4ojxNFTFZCOB3FgXNFB/c1m2Q8VRawpDQbkit+M3culaxwAkKnuzD29m0MYPA==} + /@marinade.finance/jest-utils@2.2.2(@jest/globals@29.7.0)(@solana/web3.js@1.89.1)(bn.js@5.2.1)(jest-shell-matchers@1.0.2): + resolution: {integrity: sha512-CoiftPBvMAkn0YrVf1OSFMaR0f88dndy1fZkSB55FL2I9PZu7wBFaIAbJm5TNPSuQT0/io+t4HH0IxsGzrDK4g==} peerDependencies: '@jest/globals': ^29.5.0 '@solana/web3.js': ^1.78.5 @@ -1035,12 +1048,12 @@ packages: jest-shell-matchers: ^1.0.2 dependencies: '@jest/globals': 29.7.0 - '@solana/web3.js': 1.87.6 + '@solana/web3.js': 1.89.1 bn.js: 5.2.1 jest-shell-matchers: 1.0.2(jest@29.7.0) dev: true - /@marinade.finance/ledger-utils@3.0.1(@ledgerhq/errors@6.16.1)(@ledgerhq/hw-app-solana@7.1.2)(@ledgerhq/hw-transport-node-hid-noevents@6.29.2)(@marinade.finance/ts-common@2.2.1)(@solana/web3.js@1.87.6): + /@marinade.finance/ledger-utils@3.0.1(@ledgerhq/errors@6.16.1)(@ledgerhq/hw-app-solana@7.1.2)(@ledgerhq/hw-transport-node-hid-noevents@6.29.2)(@marinade.finance/ts-common@2.2.2)(@solana/web3.js@1.89.1): resolution: {integrity: sha512-EIklQpxQHJ8f/YpE/HVYz4b6097I0kFR2aAZpgHocagSHuERRhXxoI46Ge7iDkLPnkjOaYOUtMxTdm2a1Hc4Wg==} peerDependencies: '@ledgerhq/errors': ^6.16.1 @@ -1052,19 +1065,19 @@ packages: '@ledgerhq/errors': 6.16.1 '@ledgerhq/hw-app-solana': 7.1.2 '@ledgerhq/hw-transport-node-hid-noevents': 6.29.2 - '@marinade.finance/ts-common': 2.2.1 - '@solana/web3.js': 1.87.6 + '@marinade.finance/ts-common': 2.2.2 + '@solana/web3.js': 1.89.1 dev: false - /@marinade.finance/marinade-ts-sdk@5.0.7(@solana/web3.js@1.87.6)(bn.js@5.2.1)(jsbi@4.3.0): + /@marinade.finance/marinade-ts-sdk@5.0.7(@solana/web3.js@1.89.1)(bn.js@5.2.1)(fastestsmallesttextencoderdecoder@1.0.22)(jsbi@4.3.0): resolution: {integrity: sha512-J2oTeD4C5uIeqlXaVLoYIoUa+qvBwUXTbUWXw45qDCtFbt06HYusnuDc9f2eLEmr3IiNpKYQyZpUqsxPjuNvVg==} engines: {anchor: '>=0.28.0', node: '>=16.0.0'} dependencies: '@coral-xyz/anchor': 0.28.0 - '@marinade.finance/directed-stake-sdk': 0.0.4(@solana/web3.js@1.87.6)(bn.js@5.2.1)(jsbi@4.3.0) + '@marinade.finance/directed-stake-sdk': 0.0.4(@solana/web3.js@1.89.1)(bn.js@5.2.1)(jsbi@4.3.0) '@marinade.finance/native-staking-sdk': 1.0.0 '@solana/spl-stake-pool': 0.6.5 - '@solana/spl-token-3.x': /@solana/spl-token@0.3.8(@solana/web3.js@1.87.6) + '@solana/spl-token-3.x': /@solana/spl-token@0.3.11(@solana/web3.js@1.89.1)(fastestsmallesttextencoderdecoder@1.0.22) borsh: 0.7.0 bs58: 5.0.0 transitivePeerDependencies: @@ -1072,6 +1085,7 @@ packages: - bn.js - bufferutil - encoding + - fastestsmallesttextencoderdecoder - jsbi - utf-8-validate dev: true @@ -1079,8 +1093,8 @@ packages: /@marinade.finance/native-staking-sdk@1.0.0: resolution: {integrity: sha512-Cj2dy3SH9LAgcFBpGEgRHyF/nK+L7SfNIHkGqAZYRDVgn/h7u8bqKNkYqKXgCWbA5WwPmJzugWWm1DxEt7LyPQ==} dependencies: - '@solana/spl-memo': 0.2.3(@solana/web3.js@1.87.6) - '@solana/web3.js': 1.87.6 + '@solana/spl-memo': 0.2.3(@solana/web3.js@1.89.1) + '@solana/web3.js': 1.89.1 bn.js: 5.2.1 transitivePeerDependencies: - bufferutil @@ -1088,21 +1102,21 @@ packages: - utf-8-validate dev: true - /@marinade.finance/ts-common@2.2.1: - resolution: {integrity: sha512-6NH6qX4xHkyNIDJQktMSjouw+spt3XWvmMSUJHq6aH3kd3mvLUXykgZjYCRc51RO1wqEAaUoiYGb3Mw6UGQrQA==} + /@marinade.finance/ts-common@2.2.2: + resolution: {integrity: sha512-Afitl+51JQ8IkO/bey1KkKx86vbtmd9UK9ZOEwBSPV29Y51GIsKdZfxYko4x25GQH4c/sGSl7+VPyaQtAP1g2A==} - /@marinade.finance/web3js-common@2.2.1(@marinade.finance/ts-common@2.2.1)(@solana/web3.js@1.87.6)(bn.js@5.2.1)(borsh@0.7.0)(bs58@5.0.0): - resolution: {integrity: sha512-ij+FhpVrRSjjFeO0c5MgQqkiUz9cZU6VBpvIeFi8cl9jMnpi9IfEmMRl/IxncBX6e1jRafDlLMk/ae0eOpTUyw==} + /@marinade.finance/web3js-common@2.2.2(@marinade.finance/ts-common@2.2.2)(@solana/web3.js@1.89.1)(bn.js@5.2.1)(borsh@0.7.0)(bs58@5.0.0): + resolution: {integrity: sha512-A7rlpfmk0OTsv/Pg7ORN4yxyiMdnpFqS/pJjuZ21BUQjNfUyrpSLVYjGiDTFsY1xJzpynudCf41K0E8QGLmLpA==} engines: {node: '>=16.0.0'} peerDependencies: - '@marinade.finance/ts-common': 2.2.1 + '@marinade.finance/ts-common': 2.2.2 '@solana/web3.js': ^1.78.5 bn.js: ^5.2.1 borsh: ^0.7.0 bs58: ^5.0.0 dependencies: - '@marinade.finance/ts-common': 2.2.1 - '@solana/web3.js': 1.87.6 + '@marinade.finance/ts-common': 2.2.2 + '@solana/web3.js': 1.89.1 bn.js: 5.2.1 borsh: 0.7.0 bs58: 5.0.0 @@ -1137,13 +1151,18 @@ packages: fastq: 1.15.0 dev: true - /@project-serum/borsh@0.2.5(@solana/web3.js@1.87.6): + /@pkgr/core@0.1.1: + resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + dev: true + + /@project-serum/borsh@0.2.5(@solana/web3.js@1.89.1): resolution: {integrity: sha512-UmeUkUoKdQ7rhx6Leve1SssMR/Ghv8qrEiyywyxSWg7ooV7StdpPBhciiy5eB3T0qU1BXvdRNC8TdrkxK7WC5Q==} engines: {node: '>=10'} peerDependencies: '@solana/web3.js': ^1.2.0 dependencies: - '@solana/web3.js': 1.87.6 + '@solana/web3.js': 1.89.1 bn.js: 5.2.1 buffer-layout: 1.2.2 dev: true @@ -1169,7 +1188,7 @@ packages: engines: {node: '>= 10'} dependencies: '@solana/buffer-layout': 4.0.1 - '@solana/web3.js': 1.87.6 + '@solana/web3.js': 1.89.1 bigint-buffer: 1.1.5 bignumber.js: 9.1.2 transitivePeerDependencies: @@ -1183,23 +1202,52 @@ packages: dependencies: buffer: 6.0.3 - /@solana/spl-memo@0.2.3(@solana/web3.js@1.87.6): + /@solana/codecs-core@2.0.0-experimental.8618508: + resolution: {integrity: sha512-JCz7mKjVKtfZxkuDtwMAUgA7YvJcA2BwpZaA1NOLcted4OMC4Prwa3DUe3f3181ixPYaRyptbF0Ikq2MbDkYEA==} + + /@solana/codecs-data-structures@2.0.0-experimental.8618508: + resolution: {integrity: sha512-sLpjL9sqzaDdkloBPV61Rht1tgaKq98BCtIKRuyscIrmVPu3wu0Bavk2n/QekmUzaTsj7K1pVSniM0YqCdnEBw==} + dependencies: + '@solana/codecs-core': 2.0.0-experimental.8618508 + '@solana/codecs-numbers': 2.0.0-experimental.8618508 + + /@solana/codecs-numbers@2.0.0-experimental.8618508: + resolution: {integrity: sha512-EXQKfzFr3CkKKNzKSZPOOOzchXsFe90TVONWsSnVkonO9z+nGKALE0/L9uBmIFGgdzhhU9QQVFvxBMclIDJo2Q==} + dependencies: + '@solana/codecs-core': 2.0.0-experimental.8618508 + + /@solana/codecs-strings@2.0.0-experimental.8618508(fastestsmallesttextencoderdecoder@1.0.22): + resolution: {integrity: sha512-b2yhinr1+oe+JDmnnsV0641KQqqDG8AQ16Z/x7GVWO+AWHMpRlHWVXOq8U1yhPMA4VXxl7i+D+C6ql0VGFp0GA==} + peerDependencies: + fastestsmallesttextencoderdecoder: ^1.0.22 + dependencies: + '@solana/codecs-core': 2.0.0-experimental.8618508 + '@solana/codecs-numbers': 2.0.0-experimental.8618508 + fastestsmallesttextencoderdecoder: 1.0.22 + + /@solana/options@2.0.0-experimental.8618508: + resolution: {integrity: sha512-fy/nIRAMC3QHvnKi63KEd86Xr/zFBVxNW4nEpVEU2OT0gCEKwHY4Z55YHf7XujhyuM3PNpiBKg/YYw5QlRU4vg==} + dependencies: + '@solana/codecs-core': 2.0.0-experimental.8618508 + '@solana/codecs-numbers': 2.0.0-experimental.8618508 + + /@solana/spl-memo@0.2.3(@solana/web3.js@1.89.1): resolution: {integrity: sha512-CNsKSsl85ebuVoeGq1LDYi5M/PMs1Pxv2/UsyTgS6b30qrYqZOXha5ouZzgGKtJtZ3C3dxfOAEw6caJPN1N63w==} engines: {node: '>=16'} peerDependencies: '@solana/web3.js': ^1.20.0 dependencies: - '@solana/web3.js': 1.87.6 + '@solana/web3.js': 1.89.1 buffer: 6.0.3 dev: true /@solana/spl-stake-pool@0.6.5: resolution: {integrity: sha512-gAYjX4LlRem3Bje1csZOOBStX8wAH8b8tu4sublUTIoJxLMdEbXqnwc8RJ2lAsmFkjxxomEM9Hk65F8jcvv15A==} dependencies: - '@project-serum/borsh': 0.2.5(@solana/web3.js@1.87.6) + '@project-serum/borsh': 0.2.5(@solana/web3.js@1.89.1) '@solana/buffer-layout': 4.0.1 '@solana/spl-token': 0.1.8 - '@solana/web3.js': 1.87.6 + '@solana/web3.js': 1.89.1 bn.js: 5.2.1 buffer: 6.0.3 transitivePeerDependencies: @@ -1208,12 +1256,28 @@ packages: - utf-8-validate dev: true + /@solana/spl-token-metadata@0.1.2(@solana/web3.js@1.89.1)(fastestsmallesttextencoderdecoder@1.0.22): + resolution: {integrity: sha512-hJYnAJNkDrtkE2Q41YZhCpeOGU/0JgRFXbtrtOuGGeKc3pkEUHB9DDoxZAxx+XRno13GozUleyBi0qypz4c3bw==} + engines: {node: '>=16'} + peerDependencies: + '@solana/web3.js': ^1.87.6 + dependencies: + '@solana/codecs-core': 2.0.0-experimental.8618508 + '@solana/codecs-data-structures': 2.0.0-experimental.8618508 + '@solana/codecs-numbers': 2.0.0-experimental.8618508 + '@solana/codecs-strings': 2.0.0-experimental.8618508(fastestsmallesttextencoderdecoder@1.0.22) + '@solana/options': 2.0.0-experimental.8618508 + '@solana/spl-type-length-value': 0.1.0 + '@solana/web3.js': 1.89.1 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + /@solana/spl-token@0.1.8: resolution: {integrity: sha512-LZmYCKcPQDtJgecvWOgT/cnoIQPWjdH+QVyzPcFvyDUiT0DiRjZaam4aqNUyvchLFhzgunv3d9xOoyE34ofdoQ==} engines: {node: '>= 10'} dependencies: '@babel/runtime': 7.23.2 - '@solana/web3.js': 1.87.6 + '@solana/web3.js': 1.89.1 bn.js: 5.2.1 buffer: 6.0.3 buffer-layout: 1.2.2 @@ -1224,25 +1288,33 @@ packages: - utf-8-validate dev: true - /@solana/spl-token@0.3.8(@solana/web3.js@1.87.6): - resolution: {integrity: sha512-ogwGDcunP9Lkj+9CODOWMiVJEdRtqHAtX2rWF62KxnnSWtMZtV9rDhTrZFshiyJmxDnRL/1nKE1yJHg4jjs3gg==} + /@solana/spl-token@0.3.11(@solana/web3.js@1.89.1)(fastestsmallesttextencoderdecoder@1.0.22): + resolution: {integrity: sha512-bvohO3rIMSVL24Pb+I4EYTJ6cL82eFpInEXD/I8K8upOGjpqHsKUoAempR/RnUlI1qSFNyFlWJfu6MNUgfbCQQ==} engines: {node: '>=16'} peerDependencies: - '@solana/web3.js': ^1.47.4 + '@solana/web3.js': ^1.88.0 dependencies: '@solana/buffer-layout': 4.0.1 '@solana/buffer-layout-utils': 0.2.0 - '@solana/web3.js': 1.87.6 + '@solana/spl-token-metadata': 0.1.2(@solana/web3.js@1.89.1)(fastestsmallesttextencoderdecoder@1.0.22) + '@solana/web3.js': 1.89.1 buffer: 6.0.3 transitivePeerDependencies: - bufferutil - encoding + - fastestsmallesttextencoderdecoder - utf-8-validate - /@solana/web3.js@1.87.6: - resolution: {integrity: sha512-LkqsEBgTZztFiccZZXnawWa8qNCATEqE97/d0vIwjTclmVlc8pBpD1DmjfVHtZ1HS5fZorFlVhXfpwnCNDZfyg==} + /@solana/spl-type-length-value@0.1.0: + resolution: {integrity: sha512-JBMGB0oR4lPttOZ5XiUGyvylwLQjt1CPJa6qQ5oM+MBCndfjz2TKKkw0eATlLLcYmq1jBVsNlJ2cD6ns2GR7lA==} + engines: {node: '>=16'} dependencies: - '@babel/runtime': 7.23.2 + buffer: 6.0.3 + + /@solana/web3.js@1.89.1: + resolution: {integrity: sha512-t9TTLtPQxtQB3SAf/5E8xPXfVDsC6WGOsgKY02l2cbe0HLymT7ynE8Hu48Lk5qynHCquj6nhISfEHcjMkYpu/A==} + dependencies: + '@babel/runtime': 7.23.9 '@noble/curves': 1.2.0 '@noble/hashes': 1.3.2 '@solana/buffer-layout': 4.0.1 @@ -1318,6 +1390,10 @@ packages: dependencies: '@types/node': 18.18.6 + /@types/crypto-js@4.2.1: + resolution: {integrity: sha512-FSPGd9+OcSok3RsM0UZ/9fcvMOXJ1ENE/ZbLfOPlBWj7BgXtEAM8VYfTtT760GiLbQIMoVozwVuisjvsVwqYWw==} + dev: true + /@types/graceful-fs@4.1.8: resolution: {integrity: sha512-NhRH7YzWq8WiNKVavKPBmtLYZHxNY19Hh+az28O/phfp68CF45pMFud+ZzJ8ewnxnC5smIdF3dqFeiSUQ5I+pw==} dependencies: @@ -1570,7 +1646,7 @@ packages: uri-js: 4.4.1 dev: true - /anchor-bankrun@0.3.0(@coral-xyz/anchor@0.29.0)(@solana/web3.js@1.87.6)(solana-bankrun@0.2.0): + /anchor-bankrun@0.3.0(@coral-xyz/anchor@0.29.0)(@solana/web3.js@1.89.1)(solana-bankrun@0.2.0): resolution: {integrity: sha512-PYBW5fWX+iGicIS5MIM/omhk1tQPUc0ELAnI/IkLKQJ6d75De/CQRh8MF2bU/TgGyFi6zEel80wUe3uRol9RrQ==} engines: {node: '>= 10'} peerDependencies: @@ -1579,7 +1655,7 @@ packages: solana-bankrun: ^0.2.0 dependencies: '@coral-xyz/anchor': 0.29.0 - '@solana/web3.js': 1.87.6 + '@solana/web3.js': 1.89.1 solana-bankrun: 0.2.0 dev: true @@ -1725,6 +1801,7 @@ packages: /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true /base-x@3.0.9: resolution: {integrity: sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==} @@ -1781,12 +1858,6 @@ packages: concat-map: 0.0.1 dev: true - /brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} - dependencies: - balanced-match: 1.0.2 - dev: false - /braces@3.0.2: resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} engines: {node: '>=8'} @@ -1977,14 +2048,14 @@ packages: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} dev: false + /commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + dev: false + /commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - /commander@9.5.0: - resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} - engines: {node: ^12.20.0 || >=14} - dev: false - /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: true @@ -1993,7 +2064,7 @@ packages: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} dev: true - /create-jest@29.7.0(@types/node@18.11.9)(ts-node@10.9.1): + /create-jest@29.7.0(@types/node@18.11.9)(ts-node@10.9.2): resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -2002,7 +2073,7 @@ packages: chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@18.11.9)(ts-node@10.9.1) + jest-config: 29.7.0(@types/node@18.11.9)(ts-node@10.9.2) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -2036,6 +2107,10 @@ packages: resolution: {integrity: sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg==} engines: {node: '>=8'} + /crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + dev: false + /dateformat@4.6.3: resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} dev: false @@ -2197,8 +2272,8 @@ packages: engines: {node: '>=10'} dev: true - /eslint-config-prettier@8.10.0(eslint@8.50.0): - resolution: {integrity: sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==} + /eslint-config-prettier@9.0.0(eslint@8.50.0): + resolution: {integrity: sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==} hasBin: true peerDependencies: eslint: '>=7.0.0' @@ -2232,21 +2307,25 @@ packages: semver: 6.3.1 dev: true - /eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.10.0)(eslint@8.50.0)(prettier@2.7.1): - resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==} - engines: {node: '>=12.0.0'} + /eslint-plugin-prettier@5.0.0(eslint-config-prettier@9.0.0)(eslint@8.50.0)(prettier@3.0.3): + resolution: {integrity: sha512-AgaZCVuYDXHUGxj/ZGu1u8H8CYgDY3iG6w5kUFw4AzMVXzB7VvbKgYR4nATIN+OvUrghMbiDLeimVjVY5ilq3w==} + engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: - eslint: '>=7.28.0' + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' eslint-config-prettier: '*' - prettier: '>=2.0.0' + prettier: '>=3.0.0' peerDependenciesMeta: + '@types/eslint': + optional: true eslint-config-prettier: optional: true dependencies: eslint: 8.50.0 - eslint-config-prettier: 8.10.0(eslint@8.50.0) - prettier: 2.7.1 + eslint-config-prettier: 9.0.0(eslint@8.50.0) + prettier: 3.0.3 prettier-linter-helpers: 1.0.0 + synckit: 0.8.8 dev: true /eslint-scope@5.1.1: @@ -2484,6 +2563,9 @@ packages: /fast-stable-stringify@1.0.0: resolution: {integrity: sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==} + /fastestsmallesttextencoderdecoder@1.0.22: + resolution: {integrity: sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==} + /fastq@1.15.0: resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} dependencies: @@ -2555,6 +2637,7 @@ packages: /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true /fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} @@ -2617,17 +2700,6 @@ packages: path-is-absolute: 1.0.1 dev: true - /glob@8.1.0: - resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} - engines: {node: '>=12'} - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 5.1.6 - once: 1.4.0 - dev: false - /globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} @@ -2660,9 +2732,9 @@ packages: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} dev: true - /gts@4.0.1(typescript@4.9.5): - resolution: {integrity: sha512-BeFQLqra3HH5/MHuog+uOpn/h6qVMLeBB0WZ4bgal7CbqbvfaCxCYA7vlfaMAANhgw1Ko2HFZA4iuOJqOBW5lg==} - engines: {node: '>=12'} + /gts@5.2.0(typescript@4.9.5): + resolution: {integrity: sha512-25qOnePUUX7upFc4ycqWersDBq+o1X6hXUTW56JOWCxPYKJXQ1RWzqT9q+2SU3LfPKJf+4sz4Dw3VT0p96Kv6g==} + engines: {node: '>=14'} hasBin: true peerDependencies: typescript: '>=3' @@ -2671,19 +2743,20 @@ packages: '@typescript-eslint/parser': 5.62.0(eslint@8.50.0)(typescript@4.9.5) chalk: 4.1.2 eslint: 8.50.0 - eslint-config-prettier: 8.10.0(eslint@8.50.0) + eslint-config-prettier: 9.0.0(eslint@8.50.0) eslint-plugin-node: 11.1.0(eslint@8.50.0) - eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.10.0)(eslint@8.50.0)(prettier@2.7.1) + eslint-plugin-prettier: 5.0.0(eslint-config-prettier@9.0.0)(eslint@8.50.0)(prettier@3.0.3) execa: 5.1.1 inquirer: 7.3.3 json5: 2.2.3 meow: 9.0.0 ncp: 2.0.0 - prettier: 2.7.1 + prettier: 3.0.3 rimraf: 3.0.2 typescript: 4.9.5 write-file-atomic: 4.0.2 transitivePeerDependencies: + - '@types/eslint' - supports-color dev: true @@ -2709,11 +2782,8 @@ packages: function-bind: 1.1.2 dev: true - /help-me@4.2.0: - resolution: {integrity: sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA==} - dependencies: - glob: 8.1.0 - readable-stream: 3.6.2 + /help-me@5.0.0: + resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} dev: false /homedir-polyfill@1.0.3: @@ -2795,6 +2865,7 @@ packages: dependencies: once: 1.4.0 wrappy: 1.0.2 + dev: true /inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -3003,7 +3074,7 @@ packages: - supports-color dev: true - /jest-cli@29.7.0(@types/node@18.11.9)(ts-node@10.9.1): + /jest-cli@29.7.0(@types/node@18.11.9)(ts-node@10.9.2): resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -3013,14 +3084,14 @@ packages: node-notifier: optional: true dependencies: - '@jest/core': 29.7.0(ts-node@10.9.1) + '@jest/core': 29.7.0(ts-node@10.9.2) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@18.11.9)(ts-node@10.9.1) + create-jest: 29.7.0(@types/node@18.11.9)(ts-node@10.9.2) exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@18.11.9)(ts-node@10.9.1) + jest-config: 29.7.0(@types/node@18.11.9)(ts-node@10.9.2) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -3031,7 +3102,7 @@ packages: - ts-node dev: true - /jest-config@29.7.0(@types/node@18.11.9)(ts-node@10.9.1): + /jest-config@29.7.0(@types/node@18.11.9)(ts-node@10.9.2): resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: @@ -3066,7 +3137,7 @@ packages: pretty-format: 29.7.0 slash: 3.0.0 strip-json-comments: 3.1.1 - ts-node: 10.9.1(@types/node@18.11.9)(typescript@4.9.5) + ts-node: 10.9.2(@types/node@18.11.9)(typescript@4.9.5) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -3285,7 +3356,7 @@ packages: peerDependencies: jest: ^23.0.0 || 29 dependencies: - jest: 29.7.0(@types/node@18.11.9)(ts-node@10.9.1) + jest: 29.7.0(@types/node@18.11.9)(ts-node@10.9.2) spawn-with-mocks: 1.0.2 dev: true @@ -3365,7 +3436,7 @@ packages: supports-color: 8.1.1 dev: true - /jest@29.7.0(@types/node@18.11.9)(ts-node@10.9.1): + /jest@29.7.0(@types/node@18.11.9)(ts-node@10.9.2): resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -3375,10 +3446,10 @@ packages: node-notifier: optional: true dependencies: - '@jest/core': 29.7.0(ts-node@10.9.1) + '@jest/core': 29.7.0(ts-node@10.9.2) '@jest/types': 29.6.3 import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@18.11.9)(ts-node@10.9.1) + jest-cli: 29.7.0(@types/node@18.11.9)(ts-node@10.9.2) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -3611,13 +3682,6 @@ packages: brace-expansion: 1.1.11 dev: true - /minimatch@5.1.6: - resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} - engines: {node: '>=10'} - dependencies: - brace-expansion: 2.0.1 - dev: false - /minimist-options@4.1.0: resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} engines: {node: '>= 6'} @@ -3874,15 +3938,15 @@ packages: split2: 4.2.0 dev: false - /pino-pretty@10.2.3: - resolution: {integrity: sha512-4jfIUc8TC1GPUfDyMSlW1STeORqkoxec71yhxIpLDQapUu8WOuoz2TTCoidrIssyz78LZC69whBMPIKCMbi3cw==} + /pino-pretty@10.3.1: + resolution: {integrity: sha512-az8JbIYeN/1iLj2t0jR9DV48/LQ3RC6hZPpapKPkb84Q+yTidMCpgWxIT3N0flnBDilyBQ1luWNpOeJptjdp/g==} hasBin: true dependencies: colorette: 2.0.20 dateformat: 4.6.3 fast-copy: 3.0.1 fast-safe-stringify: 2.1.1 - help-me: 4.2.0 + help-me: 5.0.0 joycon: 3.1.1 minimist: 1.2.8 on-exit-leak-free: 2.1.2 @@ -3898,8 +3962,8 @@ packages: resolution: {integrity: sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==} dev: false - /pino@8.16.1: - resolution: {integrity: sha512-3bKsVhBmgPjGV9pyn4fO/8RtoVDR8ssW1ev819FsRXlRNgW8gR/9Kx+gCK4UPWd4JjrRDLWpzd/pb1AyWm3MGA==} + /pino@8.17.2: + resolution: {integrity: sha512-LA6qKgeDMLr2ux2y/YiUt47EfgQ+S9LznBWOJdN3q1dx2sv0ziDLUBeVpyVv17TEcGCBuWf0zNtg3M5m1NhhWQ==} hasBin: true dependencies: atomic-sleep: 1.0.0 @@ -3907,7 +3971,7 @@ packages: on-exit-leak-free: 2.1.2 pino-abstract-transport: 1.1.0 pino-std-serializers: 6.2.2 - process-warning: 2.2.0 + process-warning: 3.0.0 quick-format-unescaped: 4.0.4 real-require: 0.2.0 safe-stable-stringify: 2.4.3 @@ -3958,9 +4022,9 @@ packages: fast-diff: 1.3.0 dev: true - /prettier@2.7.1: - resolution: {integrity: sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==} - engines: {node: '>=10.13.0'} + /prettier@3.0.3: + resolution: {integrity: sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==} + engines: {node: '>=14'} hasBin: true dev: true @@ -3973,8 +4037,8 @@ packages: react-is: 18.2.0 dev: true - /process-warning@2.2.0: - resolution: {integrity: sha512-/1WZ8+VQjR6avWOgHeEPd7SDQmFQ1B5mC1eRXsCm5TarlNmx/wCsa5GEaxGm05BORRtyG/Ex/3xq3TuRvq57qg==} + /process-warning@3.0.0: + resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==} dev: false /process@0.11.10: @@ -4152,7 +4216,7 @@ packages: /rpc-websockets@7.6.1: resolution: {integrity: sha512-MmRGaJJvxTHSRxYPjJJqcj2zWnCetw7YbYbKlD0Yc7qVw6PsZhRJg1MI3mpWlpBs+4zO+urlNfLl9zLsdOD/gA==} dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.9 eventemitter3: 4.0.7 uuid: 8.3.2 ws: 8.14.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -4308,7 +4372,7 @@ packages: resolution: {integrity: sha512-TS6vYoO/9YJZng7oiLOVyuz8V7yLow5Hp4SLYWW71XM3702v+z9f1fvUBKudRfa4dfpta4tRNufApSiBIALxJQ==} engines: {node: '>= 10'} dependencies: - '@solana/web3.js': 1.87.6 + '@solana/web3.js': 1.89.1 bs58: 4.0.1 optionalDependencies: solana-bankrun-darwin-arm64: 0.2.0 @@ -4473,6 +4537,14 @@ packages: engines: {node: '>= 0.4'} dev: true + /synckit@0.8.8: + resolution: {integrity: sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==} + engines: {node: ^14.18.0 || >=16.0.0} + dependencies: + '@pkgr/core': 0.1.1 + tslib: 2.6.2 + dev: true + /tar-fs@2.1.1: resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} dependencies: @@ -4552,9 +4624,9 @@ packages: engines: {node: '>=8'} dev: true - /ts-jest@29.1.1(@babel/core@7.23.2)(jest@29.7.0)(typescript@4.9.5): - resolution: {integrity: sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + /ts-jest@29.1.2(@babel/core@7.23.2)(jest@29.7.0)(typescript@4.9.5): + resolution: {integrity: sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==} + engines: {node: ^16.10.0 || ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@babel/core': '>=7.0.0-beta.0 <8' @@ -4576,7 +4648,7 @@ packages: '@babel/core': 7.23.2 bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@18.11.9)(ts-node@10.9.1) + jest: 29.7.0(@types/node@18.11.9)(ts-node@10.9.2) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 @@ -4586,8 +4658,8 @@ packages: yargs-parser: 21.1.1 dev: true - /ts-node@10.9.1(@types/node@18.11.9)(typescript@4.9.5): - resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} + /ts-node@10.9.2(@types/node@18.11.9)(typescript@4.9.5): + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true peerDependencies: '@swc/core': '>=1.2.50' @@ -4817,8 +4889,8 @@ packages: /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - /yaml@2.3.3: - resolution: {integrity: sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ==} + /yaml@2.3.4: + resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==} engines: {node: '>= 14'} dev: false diff --git a/programs/validator-bonds/Cargo.toml b/programs/validator-bonds/Cargo.toml index 70cd8fea..7d0ff8f1 100644 --- a/programs/validator-bonds/Cargo.toml +++ b/programs/validator-bonds/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "validator-bonds" -version = "0.1.0" +version = "1.1.0" description = "Marinade validator bonds program protecting validator behavior" edition = "2021" license = "Apache-2.0" @@ -25,4 +25,3 @@ anchor-lang = { workspace = true } anchor-spl = { workspace = true } solana-security-txt = { workspace = true } default-env = { workspace = true } - diff --git a/programs/validator-bonds/src/checks.rs b/programs/validator-bonds/src/checks.rs index 2290fb8e..8645ea18 100644 --- a/programs/validator-bonds/src/checks.rs +++ b/programs/validator-bonds/src/checks.rs @@ -3,6 +3,7 @@ use crate::state::bond::Bond; use anchor_lang::prelude::*; use anchor_lang::prelude::{msg, Pubkey}; use anchor_lang::require_keys_eq; +use anchor_lang::solana_program::stake::program::ID as stake_program_id; use anchor_lang::solana_program::stake::state::{Delegation, Meta, Stake}; use anchor_lang::solana_program::stake_history::{Epoch, StakeHistoryEntry}; use anchor_lang::solana_program::vote::program::id as vote_program_id; @@ -10,12 +11,12 @@ use anchor_spl::stake::StakeAccount; use std::ops::Deref; /// Verification the account is owned by vote program + matching validator identity -pub fn check_validator_vote_account_validator_identity( - validator_vote_account: &UncheckedAccount, +pub fn check_vote_account_validator_identity( + vote_account: &UncheckedAccount, expected_validator_identity: &Pubkey, ) -> Result<()> { // https://github.com/solana-labs/solana/blob/v1.17.10/sdk/program/src/vote/state/mod.rs#L287 - let node_pubkey = get_validator_vote_account_validator_identity(validator_vote_account)?; + let node_pubkey = get_validator_vote_account_validator_identity(vote_account)?; require_keys_eq!( *expected_validator_identity, node_pubkey, @@ -25,21 +26,22 @@ pub fn check_validator_vote_account_validator_identity( } pub fn get_validator_vote_account_validator_identity( - validator_vote_account: &UncheckedAccount, + vote_account: &UncheckedAccount, ) -> Result { - get_from_validator_vote_account(validator_vote_account, 4, "validator identity") + get_from_validator_vote_account(vote_account, 4, "validator identity") } fn get_from_validator_vote_account( - validator_vote_account: &UncheckedAccount, + vote_account: &UncheckedAccount, byte_position: usize, pubkey_name: &str, ) -> Result { - require!( - validator_vote_account.owner == &vote_program_id(), + require_keys_eq!( + *vote_account.owner, + vote_program_id(), ErrorCode::InvalidVoteAccountProgramId ); - let validator_vote_data = &validator_vote_account.data.borrow()[..]; + let validator_vote_data = &vote_account.data.borrow()[..]; // let's find position of the pubkey within the vote state account data // https://github.com/solana-labs/solana/pull/30515 // https://github.com/solana-labs/solana/blob/v1.17.10/sdk/program/src/vote/state/mod.rs#L290 @@ -47,7 +49,7 @@ fn get_from_validator_vote_account( msg!( "Cannot get {} from vote account {} data", pubkey_name, - validator_vote_account.key, + vote_account.key, ); return Err(ErrorCode::FailedToDeserializeVoteAccount.into()); } @@ -57,11 +59,11 @@ fn get_from_validator_vote_account( msg!( "Cannot get {} from vote account {} data: {:?}", pubkey_name, - validator_vote_account.key, + vote_account.key, err ); error!(ErrorCode::FailedToDeserializeVoteAccount) - .with_values(("validator_vote_account", validator_vote_account.key())) + .with_values(("vote_account", vote_account.key())) })?; Ok(Pubkey::from(pubkey_slice)) } @@ -70,26 +72,26 @@ fn get_from_validator_vote_account( pub fn check_bond_change_permitted( authority: &Pubkey, bond_account: &Bond, - validator_vote_account: &UncheckedAccount, + vote_account: &UncheckedAccount, ) -> bool { // TODO: is possible to sign with default Pubkey? Should be Pubkey::default() defined as disabled bound authority? + // TODO: consider use the map_or_else to return the error if authority == &bond_account.authority.key() { true } else { - check_validator_vote_account_validator_identity(validator_vote_account, authority) - .map_or(false, |_| true) + check_vote_account_validator_identity(vote_account, authority).map_or(false, |_| true) } } /// Check if the stake account is delegated to the right validator pub fn check_stake_valid_delegation( stake_account: &StakeAccount, - validator_vote_account: &Pubkey, + vote_account: &Pubkey, ) -> Result { if let Some(delegation) = stake_account.delegation() { require_keys_eq!( delegation.voter_pubkey, - *validator_vote_account, + *vote_account, ErrorCode::BondStakeWrongDelegation ); Ok(delegation) @@ -177,6 +179,19 @@ pub fn check_stake_exist_and_fully_activated( } } +pub fn deserialize_stake_account(account: &UncheckedAccount) -> Result { + require_keys_eq!( + *account.owner, + stake_program_id, + ErrorCode::InvalidStakeAccountProgramId + ); + if account.try_lamports()? == 0 { + return Err(ErrorCode::InvalidStakeAccountState.into()); + } + let stake_state = account.try_borrow_data()?; + StakeAccount::try_deserialize(&mut stake_state.as_ref()) +} + #[cfg(test)] mod tests { use super::*; @@ -203,7 +218,7 @@ mod tests { ); let wrong_owner_account = UncheckedAccount::try_from(&account); assert_eq!( - check_validator_vote_account_validator_identity( + check_vote_account_validator_identity( &wrong_owner_account, &vote_init.authorized_voter, ), @@ -223,14 +238,13 @@ mod tests { ); let unchecked_account = UncheckedAccount::try_from(&account); - check_validator_vote_account_validator_identity(&unchecked_account, &vote_init.node_pubkey) - .unwrap(); + check_vote_account_validator_identity(&unchecked_account, &vote_init.node_pubkey).unwrap(); assert_eq!( - check_validator_vote_account_validator_identity(&unchecked_account, &Pubkey::default(),), + check_vote_account_validator_identity(&unchecked_account, &Pubkey::default(),), Err(ErrorCode::VoteAccountValidatorIdentityMismatch.into()) ); assert_eq!( - check_validator_vote_account_validator_identity(&unchecked_account, &Pubkey::default(),), + check_vote_account_validator_identity(&unchecked_account, &Pubkey::default(),), Err(ErrorCode::VoteAccountValidatorIdentityMismatch.into()) ); } diff --git a/programs/validator-bonds/src/error.rs b/programs/validator-bonds/src/error.rs index ac729e84..62a5ba47 100644 --- a/programs/validator-bonds/src/error.rs +++ b/programs/validator-bonds/src/error.rs @@ -17,135 +17,144 @@ pub enum ErrorCode { #[msg("Provided vote account is not owned by the validator vote program")] InvalidVoteAccountProgramId, // 6004 0x1774 + #[msg("Provided vote account is trouble to deserialize")] + InvalidStakeAccountState, // 6005 0x1775 + + #[msg("Provided stake account is not owned by the stake account program")] + InvalidStakeAccountProgramId, // 6006 0x1776 + #[msg("Fail to create account address for Settlement")] - InvalidSettlementAddress, // 6005 0x1775 + InvalidSettlementAddress, // 6007 0x1777 #[msg("Fail to create PDA address for Settlement Authority")] - InvalidSettlementAuthorityAddress, // 6006 0x1776 + InvalidSettlementAuthorityAddress, // 6008 0x1778 #[msg("Fail to create PDA address for Bonds Withdrawer Authority")] - InvalidBondsWithdrawerAuthorityAddress, // 6007 0x1777 + InvalidBondsWithdrawerAuthorityAddress, // 6009 0x1779 #[msg("Fail to create program address for SettlementClaim")] - InvalidSettlementClaimAddress, // 6008 0x1778 + InvalidSettlementClaimAddress, // 6010 0x177a #[msg("Fail to create program address for Bond")] - InvalidBondAddress, // 6009 0x1779 + InvalidBondAddress, // 6011 0x177b #[msg("Wrong withdrawer authority of the stake account")] - WrongStakeAccountWithdrawer, // 6010 0x177a + WrongStakeAccountWithdrawer, // 6012 0x177c #[msg("Fail to create program address for WithdrawRequest")] - InvalidWithdrawRequestAddress, // 6011 0x177b + InvalidWithdrawRequestAddress, // 6013 0x177d + // note: not used #[msg("Value of hundredth basis points is too big")] - HundredthBasisPointsOverflow, // 6012 0x177c + HundredthBasisPointsOverflow, // 6014 0x177e + // note: not used #[msg("Hundredth basis points calculation failure")] - HundredthBasisPointsCalculation, // 6013 0x177d + HundredthBasisPointsCalculation, // 6015 0x177f + // note: not used #[msg("Hundredth basis points failure to parse the value")] - HundredthBasisPointsParse, // 6014 0x177e + HundredthBasisPointsParse, // 6016 0x1780 #[msg("Cannot deserialize validator vote account data")] - FailedToDeserializeVoteAccount, // 6015 0x177f + FailedToDeserializeVoteAccount, // 6017 0x1781 #[msg("Wrong authority for changing the validator bond account")] - BondChangeNotPermitted, // 6016 0x1780 + BondChangeNotPermitted, // 6018 0x1782 #[msg("Provided stake cannot be used for bonds, it's not delegated")] - StakeNotDelegated, // 6017 0x1781 + StakeNotDelegated, // 6019 0x1783 #[msg("Provided stake is delegated to a wrong validator vote account")] - BondStakeWrongDelegation, // 6018 0x1782 + BondStakeWrongDelegation, // 6020 0x1784 #[msg("Withdraw request has not elapsed the epoch lockup period yet")] - WithdrawRequestNotReady, // 6019 0x1783 + WithdrawRequestNotReady, // 6021 0x1785 #[msg("Settlement has not expired yet")] - SettlementNotExpired, // 6020 0x1784 + SettlementNotExpired, // 6022 0x1786 #[msg("Settlement has already expired")] - SettlementExpired, // 6021 0x1785 + SettlementExpired, // 6023 0x1787 #[msg("Stake is not initialized")] - UninitializedStake, // 6022 0x1786 + UninitializedStake, // 6024 0x1788 #[msg("Stake account is not fully activated")] - NoStakeOrNotFullyActivated, // 6023 0x1787 + NoStakeOrNotFullyActivated, // 6025 0x1789 #[msg("Instruction context was provided with unexpected set of remaining accounts")] - UnexpectedRemainingAccounts, // 6024 0x1788 + UnexpectedRemainingAccounts, // 6026 0x178a - #[msg("Closing SettlementClaim requires the settlement being closed")] - SettlementNotClosed, // 6025 0x1789 + #[msg("Required settlement to be closed")] + SettlementNotClosed, // 6027 0x178b #[msg("Provided stake account has been already funded to a settlement")] - StakeAccountIsFundedToSettlement, // 6026 0x178a + StakeAccountIsFundedToSettlement, // 6028 0x178c #[msg("Settlement claim proof failed")] - ClaimSettlementProofFailed, // 6027 0x178b + ClaimSettlementProofFailed, // 6029 0x178d #[msg("Provided stake account is locked-up")] - StakeLockedUp, // 6028 0x178c + StakeLockedUp, // 6030 0x178e #[msg("Stake account is not big enough to be split")] - StakeAccountNotBigEnoughToSplit, // 6029 0x178d + StakeAccountNotBigEnoughToSplit, // 6031 0x178f #[msg("Claiming bigger amount than the max total claim")] - ClaimAmountExceedsMaxTotalClaim, // 6030 0x178e + ClaimAmountExceedsMaxTotalClaim, // 6032 0x1790 #[msg("Claim exceeded number of claimable nodes in the merkle tree")] - ClaimCountExceedsMaxNumNodes, // 6031 0x178f + ClaimCountExceedsMaxMerkleNodes, // 6033 0x1791 #[msg("Empty merkle tree, nothing to be claimed")] - EmptySettlementMerkleTree, // 6032 0x1790 + EmptySettlementMerkleTree, // 6034 0x1792 #[msg("Provided stake account has not enough lamports to cover the claim")] - ClaimingStakeAccountLamportsInsufficient, // 6033 0x1791 + ClaimingStakeAccountLamportsInsufficient, // 6035 0x1793 - #[msg("Provided stake account is not funded under a settlement")] - StakeAccountNotFunded, // 6034 0x1792 + #[msg("Provided stake account is not funded under the settlement")] + StakeAccountNotFundedToSettlement, // 6036 0x1794 #[msg("Validator vote account does not match to provided validator identity signature")] - VoteAccountValidatorIdentityMismatch, // 6035 0x1793 + VoteAccountValidatorIdentityMismatch, // 6037 0x1795 #[msg("Bond vote account address does not match with the provided validator vote account")] - VoteAccountMismatch, // 6036 0x1794 + VoteAccountMismatch, // 6038 0x1796 #[msg("Bond config address does not match with the provided config account")] - ConfigAccountMismatch, // 6037 0x1795 + ConfigAccountMismatch, // 6039 0x1797 #[msg("Withdraw request vote account address does not match with the provided validator vote account")] - WithdrawRequestVoteAccountMismatch, // 6038 0x1796 + WithdrawRequestVoteAccountMismatch, // 6040 0x1798 #[msg("Bond account address does not match with the stored one")] - BondAccountMismatch, // 6039 0x1797 + BondAccountMismatch, // 6041 0x1799 #[msg("Settlement account address does not match with the stored one")] - SettlementAccountMismatch, // 6040 0x1798 + SettlementAccountMismatch, // 6042 0x179a #[msg("Rent collector address does not match permitted rent collector")] - RentCollectorMismatch, // 6041 0x1799 + RentCollectorMismatch, // 6043 0x179b #[msg("Stake account's staker does not match with the provided authority")] - StakerAuthorityMismatch, // 6042 0x179a + StakerAuthorityMismatch, // 6044 0x179c #[msg("One or both stake authorities does not belong to bonds program")] - NonBondStakeAuthorities, // 6043 0x179b + NonBondStakeAuthorities, // 6045 0x179d - #[msg("Settlement stake account authority does not match with the provided stake account authority")] - SettlementAuthorityMismatch, // 6044 0x179c + #[msg("Stake account staker authority mismatches with the settlement authority")] + SettlementAuthorityMismatch, // 6046 0x179e #[msg("Delegation of provided stake account mismatches")] - StakeDelegationMismatch, // 6045 0x179d + StakeDelegationMismatch, // 6047 0x179f #[msg("Too small non-withdrawn withdraw request amount, cancel and init new one")] - WithdrawRequestAmountTooSmall, // 6046 0x179e + WithdrawRequestAmountTooSmall, // 6048 0x17a0 #[msg("Withdraw request has been already fulfilled")] - WithdrawRequestAlreadyFulfilled, // 6047 0x179f + WithdrawRequestAlreadyFulfilled, // 6049 0x17a1 - #[msg("Not yet implemented")] - NotYetImplemented, // 6048 0x17a0 + #[msg("Claim settlement merkle tree node mismatch")] + ClaimSettlementMerkleTreeNodeMismatch, // 6050 0x17a2 } diff --git a/programs/validator-bonds/src/events/bond.rs b/programs/validator-bonds/src/events/bond.rs index 89952098..03fb66e7 100644 --- a/programs/validator-bonds/src/events/bond.rs +++ b/programs/validator-bonds/src/events/bond.rs @@ -1,36 +1,35 @@ -use crate::events::{HundrethBasisPointChange, PubkeyValueChange}; -use crate::utils::basis_points::HundredthBasisPoint; +use crate::events::{PubkeyValueChange, U64ValueChange}; use anchor_lang::prelude::*; #[event] pub struct InitBondEvent { pub config_address: Pubkey, - pub validator_vote_account: Pubkey, + pub vote_account: Pubkey, pub validator_identity: Pubkey, pub authority: Pubkey, - pub revenue_share: HundredthBasisPoint, + pub cpmpe: u64, pub bond_bump: u8, } #[event] pub struct ConfigureBondEvent { pub bond_authority: Option, - pub revenue_share: Option, + pub cpmpe: Option, } #[event] pub struct CloseBondEvent { pub config_address: Pubkey, - pub validator_vote_account: Pubkey, + pub vote_account: Pubkey, pub authority: Pubkey, - pub revenue_share: HundredthBasisPoint, + pub cpmpe: u64, pub bump: u8, } #[event] pub struct FundBondEvent { pub bond: Pubkey, - pub validator_vote: Pubkey, + pub vote_account: Pubkey, pub stake_account: Pubkey, pub stake_authority_signer: Pubkey, pub deposited_amount: u64, diff --git a/programs/validator-bonds/src/events/mod.rs b/programs/validator-bonds/src/events/mod.rs index 7dd0e76f..659a43a1 100644 --- a/programs/validator-bonds/src/events/mod.rs +++ b/programs/validator-bonds/src/events/mod.rs @@ -8,8 +8,6 @@ pub mod settlement_claim; pub mod stake; pub mod withdraw; -use crate::utils::basis_points::HundredthBasisPoint; - #[derive(Clone, AnchorSerialize, AnchorDeserialize)] pub struct PubkeyValueChange { pub old: Pubkey, @@ -22,12 +20,6 @@ pub struct U64ValueChange { pub new: u64, } -#[derive(Clone, AnchorSerialize, AnchorDeserialize)] -pub struct HundrethBasisPointChange { - pub old: HundredthBasisPoint, - pub new: HundredthBasisPoint, -} - #[derive(Clone, AnchorSerialize, AnchorDeserialize)] pub struct DelegationInfo { /// to whom the stake is delegated diff --git a/programs/validator-bonds/src/events/settlement.rs b/programs/validator-bonds/src/events/settlement.rs index 306a2da2..cb0a330a 100644 --- a/programs/validator-bonds/src/events/settlement.rs +++ b/programs/validator-bonds/src/events/settlement.rs @@ -6,11 +6,11 @@ use anchor_lang::prelude::*; pub struct InitSettlementEvent { pub bond: Pubkey, pub vote_account: Pubkey, - pub settlement_authority: Pubkey, + pub authority: Pubkey, pub merkle_root: [u8; 32], pub max_total_claim: u64, - pub max_num_nodes: u64, - pub epoch: u64, + pub max_merkle_nodes: u64, + pub epoch_created_at: u64, pub rent_collector: Pubkey, pub bumps: Bumps, } @@ -21,11 +21,12 @@ pub struct CloseSettlementEvent { pub settlement: Pubkey, pub merkle_root: [u8; 32], pub max_total_claim: u64, - pub max_num_nodes: u64, - pub total_funded: u64, - pub total_funds_claimed: u64, - pub num_nodes_claimed: u64, + pub max_merkle_nodes: u64, + pub lamports_funded: u64, + pub lamports_claimed: u64, + pub merkle_nodes_claimed: u64, pub split_rent_collector: Option, + pub split_rent_refund_account: Pubkey, pub rent_collector: Pubkey, pub expiration_epoch: u64, pub current_epoch: u64, @@ -36,9 +37,9 @@ pub struct FundSettlementEvent { pub bond: Pubkey, pub vote_account: Pubkey, pub settlement: Pubkey, - pub total_funded: u64, - pub total_funds_claimed: u64, - pub num_nodes_claimed: u64, + pub lamports_funded: u64, + pub lamports_claimed: u64, + pub merkle_nodes_claimed: u64, pub stake_account: Pubkey, pub split_stake_account: Option, pub split_rent_collector: Option, diff --git a/programs/validator-bonds/src/events/settlement_claim.rs b/programs/validator-bonds/src/events/settlement_claim.rs index 402e41fe..9df711f9 100644 --- a/programs/validator-bonds/src/events/settlement_claim.rs +++ b/programs/validator-bonds/src/events/settlement_claim.rs @@ -4,10 +4,11 @@ use anchor_lang::prelude::*; pub struct ClaimSettlementEvent { pub settlement: Pubkey, pub settlement_claim: Pubkey, - pub staker_authority: Pubkey, - pub withdrawer_authority: Pubkey, + pub settlement_lamports_claimed: u64, + pub settlement_merkle_nodes_claimed: u64, + pub withdraw_authority: Pubkey, pub vote_account: Pubkey, - pub claim: u64, + pub amount: u64, pub rent_collector: Pubkey, pub bump: u8, } diff --git a/programs/validator-bonds/src/events/stake.rs b/programs/validator-bonds/src/events/stake.rs index e4247014..81a884c6 100644 --- a/programs/validator-bonds/src/events/stake.rs +++ b/programs/validator-bonds/src/events/stake.rs @@ -17,7 +17,6 @@ pub struct ResetEvent { pub bond: Pubkey, pub settlement: Pubkey, pub stake_account: Pubkey, - pub validator_vote_acount: Pubkey, + pub vote_account: Pubkey, pub settlement_authority: Pubkey, - pub bonds_withdrawer_authority: Pubkey, } diff --git a/programs/validator-bonds/src/events/withdraw.rs b/programs/validator-bonds/src/events/withdraw.rs index 87344825..8c31d2ce 100644 --- a/programs/validator-bonds/src/events/withdraw.rs +++ b/programs/validator-bonds/src/events/withdraw.rs @@ -5,7 +5,7 @@ use anchor_lang::prelude::*; pub struct InitWithdrawRequestEvent { pub withdraw_request: Pubkey, pub bond: Pubkey, - pub validator_vote_account: Pubkey, + pub vote_account: Pubkey, pub bump: u8, pub epoch: u64, pub requested_amount: u64, @@ -24,7 +24,7 @@ pub struct CancelWithdrawRequestEvent { pub struct ClaimWithdrawRequestEvent { pub withdraw_request: Pubkey, pub bond: Pubkey, - pub validator_vote_account: Pubkey, + pub vote_account: Pubkey, pub stake_account: Pubkey, pub split_stake: Option, pub new_stake_account_owner: Pubkey, diff --git a/programs/validator-bonds/src/instructions/bond/configure_bond.rs b/programs/validator-bonds/src/instructions/bond/configure_bond.rs index d85446b2..7ed8054a 100644 --- a/programs/validator-bonds/src/instructions/bond/configure_bond.rs +++ b/programs/validator-bonds/src/instructions/bond/configure_bond.rs @@ -1,14 +1,14 @@ use crate::checks::check_bond_change_permitted; use crate::error::ErrorCode; -use crate::events::{bond::ConfigureBondEvent, HundrethBasisPointChange, PubkeyValueChange}; +use crate::events::{bond::ConfigureBondEvent, PubkeyValueChange, U64ValueChange}; use crate::state::bond::Bond; -use crate::utils::basis_points::HundredthBasisPoint; use anchor_lang::prelude::*; +use anchor_lang::solana_program::vote::program::ID as vote_program_id; #[derive(AnchorDeserialize, AnchorSerialize)] pub struct ConfigureBondArgs { pub bond_authority: Option, - pub revenue_share: Option, + pub cpmpe: Option, } /// Change parameters of validator bond account @@ -16,11 +16,11 @@ pub struct ConfigureBondArgs { pub struct ConfigureBond<'info> { #[account( mut, - has_one = validator_vote_account @ ErrorCode::VoteAccountMismatch, + has_one = vote_account @ ErrorCode::VoteAccountMismatch, seeds = [ b"bond_account", bond.config.as_ref(), - validator_vote_account.key().as_ref(), + vote_account.key().as_ref(), ], bump = bond.bump, )] @@ -31,8 +31,10 @@ pub struct ConfigureBond<'info> { authority: Signer<'info>, /// CHECK: check&deserialize the vote account in the code - #[account()] - validator_vote_account: UncheckedAccount<'info>, + #[account( + owner = vote_program_id @ ErrorCode::InvalidVoteAccountProgramId, + )] + vote_account: UncheckedAccount<'info>, } impl<'info> ConfigureBond<'info> { @@ -40,15 +42,11 @@ impl<'info> ConfigureBond<'info> { &mut self, ConfigureBondArgs { bond_authority, - revenue_share, + cpmpe, }: ConfigureBondArgs, ) -> Result<()> { require!( - check_bond_change_permitted( - &self.authority.key(), - &self.bond, - &self.validator_vote_account - ), + check_bond_change_permitted(&self.authority.key(), &self.bond, &self.vote_account), ErrorCode::BondChangeNotPermitted ); @@ -60,18 +58,21 @@ impl<'info> ConfigureBond<'info> { new: authority, } }); - let revenue_share_change = match revenue_share { - Some(revenue) => { - let old = self.bond.revenue_share; - self.bond.revenue_share = revenue.check()?; - Some(HundrethBasisPointChange { old, new: revenue }) + let cpmpe_change = match cpmpe { + Some(new_cpmpe) => { + let old = self.bond.cpmpe; + self.bond.cpmpe = new_cpmpe; + Some(U64ValueChange { + old, + new: new_cpmpe, + }) } None => None, }; emit!(ConfigureBondEvent { bond_authority: bond_authority_change, - revenue_share: revenue_share_change, + cpmpe: cpmpe_change, }); Ok(()) diff --git a/programs/validator-bonds/src/instructions/bond/fund_bond.rs b/programs/validator-bonds/src/instructions/bond/fund_bond.rs index f103bb6e..e0eaf715 100644 --- a/programs/validator-bonds/src/instructions/bond/fund_bond.rs +++ b/programs/validator-bonds/src/instructions/bond/fund_bond.rs @@ -23,7 +23,7 @@ pub struct FundBond<'info> { seeds = [ b"bond_account", config.key().as_ref(), - bond.validator_vote_account.as_ref() + bond.vote_account.as_ref() ], bump = bond.bump, )] @@ -85,7 +85,7 @@ impl<'info> FundBond<'info> { self.clock.epoch, &self.stake_history, )?; - check_stake_valid_delegation(&self.stake_account, &self.bond.validator_vote_account)?; + check_stake_valid_delegation(&self.stake_account, &self.bond.vote_account)?; authorize( CpiContext::new( @@ -118,7 +118,7 @@ impl<'info> FundBond<'info> { emit!(FundBondEvent { bond: self.bond.key(), - validator_vote: self.bond.validator_vote_account.key(), + vote_account: self.bond.vote_account.key(), stake_account: self.stake_account.key(), stake_authority_signer: self.stake_authority.key(), deposited_amount: self.stake_account.get_lamports(), diff --git a/programs/validator-bonds/src/instructions/bond/init_bond.rs b/programs/validator-bonds/src/instructions/bond/init_bond.rs index e6543db7..22dcc6e7 100644 --- a/programs/validator-bonds/src/instructions/bond/init_bond.rs +++ b/programs/validator-bonds/src/instructions/bond/init_bond.rs @@ -1,12 +1,10 @@ use crate::checks::{ - check_validator_vote_account_validator_identity, get_validator_vote_account_validator_identity, + check_vote_account_validator_identity, get_validator_vote_account_validator_identity, }; use crate::error::ErrorCode; use crate::events::bond::InitBondEvent; use crate::state::bond::Bond; use crate::state::config::Config; -use crate::state::Reserved150; -use crate::utils::basis_points::HundredthBasisPoint; use anchor_lang::prelude::*; use anchor_lang::solana_program::system_program; use anchor_lang::solana_program::vote::program::ID as vote_program_id; @@ -14,7 +12,7 @@ use anchor_lang::solana_program::vote::program::ID as vote_program_id; #[derive(AnchorDeserialize, AnchorSerialize)] pub struct InitBondArgs { pub bond_authority: Pubkey, - pub revenue_share: HundredthBasisPoint, + pub cpmpe: u64, } /// Creates new validator bond account based on the validator vote address @@ -28,7 +26,7 @@ pub struct InitBond<'info> { #[account( owner = vote_program_id @ ErrorCode::InvalidVoteAccountProgramId, )] - validator_vote_account: UncheckedAccount<'info>, + vote_account: UncheckedAccount<'info>, /// when validator identity signs the instruction then configuration arguments are applied /// otherwise it's a permission-less operation that uses default init bond setup @@ -42,7 +40,7 @@ pub struct InitBond<'info> { seeds = [ b"bond_account", config.key().as_ref(), - validator_vote_account.key().as_ref() + vote_account.key().as_ref() ], bump, )] @@ -63,42 +61,42 @@ impl<'info> InitBond<'info> { &mut self, InitBondArgs { bond_authority, - revenue_share, + cpmpe, }: InitBondArgs, bond_bump: u8, ) -> Result<()> { - let mut revenue_share = revenue_share; + let mut cpmpe = cpmpe; let mut bond_authority = bond_authority; let validator_identity = if let Some(validator_identity_info) = &self.validator_identity { // permission-ed: verification of validator identity as a signer, config of bond account possible - check_validator_vote_account_validator_identity( - &self.validator_vote_account, + check_vote_account_validator_identity( + &self.vote_account, &validator_identity_info.key(), )?; validator_identity_info.key() } else { // permission-less: not possible to configure bond account - revenue_share = HundredthBasisPoint { hundredth_bps: 0 }; + cpmpe = 0; let validator_identity = - get_validator_vote_account_validator_identity(&self.validator_vote_account)?; + get_validator_vote_account_validator_identity(&self.vote_account)?; bond_authority = validator_identity; validator_identity }; self.bond.set_inner(Bond { config: self.config.key(), - validator_vote_account: self.validator_vote_account.key(), + vote_account: self.vote_account.key(), authority: bond_authority, - revenue_share: revenue_share.check()?, + cpmpe, bump: bond_bump, - reserved: Reserved150::default(), + ..Bond::default() }); emit!(InitBondEvent { config_address: self.bond.config, - validator_vote_account: self.bond.validator_vote_account, + vote_account: self.bond.vote_account, validator_identity, authority: self.bond.authority, - revenue_share: self.bond.revenue_share, + cpmpe: self.bond.cpmpe, bond_bump: self.bond.bump, }); diff --git a/programs/validator-bonds/src/instructions/config/migrate_bond_cpmpe.rs b/programs/validator-bonds/src/instructions/config/migrate_bond_cpmpe.rs new file mode 100644 index 00000000..e627459a --- /dev/null +++ b/programs/validator-bonds/src/instructions/config/migrate_bond_cpmpe.rs @@ -0,0 +1,82 @@ +use crate::state::bond::{find_bond_address, Bond, BondWithRevenueShare}; +use crate::state::config::Config; +use anchor_lang::prelude::*; + +/// Migrate Bond account to cpmpe +#[derive(Accounts)] +pub struct MigrateBondCpmpe<'info> { + /// config root account that will be configured + #[account( + constraint = config.admin_authority == admin_authority.key() || config.operator_authority == admin_authority.key(), + )] + config: Account<'info, Config>, + + /// CHECK: deserialization check in code + #[account(mut)] + bond: UncheckedAccount<'info>, + + #[account()] + admin_authority: Signer<'info>, +} + +impl<'info> MigrateBondCpmpe<'info> { + pub fn process(&mut self) -> Result<()> { + let new_bond_data: Vec; + { + let original_bond_account_data = self.bond.try_borrow_data()?; + let mut original_bond_account_data_slice: &[u8] = &original_bond_account_data; + msg!( + "original_bond_account_data: {:?}", + original_bond_account_data + ); + let original_bond = BondWithRevenueShare::try_deserialize_unchecked( + &mut original_bond_account_data_slice, + )?; + + let (bond_address, bond_bump) = + find_bond_address(&self.config.key(), &original_bond.vote_account); + msg!( + "bond_address: {:?}, before check - config: {}, vote account: {}, bump: {}", + bond_address, + self.config.key(), + original_bond.vote_account, + original_bond.bump + ); + require_eq!(bond_address, self.bond.key()); + require_eq!(original_bond.config, self.config.key()); + require_eq!(original_bond.bump, bond_bump); + + let new_bond = Bond { + config: original_bond.config, + vote_account: original_bond.vote_account, + authority: original_bond.authority, + cpmpe: 0, + bump: original_bond.bump, + ..Bond::default() + }; + // NOTE: try to vec does not add the discriminator stuff + new_bond_data = new_bond.try_to_vec()?; + + msg!( + "Migrating bond account to cpmpe from '{:?}' to '{:?}'", + self.bond, + new_bond + ); + } + + let bond_account_info = self.bond.to_account_info(); + let mut bond_account_data = bond_account_info.try_borrow_mut_data()?; + if new_bond_data.len() + 8 > bond_account_data.len() { + msg!( + "New bond data is larger than the old one, old : {}, new : {}", + bond_account_data.len(), + new_bond_data.len() + ); + return Err(ErrorCode::InvalidProgramId.into()); + } + msg!("new_bond_data: {:?}", new_bond_data); + bond_account_data[8..new_bond_data.len() + 8].copy_from_slice(&new_bond_data[..]); + + Ok(()) + } +} diff --git a/programs/validator-bonds/src/instructions/config/mod.rs b/programs/validator-bonds/src/instructions/config/mod.rs index 1fc27510..95440c45 100644 --- a/programs/validator-bonds/src/instructions/config/mod.rs +++ b/programs/validator-bonds/src/instructions/config/mod.rs @@ -1,5 +1,7 @@ pub mod configure_config; pub mod init_config; +pub mod migrate_bond_cpmpe; pub use configure_config::*; pub use init_config::*; +pub use migrate_bond_cpmpe::*; diff --git a/programs/validator-bonds/src/instructions/settlement/claim_settlement.rs b/programs/validator-bonds/src/instructions/settlement/claim_settlement.rs index 54edaaa4..33cf907c 100644 --- a/programs/validator-bonds/src/instructions/settlement/claim_settlement.rs +++ b/programs/validator-bonds/src/instructions/settlement/claim_settlement.rs @@ -1,5 +1,6 @@ use crate::checks::{ - check_stake_is_initialized_with_withdrawer_authority, check_stake_valid_delegation, + check_stake_is_initialized_with_withdrawer_authority, check_stake_is_not_locked, + check_stake_valid_delegation, }; use crate::constants::BONDS_AUTHORITY_SEED; use crate::error::ErrorCode; @@ -9,7 +10,7 @@ use crate::state::config::Config; use crate::state::settlement::Settlement; use crate::state::settlement_claim::SettlementClaim; use crate::state::Reserved150; -use crate::utils::{merkle_proof, minimal_size_stake_account}; +use crate::utils::{merkle_proof, minimal_size_stake_account, TreeNode}; use anchor_lang::prelude::*; use anchor_lang::solana_program::sysvar::stake_history; use anchor_lang::system_program::ID as system_program_id; @@ -17,13 +18,8 @@ use anchor_spl::stake::{withdraw, Stake, StakeAccount, Withdraw}; #[derive(AnchorDeserialize, AnchorSerialize)] pub struct ClaimSettlementArgs { - pub amount: u64, pub proof: Vec<[u8; 32]>, - // staker authority - pub staker: Pubkey, - /// claim holder, withdrawer_authority - pub withdrawer: Pubkey, - pub vote_account: Pubkey, + /// claim amount; merkle root verification pub claim: u64, } @@ -36,11 +32,10 @@ pub struct ClaimSettlement<'info> { #[account( has_one = config @ ErrorCode::ConfigAccountMismatch, - constraint = bond.validator_vote_account == params.vote_account @ ErrorCode::VoteAccountMismatch, seeds = [ b"bond_account", config.key().as_ref(), - params.vote_account.as_ref(), + bond.vote_account.as_ref(), ], bump = bond.bump, )] @@ -60,7 +55,11 @@ pub struct ClaimSettlement<'info> { )] settlement: Account<'info, Settlement>, + // TODO: verify in test that the claim account is created with the right bump and cannot be created with other bump + // TODO: verify that understanding of the TreeNode values is correct /// deduplication, one amount cannot be claimed twice + // INFO: IDL generation generates WARNING: unexpected seed category for var: SeedPath("merkle_proof :: TreeNode { stake_authority : bonds_withdrawer_authority", []) + // https://github.com/coral-xyz/anchor/issues/1550 #[account( init, payer = rent_payer, @@ -68,10 +67,12 @@ pub struct ClaimSettlement<'info> { seeds = [ b"claim_account", settlement.key().as_ref(), - params.staker.as_ref(), - params.withdrawer.as_ref(), - params.vote_account.as_ref(), - params.claim.to_le_bytes().as_ref(), + TreeNode { + stake_authority: bonds_withdrawer_authority.key().to_string(), + withdraw_authority: withdraw_authority.key().to_string(), + vote_account: bond.vote_account.key().to_string(), + claim: params.claim, + }.hash().to_bytes().as_ref(), ], bump, )] @@ -83,17 +84,14 @@ pub struct ClaimSettlement<'info> { /// CHECK: verification within merkle proof /// account that will receive the funds on this claim - #[account( - mut, - constraint = params.withdrawer == withdrawer_authority.key(), - )] - withdrawer_authority: UncheckedAccount<'info>, + #[account(mut)] + withdraw_authority: UncheckedAccount<'info>, /// CHECK: PDA /// authority that manages (owns == being withdrawer authority) all stakes account under the bonds program #[account( seeds = [ - b"bonds_authority", + b"bonds_authority", config.key().as_ref(), ], bump = config.bonds_withdrawer_authority_bump @@ -122,39 +120,48 @@ pub struct ClaimSettlement<'info> { impl<'info> ClaimSettlement<'info> { pub fn process( &mut self, - ClaimSettlementArgs { - amount, - proof, - staker: staker_authority, - withdrawer: withdrawer_authority, - vote_account, - claim, - }: ClaimSettlementArgs, + ClaimSettlementArgs { proof, claim }: ClaimSettlementArgs, settlement_claim_bump: u8, ) -> Result<()> { - require!(true == false, ErrorCode::NotYetImplemented); - - if self.settlement.total_funds_claimed + amount > self.settlement.max_total_claim { + // TODO: let's check with Roman what's a better way + // let tree_node_calculated = TreeNode { + // stake_authority: self.bonds_withdrawer_authority.key().to_string(), + // withdraw_authority: self.withdraw_authority.key().to_string(), + // vote_account: self.bond.vote_account.key().to_string(), + // claim, + // }; + // if tree_node_calculated.hash().to_bytes() != tree_node { + // return Err(error!(ErrorCode::ClaimSettlementMerkleTreeNodeMismatch) + // .with_account_name("settlement_claim") + // .with_values(( + // "tree_node_calculated.hash().to_bytes() != tree_node", + // format!( + // "{:?}/{:?} != {:?}", + // tree_node_calculated.hash(), + // tree_node_calculated.hash().to_bytes(), + // tree_node + // ), + // ))); + // } + if self.settlement.lamports_claimed + claim > self.settlement.max_total_claim { return Err(error!(ErrorCode::ClaimAmountExceedsMaxTotalClaim) .with_account_name("settlement") .with_values(( - "total_funds_claimed + amount > max_total_claim", + "total_funds_claimed + claim_amount > max_total_claim", format!( "{} + {} <= {}", - self.settlement.total_funds_claimed, - amount, - self.settlement.max_total_claim + self.settlement.lamports_claimed, claim, self.settlement.max_total_claim ), ))); } - if self.settlement.num_nodes_claimed + 1 > self.settlement.max_num_nodes { - return Err(error!(ErrorCode::ClaimCountExceedsMaxNumNodes) + if self.settlement.merkle_nodes_claimed + 1 > self.settlement.max_merkle_nodes { + return Err(error!(ErrorCode::ClaimCountExceedsMaxMerkleNodes) .with_account_name("settlement") .with_values(( - "num_nodes_claimed + 1 > max_num_nodes", + "merkle_nodes_claimed + 1 > max_merkle_nodes", format!( "{} + 1 <= {}", - self.settlement.num_nodes_claimed, self.settlement.max_num_nodes + self.settlement.merkle_nodes_claimed, self.settlement.max_merkle_nodes ), ))); } @@ -165,50 +172,56 @@ impl<'info> ClaimSettlement<'info> { &self.bonds_withdrawer_authority.key(), "stake_account", )?; - // stake account is delegated (deposited by) the bond validator - check_stake_valid_delegation(&self.stake_account, &self.bond.validator_vote_account)?; // provided stake account must be funded; staker == settlement staker authority require_keys_eq!( stake_meta.authorized.staker, - self.settlement.settlement_authority, - ErrorCode::StakeAccountNotFunded, + self.settlement.authority, + ErrorCode::StakeAccountNotFundedToSettlement, ); + // stake account is delegated (deposited by) the bond validator + check_stake_valid_delegation(&self.stake_account, &self.bond.vote_account)?; + // stake account cannot be locked (constraints do not permit a correctly set-up account being locked) + check_stake_is_not_locked(&self.stake_account, &self.clock, "stake_account")?; // provided stake account has to be big enough to cover the claim and still be valid to exist - // it's responsibility of the SDK to merge the stake accounts if needed + // responsibility of the SDK to merge the stake accounts if needed // - the invariant here is that the stake account will be always rent exempt + min size // this has to be ensured by fund_settlement instruction if self.stake_account.get_lamports() - < amount + minimal_size_stake_account(&stake_meta, &self.config) + < claim + minimal_size_stake_account(&stake_meta, &self.config) { return Err(error!(ErrorCode::ClaimingStakeAccountLamportsInsufficient) .with_account_name("stake_account") .with_values(( - "stake_account_lamports < amount + minimal_size_stake_account", + "stake_account_lamports < claim_amount + minimal_size_stake_account", format!( "{} < {} + {}", self.stake_account.get_lamports(), - amount, + claim, minimal_size_stake_account(&stake_meta, &self.config) ), ))); } - let merkle_tree_node = - merkle_proof::tree_node(staker_authority, withdrawer_authority, vote_account, claim); + let merkle_tree_node = merkle_proof::tree_node_leaf_hash( + self.bonds_withdrawer_authority.key(), + self.withdraw_authority.key(), + self.bond.vote_account.key(), + claim, + ); if !merkle_proof::verify(proof, self.settlement.merkle_root, merkle_tree_node) { - msg!("Merkle proof verification failed. Merkle tree node: {:?}, staker_authority: {}, withdrawer_authority: {}, vote_account: {}, claim: {}", - merkle_tree_node, staker_authority, withdrawer_authority, vote_account, claim); + msg!("Merkle proof verification failed. Merkle tree node: {:?}, staker_authority: {}, withdrawer_authority: {}, vote_account: {}, claim_amount: {}", + merkle_tree_node, self.bonds_withdrawer_authority.key(), self.withdraw_authority.key(), self.bond.vote_account.key(), claim); return err!(ErrorCode::ClaimSettlementProofFailed); } self.settlement_claim.set_inner(SettlementClaim { settlement: self.settlement.key(), - staker_authority, - withdrawer_authority, - vote_account, - claim, + stake_authority: self.bonds_withdrawer_authority.key(), + withdraw_authority: self.withdraw_authority.key(), + vote_account: self.bond.vote_account.key(), + amount: claim, bump: settlement_claim_bump, rent_collector: self.rent_payer.key(), reserved: Reserved150::default(), @@ -220,7 +233,7 @@ impl<'info> ClaimSettlement<'info> { Withdraw { stake: self.stake_account.to_account_info(), withdrawer: self.bonds_withdrawer_authority.to_account_info(), - to: self.withdrawer_authority.to_account_info(), + to: self.withdraw_authority.to_account_info(), clock: self.clock.to_account_info(), stake_history: self.stake_history.to_account_info(), }, @@ -230,20 +243,21 @@ impl<'info> ClaimSettlement<'info> { &[self.config.bonds_withdrawer_authority_bump], ]], ), - amount, + claim, None, )?; - self.settlement.total_funds_claimed += amount; - self.settlement.num_nodes_claimed += 1; + self.settlement.lamports_claimed += claim; + self.settlement.merkle_nodes_claimed += 1; emit!(ClaimSettlementEvent { settlement: self.settlement_claim.settlement, settlement_claim: self.settlement_claim.key(), - staker_authority: self.settlement_claim.staker_authority, + settlement_lamports_claimed: self.settlement.lamports_claimed, + settlement_merkle_nodes_claimed: self.settlement.merkle_nodes_claimed, vote_account: self.settlement_claim.vote_account, - withdrawer_authority: self.settlement_claim.withdrawer_authority, - claim: self.settlement_claim.claim, + withdraw_authority: self.settlement_claim.withdraw_authority, + amount: self.settlement_claim.amount, rent_collector: self.settlement_claim.rent_collector, bump: settlement_claim_bump, }); diff --git a/programs/validator-bonds/src/instructions/settlement/close_settlement.rs b/programs/validator-bonds/src/instructions/settlement/close_settlement.rs index 15daf52e..732d4202 100644 --- a/programs/validator-bonds/src/instructions/settlement/close_settlement.rs +++ b/programs/validator-bonds/src/instructions/settlement/close_settlement.rs @@ -1,5 +1,6 @@ use crate::checks::{ check_stake_is_initialized_with_withdrawer_authority, check_stake_valid_delegation, + deserialize_stake_account, }; use crate::constants::BONDS_AUTHORITY_SEED; use crate::error::ErrorCode; @@ -9,7 +10,7 @@ use crate::state::config::Config; use crate::state::settlement::Settlement; use anchor_lang::prelude::*; use anchor_lang::solana_program::sysvar::stake_history; -use anchor_spl::stake::{withdraw, Stake, StakeAccount, Withdraw}; +use anchor_spl::stake::{withdraw, Stake, Withdraw}; /// Closes the settlement account, whoever can close it when the epoch expires #[derive(Accounts)] @@ -22,7 +23,7 @@ pub struct CloseSettlement<'info> { seeds = [ b"bond_account", config.key().as_ref(), - bond.validator_vote_account.as_ref(), + bond.vote_account.as_ref(), ], bump = bond.bump, )] @@ -46,14 +47,6 @@ pub struct CloseSettlement<'info> { )] settlement: Account<'info, Settlement>, - /// CHECK: verified against settlement account - #[account(mut)] - rent_collector: UncheckedAccount<'info>, - - /// CHECK: verified against settlement account - #[account(mut)] - split_rent_collector: UncheckedAccount<'info>, - /// CHECK: PDA #[account( seeds = [ @@ -64,9 +57,20 @@ pub struct CloseSettlement<'info> { )] bonds_withdrawer_authority: UncheckedAccount<'info>, - /// a stake account to be used to return back the split rent exempt fee + /// CHECK: verified at settlement #[account()] + #[account(mut)] + rent_collector: UncheckedAccount<'info>, + + /// CHECK: verified at settlement #[account()] #[account(mut)] - stake_account: Account<'info, StakeAccount>, + split_rent_collector: UncheckedAccount<'info>, + + /// CHECK: deserialization in code only when needed + /// a stake account that was funded to the settlement credited to bond's validator vote account + /// lamports of the stake accounts are used to pay back rent exempt of the split_stake_account + /// that can be created on funding the settlement + #[account(mut)] + split_rent_refund_account: UncheckedAccount<'info>, clock: Sysvar<'info, Clock>, @@ -79,28 +83,29 @@ pub struct CloseSettlement<'info> { impl<'info> CloseSettlement<'info> { pub fn process(&mut self) -> Result<()> { - require!(true == false, ErrorCode::NotYetImplemented); - if self.settlement.split_rent_collector.is_some() { + let stake_account = deserialize_stake_account(&self.split_rent_refund_account)?; // stake account is managed by bonds program let stake_meta = check_stake_is_initialized_with_withdrawer_authority( - &self.stake_account, + &stake_account, &self.bonds_withdrawer_authority.key(), "stake_account", )?; - // stake account is delegated (deposited by) the bond validator - check_stake_valid_delegation(&self.stake_account, &self.bond.validator_vote_account)?; - // provided stake account must be funded; staker == settlement staker authority + // TODO: could be stake account only under bonds? do we need to have here settlement stake account? + // stake account must be attached to the settlement, i.e., staker == settlement staker authority require_keys_eq!( stake_meta.authorized.staker, - self.settlement.settlement_authority, - ErrorCode::StakeAccountNotFunded, + self.settlement.authority, + ErrorCode::StakeAccountNotFundedToSettlement, ); + // stake account is delegated to bond's validator vote account + check_stake_valid_delegation(&stake_account, &self.bond.vote_account)?; + withdraw( CpiContext::new_with_signer( self.stake_program.to_account_info(), Withdraw { - stake: self.stake_account.to_account_info(), + stake: self.split_rent_refund_account.to_account_info(), withdrawer: self.bonds_withdrawer_authority.to_account_info(), to: self.split_rent_collector.to_account_info(), clock: self.clock.to_account_info(), @@ -122,12 +127,13 @@ impl<'info> CloseSettlement<'info> { settlement: self.settlement.key(), merkle_root: self.settlement.merkle_root, max_total_claim: self.settlement.max_total_claim, - max_num_nodes: self.settlement.max_num_nodes, - total_funded: self.settlement.total_funded, - total_funds_claimed: self.settlement.total_funds_claimed, - num_nodes_claimed: self.settlement.num_nodes_claimed, + max_merkle_nodes: self.settlement.max_merkle_nodes, + lamports_funded: self.settlement.lamports_funded, + lamports_claimed: self.settlement.lamports_claimed, + merkle_nodes_claimed: self.settlement.merkle_nodes_claimed, rent_collector: self.rent_collector.key(), split_rent_collector: self.settlement.split_rent_collector, + split_rent_refund_account: self.split_rent_refund_account.key(), expiration_epoch: self.settlement.epoch_created_at + self.config.epochs_to_claim_settlement, current_epoch: self.clock.epoch, diff --git a/programs/validator-bonds/src/instructions/settlement/close_settlement_claim.rs b/programs/validator-bonds/src/instructions/settlement/close_settlement_claim.rs index c2c810ec..9f466504 100644 --- a/programs/validator-bonds/src/instructions/settlement/close_settlement_claim.rs +++ b/programs/validator-bonds/src/instructions/settlement/close_settlement_claim.rs @@ -24,10 +24,7 @@ pub struct CloseSettlementClaim<'info> { impl<'info> CloseSettlementClaim<'info> { pub fn process(&mut self) -> Result<()> { - require!(true == false, ErrorCode::NotYetImplemented); - // The rule is that the settlement claim can be closed only when settlement does not exist - // TODO: is there a nicer (more anchor native) way to verify the non-existence of the account? require_eq!( self.settlement.lamports(), 0, diff --git a/programs/validator-bonds/src/instructions/settlement/fund_settlement.rs b/programs/validator-bonds/src/instructions/settlement/fund_settlement.rs index f672acfc..47e069b8 100644 --- a/programs/validator-bonds/src/instructions/settlement/fund_settlement.rs +++ b/programs/validator-bonds/src/instructions/settlement/fund_settlement.rs @@ -1,5 +1,6 @@ use crate::checks::{ - check_stake_is_initialized_with_withdrawer_authority, check_stake_valid_delegation, + check_stake_is_initialized_with_withdrawer_authority, check_stake_is_not_locked, + check_stake_valid_delegation, }; use crate::constants::BONDS_AUTHORITY_SEED; use crate::error::ErrorCode; @@ -34,7 +35,7 @@ pub struct FundSettlement<'info> { seeds = [ b"bond_account", config.key().as_ref(), - bond.validator_vote_account.as_ref(), + bond.vote_account.as_ref(), ], bump = bond.bump, )] @@ -43,7 +44,7 @@ pub struct FundSettlement<'info> { #[account( mut, has_one = bond @ ErrorCode::BondAccountMismatch, - has_one = settlement_authority @ ErrorCode::SettlementAuthorityMismatch, + constraint = settlement.authority == settlement_authority.key() @ ErrorCode::SettlementAuthorityMismatch, seeds = [ b"settlement_account", bond.key().as_ref(), @@ -54,6 +55,7 @@ pub struct FundSettlement<'info> { )] settlement: Account<'info, Settlement>, + // TODO: verify with lj how we want to manage this /// operator signer authority is allowed to fund the settlement account /// (making this operation permission-ed, at least for the first version of the contract) operator_authority: Signer<'info>, @@ -85,9 +87,10 @@ pub struct FundSettlement<'info> { )] bonds_withdrawer_authority: UncheckedAccount<'info>, - // TODO: could this be a PDA? - // this is a whatever address that does not exist (needed a signature for it) and will be initiated here - /// a split stake account is needed when the provided stake_account is bigger than the settlement + /// an account that does not exist, it will be initialized as a stake account (the signature needed) + /// the split_stake_account is needed when the provided stake_account is consists of more lamports + /// than the amount needed to fund the settlement, the left-over lamports from the stake account is split + /// into the new split_stake_account; when the split_stake_account is not needed, the rent payer is refunded #[account( init, payer = split_stake_rent_payer, @@ -96,10 +99,10 @@ pub struct FundSettlement<'info> { )] split_stake_account: Account<'info, StakeAccount>, - /// This is an account used to prefund the split stake account. - /// If a split stake account is not needed then rent payer is fully refunded at the end of the transaction. - /// If a split stake account is created for the settlement, the payer needs to manually close the claim_settlement - /// instruction to get the rent back (success only when the stake account is already deactivated). + /// rent exempt payer of the split_stake_account creation + /// if the split_stake_account is not needed (no left-over lamports on funding) then rent payer is refunded + /// it the split_stake_account is needed to spill out over funding of the settlement + /// then the rent payer is refunded when the settlement is closed #[account( mut, owner = system_program::ID @@ -121,10 +124,15 @@ pub struct FundSettlement<'info> { impl<'info> FundSettlement<'info> { pub fn process(&mut self) -> Result<()> { - require!(true == false, ErrorCode::NotYetImplemented); - - if self.settlement.total_funded >= self.settlement.max_total_claim { + if self.settlement.lamports_funded >= self.settlement.max_total_claim { msg!("Settlement is already fully funded"); + return_unused_split_stake_account_rent( + &self.stake_program, + &self.split_stake_account, + &self.split_stake_rent_payer, + &self.clock, + &self.stake_history, + )?; return Ok(()); } @@ -134,8 +142,6 @@ impl<'info> FundSettlement<'info> { &self.bonds_withdrawer_authority.key(), "stake_account", )?; - // settlement funding may accept only stake account delegated to (i.e., deposited by) the bond validator - check_stake_valid_delegation(&self.stake_account, &self.bond.validator_vote_account)?; // provided stake account must NOT have been used to fund settlement (but must be owned by bonds program) // funded to bond account -> staker == bonds withdrawer authority, funded to settlement -> staker == settlement staker authority require_keys_eq!( @@ -143,9 +149,11 @@ impl<'info> FundSettlement<'info> { self.bonds_withdrawer_authority.key(), ErrorCode::StakeAccountIsFundedToSettlement, ); + // only stake account delegated to (i.e., funded by) the bond validator vote account + check_stake_valid_delegation(&self.stake_account, &self.bond.vote_account)?; - // TODO: consider if missing to check the stake account is fully activated - // considering we don't care as it will be just deactivated(?) + // funded stake account cannot be locked as we want to deactivate&withdraw + check_stake_is_not_locked(&self.stake_account, &self.clock, "stake_account")?; let split_stake_rent_exempt = self.split_stake_account.to_account_info().lamports(); let stake_account_min_size = minimal_size_stake_account(&stake_meta, &self.config); @@ -155,16 +163,17 @@ impl<'info> FundSettlement<'info> { // amount needed: "amount + rent exempt + minimal stake size" -> ensuring stake account may exist // NOTE: once deactivated the balance may drop only to "rent exempt" and "minimal stake size" is not needed anymore // but we want to re-activate later the left-over at the stake account thus needed to be funded with plus minimal stake size - let amount_needed = - self.settlement.max_total_claim - self.settlement.total_funded + stake_account_min_size; + let amount_needed = self.settlement.max_total_claim - self.settlement.lamports_funded + + stake_account_min_size; // the left-over stake account has to be capable to exist after splitting - let left_over_splittable = amount_available - amount_needed >= stake_account_min_size; + let left_over_splittable = amount_available > amount_needed + && amount_available - amount_needed >= stake_account_min_size + split_stake_rent_exempt; let (funding_amount, is_split) = - // no split needed or possible, all stake amount goes into non-fulfilled settlement amount + // no split needed or possible, whole stake account funded, still amout funded is substracted off the min size + // as after claiming the stake will be capable to exist if amount_available <= amount_needed || !left_over_splittable { - let lamports_to_fund = self.stake_account.get_lamports(); - self.settlement.total_funded += lamports_to_fund; + let lamports_to_fund = self.stake_account.get_lamports() - stake_account_min_size; // whole amount used, not split - closing and returning rent return_unused_split_stake_account_rent( @@ -176,11 +185,11 @@ impl<'info> FundSettlement<'info> { )?; (lamports_to_fund, false) } else { - let lamports_to_fund = amount_needed; - self.settlement.total_funded += lamports_to_fund; - + let lamports_to_fund = amount_needed - stake_account_min_size; + // split gains what overflows from funding + amount needed to pay back to split rent payer on close let fund_split_leftover = - self.stake_account.get_lamports() - lamports_to_fund; + self.stake_account.get_lamports() - amount_needed - split_stake_rent_exempt; + let split_instruction = stake::instruction::split( self.stake_account.to_account_info().key, self.bonds_withdrawer_authority.key, @@ -246,14 +255,16 @@ impl<'info> FundSettlement<'info> { None, )?; + self.settlement.lamports_funded += funding_amount; + emit!(FundSettlementEvent { bond: self.bond.key(), - vote_account: self.bond.validator_vote_account, + vote_account: self.bond.vote_account, settlement: self.settlement.key(), funding_amount, - total_funded: self.settlement.total_funded, - total_funds_claimed: self.settlement.total_funds_claimed, - num_nodes_claimed: self.settlement.num_nodes_claimed, + lamports_funded: self.settlement.lamports_funded, + lamports_claimed: self.settlement.lamports_claimed, + merkle_nodes_claimed: self.settlement.merkle_nodes_claimed, stake_account: self.stake_account.key(), split_stake_account: if is_split { Some(SplitStakeData { diff --git a/programs/validator-bonds/src/instructions/settlement/init_settlement.rs b/programs/validator-bonds/src/instructions/settlement/init_settlement.rs index 4a3f3862..0288d6a1 100644 --- a/programs/validator-bonds/src/instructions/settlement/init_settlement.rs +++ b/programs/validator-bonds/src/instructions/settlement/init_settlement.rs @@ -9,10 +9,14 @@ use anchor_lang::solana_program::system_program; #[derive(AnchorDeserialize, AnchorSerialize)] pub struct InitSettlementArgs { + /// merkle root for this settlement, multiple settlements can be created with the same merkle root, + /// settlements will be distinguished by the vote_account pub merkle_root: [u8; 32], - pub vote_account: Pubkey, - pub settlement_total_claim: u64, - pub settlement_num_nodes: u64, + /// maximal number of lamports that can be claimed from this settlement + pub max_total_claim: u64, + /// maximal number of merkle tree nodes that can be claimed from this settlement + pub max_merkle_nodes: u64, + /// collects the rent exempt from the settlement account when closed pub rent_collector: Pubkey, } @@ -30,12 +34,14 @@ pub struct InitSettlement<'info> { seeds = [ b"bond_account", config.key().as_ref(), - params.vote_account.as_ref() + bond.vote_account.as_ref() ], bump = bond.bump, )] bond: Account<'info, Bond>, + // TODO: settlement account is defined by clock account epoch at time when it's created. + // Question is if it's ok to not be sure of the settlement address when calling from SDK. Or how to return correct address? #[account( init, payer = rent_payer, @@ -70,51 +76,47 @@ impl<'info> InitSettlement<'info> { &mut self, InitSettlementArgs { merkle_root, - vote_account, rent_collector, - settlement_total_claim, - settlement_num_nodes, + max_total_claim, + max_merkle_nodes, }: InitSettlementArgs, settlement_bump: u8, ) -> Result<()> { - require!(true == false, ErrorCode::NotYetImplemented); - - if settlement_total_claim == 0 || settlement_num_nodes == 0 { + if max_total_claim == 0 || max_merkle_nodes == 0 { return Err(error!(ErrorCode::EmptySettlementMerkleTree).with_values(( - "settlement_total_claim, settlement_num_nodes", - format!("{}, {}", settlement_total_claim, settlement_num_nodes), + "max_total_claim, max_merkle_nodes", + format!("{}, {}", max_total_claim, max_merkle_nodes), ))); } - let (settlement_authority, settlement_authority_bump) = - find_settlement_authority(&self.settlement.key()); + let (authority, authority_bump) = find_settlement_authority(&self.settlement.key()); self.settlement.set_inner(Settlement { bond: self.bond.key(), - settlement_authority, + authority, merkle_root, - max_total_claim: settlement_total_claim, - max_num_nodes: settlement_num_nodes, - total_funded: 0, - total_funds_claimed: 0, - num_nodes_claimed: 0, + max_total_claim, + max_merkle_nodes, + lamports_funded: 0, + lamports_claimed: 0, + merkle_nodes_claimed: 0, epoch_created_at: self.clock.epoch, rent_collector, split_rent_collector: None, split_rent_amount: 0, bumps: Bumps { pda: settlement_bump, - authority: settlement_authority_bump, + authority: authority_bump, }, reserved: Reserved150::default(), }); emit!(InitSettlementEvent { bond: self.settlement.bond, - vote_account, - settlement_authority: self.settlement.settlement_authority, + vote_account: self.bond.vote_account, + authority: self.settlement.authority, merkle_root: self.settlement.merkle_root, max_total_claim: self.settlement.max_total_claim, - max_num_nodes: self.settlement.max_num_nodes, - epoch: self.settlement.epoch_created_at, + max_merkle_nodes: self.settlement.max_merkle_nodes, + epoch_created_at: self.settlement.epoch_created_at, rent_collector: self.settlement.rent_collector, bumps: self.settlement.bumps.clone(), }); diff --git a/programs/validator-bonds/src/instructions/stake/reset.rs b/programs/validator-bonds/src/instructions/stake/reset.rs index 8f655899..5e20df25 100644 --- a/programs/validator-bonds/src/instructions/stake/reset.rs +++ b/programs/validator-bonds/src/instructions/stake/reset.rs @@ -6,11 +6,13 @@ use crate::error::ErrorCode; use crate::events::stake::ResetEvent; use crate::state::bond::Bond; use crate::state::config::Config; +use crate::state::settlement::find_settlement_authority; use anchor_lang::prelude::*; use anchor_lang::solana_program::program::invoke_signed; use anchor_lang::solana_program::stake; use anchor_lang::solana_program::stake::state::StakeAuthorize; use anchor_lang::solana_program::sysvar::stake_history; +use anchor_lang::solana_program::vote::program::ID as vote_program_id; use anchor_spl::stake::{authorize, Authorize, Stake, StakeAccount}; /// Resetting stake authority of a funded stake account belonging to removed settlement. @@ -23,27 +25,27 @@ pub struct ResetStake<'info> { #[account( has_one = config @ ErrorCode::ConfigAccountMismatch, + has_one = vote_account @ ErrorCode::VoteAccountMismatch, seeds = [ b"bond_account", config.key().as_ref(), - bond.validator_vote_account.key().as_ref() + vote_account.key().as_ref() ], bump = bond.bump, )] bond: Account<'info, Bond>, - /// CHECK: verification that it does not exist + /// CHECK: cannot exist + /// settlment account used to derive settlement authority which cannot exists settlement: UncheckedAccount<'info>, /// stake account belonging to authority of the settlement #[account(mut)] stake_account: Account<'info, StakeAccount>, - /// CHECK: CPI calls of stake authorize permits to change the staker only with correct settlement authority - settlement_authority: UncheckedAccount<'info>, - /// CHECK: PDA - /// authority that owns (withdrawer authority) all stakes account under the bonds program + /// bonds withdrawer authority + /// to cancel settlement funding of the stake account changing staker authority to address #[account( seeds = [ b"bonds_authority", @@ -54,7 +56,10 @@ pub struct ResetStake<'info> { bonds_withdrawer_authority: UncheckedAccount<'info>, /// CHECK: the validator vote account to which the stake account is delegated, check in code - validator_vote_account: UncheckedAccount<'info>, + #[account( + owner = vote_program_id @ ErrorCode::InvalidVoteAccountProgramId, + )] + vote_account: UncheckedAccount<'info>, /// CHECK: have no CPU budget to parse #[account(address = stake_history::ID)] @@ -71,8 +76,6 @@ pub struct ResetStake<'info> { impl<'info> ResetStake<'info> { pub fn process(&mut self) -> Result<()> { - require!(true == false, ErrorCode::NotYetImplemented); - // settlement account cannot exists require_eq!( self.settlement.lamports(), @@ -86,13 +89,13 @@ impl<'info> ResetStake<'info> { &self.bonds_withdrawer_authority.key(), "stake_account", )?; - // one bond can be created for a validator vote account, this stake account belongs to bond - check_stake_valid_delegation(&self.stake_account, &self.bond.validator_vote_account)?; - check_stake_valid_delegation(&self.stake_account, &self.validator_vote_account.key())?; + // a bond account is tightly coupled to a vote account, this stake account belongs to bond + check_stake_valid_delegation(&self.stake_account, &self.bond.vote_account)?; // stake account is funded to particular settlement + let settlement_authority = find_settlement_authority(&self.settlement.key()).0; require_eq!( stake_meta.authorized.staker, - self.settlement_authority.key(), + settlement_authority, ErrorCode::SettlementAuthorityMismatch ); @@ -103,7 +106,7 @@ impl<'info> ResetStake<'info> { self.stake_program.to_account_info(), Authorize { stake: self.stake_account.to_account_info(), - authorized: self.settlement_authority.to_account_info(), + authorized: self.bonds_withdrawer_authority.to_account_info(), new_authorized: self.bonds_withdrawer_authority.to_account_info(), clock: self.clock.to_account_info(), }, @@ -117,11 +120,11 @@ impl<'info> ResetStake<'info> { None, )?; - // activate the stake, i.e., resetting is delegating to the validator again + // activate the stake, i.e., resetting delegation to the validator again let delegate_instruction = &stake::instruction::delegate_stake( &self.stake_account.key(), &self.bonds_withdrawer_authority.key(), - &self.bond.validator_vote_account, + &self.bond.vote_account, ); invoke_signed( delegate_instruction, @@ -129,7 +132,7 @@ impl<'info> ResetStake<'info> { self.stake_program.to_account_info(), self.stake_account.to_account_info(), self.bonds_withdrawer_authority.to_account_info(), - self.validator_vote_account.to_account_info(), + self.vote_account.to_account_info(), self.clock.to_account_info(), self.stake_history.to_account_info(), self.stake_config.to_account_info(), @@ -146,9 +149,8 @@ impl<'info> ResetStake<'info> { bond: self.bond.key(), settlement: self.settlement.key(), stake_account: self.stake_account.key(), - validator_vote_acount: self.validator_vote_account.key(), - settlement_authority: self.settlement_authority.key(), - bonds_withdrawer_authority: self.bonds_withdrawer_authority.key(), + vote_account: self.vote_account.key(), + settlement_authority, }); Ok(()) diff --git a/programs/validator-bonds/src/instructions/withdraw/cancel_withdraw_request.rs b/programs/validator-bonds/src/instructions/withdraw/cancel_withdraw_request.rs index a7b31daf..29e4e9b8 100644 --- a/programs/validator-bonds/src/instructions/withdraw/cancel_withdraw_request.rs +++ b/programs/validator-bonds/src/instructions/withdraw/cancel_withdraw_request.rs @@ -4,6 +4,7 @@ use crate::events::withdraw::CancelWithdrawRequestEvent; use crate::state::bond::Bond; use crate::state::withdraw_request::WithdrawRequest; use anchor_lang::prelude::*; +use anchor_lang::solana_program::vote::program::ID as vote_program_id; /// Cancelling validator bond withdraw request. /// Only one withdraw request per bond. Cancelling makes a way for a new request with new amount. @@ -11,19 +12,21 @@ use anchor_lang::prelude::*; pub struct CancelWithdrawRequest<'info> { #[account( mut, - has_one = validator_vote_account @ ErrorCode::VoteAccountMismatch, + has_one = vote_account @ ErrorCode::VoteAccountMismatch, seeds = [ b"bond_account", bond.config.key().as_ref(), - validator_vote_account.key().as_ref() + vote_account.key().as_ref() ], bump = bond.bump, )] bond: Account<'info, Bond>, /// CHECK: check&deserialize of the vote account in the code - #[account()] - validator_vote_account: UncheckedAccount<'info>, + #[account( + owner = vote_program_id @ ErrorCode::InvalidVoteAccountProgramId, + )] + vote_account: UncheckedAccount<'info>, /// validator vote account validator identity or bond authority may ask for cancelling #[account()] @@ -48,11 +51,7 @@ pub struct CancelWithdrawRequest<'info> { impl<'info> CancelWithdrawRequest<'info> { pub fn process(&mut self) -> Result<()> { require!( - check_bond_change_permitted( - &self.authority.key(), - &self.bond, - &self.validator_vote_account - ), + check_bond_change_permitted(&self.authority.key(), &self.bond, &self.vote_account), ErrorCode::InvalidWithdrawRequestAuthority ); diff --git a/programs/validator-bonds/src/instructions/withdraw/claim_withdraw_request.rs b/programs/validator-bonds/src/instructions/withdraw/claim_withdraw_request.rs index 13fc2d50..b407e1b4 100644 --- a/programs/validator-bonds/src/instructions/withdraw/claim_withdraw_request.rs +++ b/programs/validator-bonds/src/instructions/withdraw/claim_withdraw_request.rs @@ -13,6 +13,7 @@ use crate::state::withdraw_request::WithdrawRequest; use crate::utils::{minimal_size_stake_account, return_unused_split_stake_account_rent}; use anchor_lang::prelude::*; use anchor_lang::solana_program::stake::state::{StakeAuthorize, StakeState}; +use anchor_lang::solana_program::vote::program::ID as vote_program_id; use anchor_lang::solana_program::{program::invoke_signed, stake, system_program}; use anchor_spl::stake::{authorize, Authorize, Stake, StakeAccount}; @@ -27,19 +28,21 @@ pub struct ClaimWithdrawRequest<'info> { #[account( mut, has_one = config @ ErrorCode::ConfigAccountMismatch, - has_one = validator_vote_account @ ErrorCode::VoteAccountMismatch, + has_one = vote_account @ ErrorCode::VoteAccountMismatch, seeds = [ b"bond_account", config.key().as_ref(), - validator_vote_account.key().as_ref() + vote_account.key().as_ref() ], bump = bond.bump, )] bond: Account<'info, Bond>, /// CHECK: deserialization of the vote account in the code - #[account()] - validator_vote_account: UncheckedAccount<'info>, + #[account( + owner = vote_program_id @ ErrorCode::InvalidVoteAccountProgramId, + )] + vote_account: UncheckedAccount<'info>, /// validator vote account node identity or bond authority may claim #[account()] @@ -47,7 +50,7 @@ pub struct ClaimWithdrawRequest<'info> { #[account( mut, - has_one = validator_vote_account @ ErrorCode::WithdrawRequestVoteAccountMismatch, + has_one = vote_account @ ErrorCode::WithdrawRequestVoteAccountMismatch, has_one = bond @ ErrorCode::BondAccountMismatch, constraint = withdraw_request.epoch + config.withdraw_lockup_epochs < clock.epoch @ ErrorCode::WithdrawRequestNotReady, seeds = [ @@ -117,11 +120,7 @@ impl<'info> ClaimWithdrawRequest<'info> { // claim is permission-ed as the init withdraw request require!( - check_bond_change_permitted( - &self.authority.key(), - &self.bond, - &self.validator_vote_account - ), + check_bond_change_permitted(&self.authority.key(), &self.bond, &self.vote_account), ErrorCode::InvalidWithdrawRequestAuthority ); @@ -133,7 +132,7 @@ impl<'info> ClaimWithdrawRequest<'info> { &self.stake_history, )?; // stake account is delegated to the validator vote account associated with the bond - check_stake_valid_delegation(&self.stake_account, &self.bond.validator_vote_account)?; + check_stake_valid_delegation(&self.stake_account, &self.bond.vote_account)?; // stake account belongs under the bonds program let stake_meta = check_stake_is_initialized_with_withdrawer_authority( @@ -270,7 +269,7 @@ impl<'info> ClaimWithdrawRequest<'info> { emit!(ClaimWithdrawRequestEvent { bond: self.bond.key(), - validator_vote_account: self.validator_vote_account.key(), + vote_account: self.vote_account.key(), withdraw_request: self.withdraw_request.key(), stake_account: self.stake_account.key(), split_stake: if is_split { diff --git a/programs/validator-bonds/src/instructions/withdraw/init_withdraw_request.rs b/programs/validator-bonds/src/instructions/withdraw/init_withdraw_request.rs index cf88e16f..6ec480de 100644 --- a/programs/validator-bonds/src/instructions/withdraw/init_withdraw_request.rs +++ b/programs/validator-bonds/src/instructions/withdraw/init_withdraw_request.rs @@ -7,6 +7,7 @@ use crate::state::withdraw_request::WithdrawRequest; use crate::state::Reserved150; use anchor_lang::prelude::*; use anchor_lang::solana_program::system_program; +use anchor_lang::solana_program::vote::program::ID as vote_program_id; #[derive(AnchorDeserialize, AnchorSerialize)] pub struct InitWithdrawRequestArgs { @@ -22,19 +23,21 @@ pub struct InitWithdrawRequest<'info> { #[account( has_one = config @ ErrorCode::ConfigAccountMismatch, - has_one = validator_vote_account @ ErrorCode::VoteAccountMismatch, + has_one = vote_account @ ErrorCode::VoteAccountMismatch, seeds = [ b"bond_account", config.key().as_ref(), - validator_vote_account.key().as_ref() + vote_account.key().as_ref() ], bump = bond.bump, )] bond: Account<'info, Bond>, - /// CHECK: check&deserialize of the vote account in the code - #[account()] - validator_vote_account: UncheckedAccount<'info>, + /// CHECK: check&deserialize of the validator vote account in the code + #[account( + owner = vote_program_id @ ErrorCode::InvalidVoteAccountProgramId, + )] + vote_account: UncheckedAccount<'info>, /// validator vote account node identity or bond authority may ask for the withdrawal #[account()] @@ -71,17 +74,13 @@ impl<'info> InitWithdrawRequest<'info> { withdraw_request_bump: u8, ) -> Result<()> { require!( - check_bond_change_permitted( - &self.authority.key(), - &self.bond, - &self.validator_vote_account - ), + check_bond_change_permitted(&self.authority.key(), &self.bond, &self.vote_account), ErrorCode::InvalidWithdrawRequestAuthority ); self.withdraw_request.set_inner(WithdrawRequest { bond: self.bond.key(), - validator_vote_account: self.bond.validator_vote_account.key(), + vote_account: self.bond.vote_account.key(), bump: withdraw_request_bump, epoch: self.clock.epoch, withdrawn_amount: 0, @@ -91,7 +90,7 @@ impl<'info> InitWithdrawRequest<'info> { emit!(InitWithdrawRequestEvent { withdraw_request: self.withdraw_request.key(), bond: self.withdraw_request.bond.key(), - validator_vote_account: self.withdraw_request.validator_vote_account.key(), + vote_account: self.withdraw_request.vote_account.key(), bump: self.withdraw_request.bump, requested_amount: self.withdraw_request.requested_amount, epoch: self.withdraw_request.epoch, diff --git a/programs/validator-bonds/src/lib.rs b/programs/validator-bonds/src/lib.rs index 8ee31c64..ffe9c1fa 100644 --- a/programs/validator-bonds/src/lib.rs +++ b/programs/validator-bonds/src/lib.rs @@ -35,6 +35,7 @@ declare_id!("vBoNdEvzMrSai7is21XgVYik65mqtaKXuSdMBJ1xkW4"); // - consider utility of zero_copy, if it can be used, what are benefits? // - consider using only PDA hash and not has_one at anchor constraints // - readme with table of what stake_authority/withdraw_authority are at which stages +// - verify that really every input account is checked for owner program! fn check_context(ctx: &Context) -> Result<()> { if !check_id(ctx.program_id) { @@ -143,6 +144,11 @@ pub mod validator_bonds { check_context(&ctx)?; ctx.accounts.process() } + + pub fn migrate_bond_cpmpe(ctx: Context) -> Result<()> { + check_context(&ctx)?; + ctx.accounts.process() + } } #[cfg(test)] diff --git a/programs/validator-bonds/src/state/bond.rs b/programs/validator-bonds/src/state/bond.rs index ec64ca6c..b56ae885 100644 --- a/programs/validator-bonds/src/state/bond.rs +++ b/programs/validator-bonds/src/state/bond.rs @@ -1,13 +1,12 @@ use crate::constants::BOND_SEED; use crate::error::ErrorCode; use crate::state::Reserved150; -use crate::utils::basis_points::HundredthBasisPoint; use crate::ID; use anchor_lang::prelude::*; /// Bond account for a validator vote address #[account] -#[derive(Debug, Default)] +#[derive(Debug)] pub struct Bond { /// Contract root config address. Validator bond is created for this config as PDA /// but saving the address here for easier access with getProgramAccounts call @@ -15,27 +14,51 @@ pub struct Bond { /// Validator vote address that this bond account is crated for /// INVARIANTS: /// - one bond account per validator vote address - /// - bond program does not change received stake account delegation voter_pubkey to any other validator vote - pub validator_vote_account: Pubkey, + /// - this program does NOT change stake account delegation voter_pubkey to any other validator vote account + pub vote_account: Pubkey, /// Authority that may close the bond or withdraw stake accounts associated with the bond /// The same powers has got the owner of the validator vote account // https://github.com/solana-labs/solana/blob/master/vote/src/vote_account.rs pub authority: Pubkey, - /// Revenue that is distributed from the bond (from validator) to the protocol - pub revenue_share: HundredthBasisPoint, + /// Cost per mille per epoch + pub cpmpe: u64, /// PDA Bond address bump seed pub bump: u8, /// reserve space for future extensions + pub reserved: [u8; 142], +} + +#[account] +#[derive(Debug)] +pub struct BondWithRevenueShare { + pub config: Pubkey, + pub vote_account: Pubkey, + pub authority: Pubkey, + pub revenue_share: u32, + pub bump: u8, pub reserved: Reserved150, } +impl Default for Bond { + fn default() -> Self { + Self { + config: Pubkey::default(), + vote_account: Pubkey::default(), + authority: Pubkey::default(), + cpmpe: 0, + bump: 0, + reserved: [0; 142], + } + } +} + impl Bond { pub fn find_address(&self) -> Result { Pubkey::create_program_address( &[ BOND_SEED, &self.config.key().as_ref(), - &self.validator_vote_account.as_ref(), + &self.vote_account.as_ref(), &[self.bump], ], &ID, @@ -43,3 +66,10 @@ impl Bond { .map_err(|_| ErrorCode::InvalidBondAddress.into()) } } + +pub fn find_bond_address(config: &Pubkey, vote_account: &Pubkey) -> (Pubkey, u8) { + Pubkey::find_program_address( + &[BOND_SEED, config.key().as_ref(), vote_account.as_ref()], + &ID, + ) +} diff --git a/programs/validator-bonds/src/state/settlement.rs b/programs/validator-bonds/src/state/settlement.rs index cc821fed..6e39b8f8 100644 --- a/programs/validator-bonds/src/state/settlement.rs +++ b/programs/validator-bonds/src/state/settlement.rs @@ -11,25 +11,26 @@ use anchor_lang::prelude::*; pub struct Settlement { /// this settlement belongs under particular bond, i.e., under particular validator vote account pub bond: Pubkey, - /// stake account authority that manages the funded stake accounts - pub settlement_authority: Pubkey, + /// settlement authority used as the 'staker' stake account authority + /// of stake accounts funded to this settlement + pub authority: Pubkey, /// 256-bit merkle root to check the claims against pub merkle_root: [u8; 32], /// maximum number of funds that can ever be claimed from this [Settlement] pub max_total_claim: u64, - /// maximum number of nodes that can ever be claimed from this [Settlement] - pub max_num_nodes: u64, - /// total funds that have been deposited to this [Settlement] - pub total_funded: u64, - /// total funds that have been claimed from this [Settlement] - pub total_funds_claimed: u64, + /// maximum number of merkle tree nodes that can ever be claimed from this [Settlement] + pub max_merkle_nodes: u64, + /// total lamports funded to this [Settlement] + pub lamports_funded: u64, + /// total lamports that have been claimed from this [Settlement] + pub lamports_claimed: u64, /// number of nodes that have been claimed from this [Settlement] - pub num_nodes_claimed: u64, + pub merkle_nodes_claimed: u64, /// epoch that the [Settlement] has been created at pub epoch_created_at: u64, /// address that collects the rent exempt from the [Settlement] account when closed pub rent_collector: Pubkey, - /// address that may claim the rent exempt for creation of "split stake account" + /// address claiming the rent exempt for "split stake account" created on funding settlement pub split_rent_collector: Option, pub split_rent_amount: u64, /// PDA bumps diff --git a/programs/validator-bonds/src/state/settlement_claim.rs b/programs/validator-bonds/src/state/settlement_claim.rs index a4212632..2ae59556 100644 --- a/programs/validator-bonds/src/state/settlement_claim.rs +++ b/programs/validator-bonds/src/state/settlement_claim.rs @@ -1,6 +1,7 @@ use crate::constants::SETTLEMENT_CLAIM_SEED; use crate::error::ErrorCode; use crate::state::Reserved150; +use crate::utils::merkle_proof; use crate::ID; use anchor_lang::prelude::*; @@ -11,14 +12,14 @@ use anchor_lang::prelude::*; pub struct SettlementClaim { /// settlement account this claim belongs under pub settlement: Pubkey, - /// staker authority as part of the merkle proof for this claim - pub staker_authority: Pubkey, + /// stake authority as part of the merkle proof for this claim + pub stake_authority: Pubkey, /// withdrawer authority that has got permission to withdraw the claim - pub withdrawer_authority: Pubkey, + pub withdraw_authority: Pubkey, /// vote account as part of the merkle proof for this claim pub vote_account: Pubkey, /// claim amount - pub claim: u64, + pub amount: u64, /// PDA account bump, one claim per settlement pub bump: u8, /// rent collector account to get the rent back for claim account creation @@ -29,15 +30,18 @@ pub struct SettlementClaim { impl SettlementClaim { pub fn find_address(&self) -> Result { - // TODO: I'm not sure about the maximal seed length for the program address Pubkey::create_program_address( &[ SETTLEMENT_CLAIM_SEED, &self.settlement.key().as_ref(), - &self.staker_authority.as_ref(), - &self.withdrawer_authority.as_ref(), - &self.vote_account.as_ref(), - &self.claim.to_le_bytes().as_ref(), + merkle_proof::TreeNode { + stake_authority: self.withdraw_authority.to_string(), + withdraw_authority: self.stake_authority.to_string(), + vote_account: self.vote_account.to_string(), + claim: self.amount, + } + .hash() + .as_ref(), &[self.bump], ], &ID, diff --git a/programs/validator-bonds/src/state/withdraw_request.rs b/programs/validator-bonds/src/state/withdraw_request.rs index 8aceaf4b..ac36b18e 100644 --- a/programs/validator-bonds/src/state/withdraw_request.rs +++ b/programs/validator-bonds/src/state/withdraw_request.rs @@ -8,9 +8,9 @@ use anchor_lang::prelude::*; #[account] #[derive(Debug, Default)] pub struct WithdrawRequest { - /// Validator that requested the withdraw - pub validator_vote_account: Pubkey, - /// Bond account that the withdraw request is for + /// Validator vote account that requested the withdraw + pub vote_account: Pubkey, + /// Bond account that the withdraw request is for (has to match with vote_account) pub bond: Pubkey, /// Epoch when the withdraw was requested, i.e., when this "ticket" is created pub epoch: u64, diff --git a/programs/validator-bonds/src/utils/basis_points.rs b/programs/validator-bonds/src/utils/basis_points.rs deleted file mode 100644 index 9039cbb6..00000000 --- a/programs/validator-bonds/src/utils/basis_points.rs +++ /dev/null @@ -1,191 +0,0 @@ -use crate::error::ErrorCode; -use anchor_lang::prelude::*; -use std::fmt::Display; - -#[cfg(feature = "no-entrypoint")] -use std::num::ParseFloatError; -#[cfg(feature = "no-entrypoint")] -use std::str::FromStr; - -/// It's a smaller unit of a basis point (basis point = 1/100%), we calculate 1/10_000% here instead. -/// The max value is 1_000_000 (100%). -/// 1 HundredthBasisPoint = 0.0001%, 10_000 HundredthBasisPoint = 1%, 1_000_000 HundredthBasisPoint = 100%. -#[derive( - Clone, Copy, Debug, Default, AnchorSerialize, AnchorDeserialize, PartialEq, Eq, PartialOrd, Ord, -)] -pub struct HundredthBasisPoint { - pub hundredth_bps: u32, -} - -impl HundredthBasisPoint { - const DIVIDER: u32 = 10_000; - const MAX_BPS: u32 = 1_000_000; - pub const MAX_HUNDREDTH_BPS: HundredthBasisPoint = HundredthBasisPoint { - hundredth_bps: HundredthBasisPoint::MAX_BPS, - }; // 100% - - pub fn from_hundredth_bp(hundredth_bps: u32) -> Result { - let initialized = Self { hundredth_bps }; - initialized.check() - } - - pub fn check(self) -> Result { - require_gte!( - &Self::MAX_HUNDREDTH_BPS, - &self, - ErrorCode::HundredthBasisPointsOverflow - ); - Ok(self) - } - - pub fn apply(&self, lamports: u64) -> u64 { - (lamports as u128 * self.hundredth_bps as u128 - / Self::MAX_HUNDREDTH_BPS.hundredth_bps as u128) as u64 - } -} - -impl Display for HundredthBasisPoint { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}.{:0>4}%", - self.hundredth_bps / HundredthBasisPoint::DIVIDER, - self.hundredth_bps % HundredthBasisPoint::DIVIDER - ) - } -} - -/// Parsing from string considering the input being in percents. -#[cfg(feature = "no-entrypoint")] -impl TryFrom for HundredthBasisPoint { - type Error = Error; - - fn try_from(n: f64) -> Result { - let hundredth_bps_i = (n * HundredthBasisPoint::DIVIDER as f64).floor() as i64; // 4.5% => 45000 bp_cents - let hundredths_bps = u32::try_from(hundredth_bps_i).map_err(|_| { - msg!( - "failed to convert f64 to u32, hundredth_bps_i: {}", - hundredth_bps_i - ); - error!(ErrorCode::HundredthBasisPointsCalculation) - })?; - Ok(HundredthBasisPoint::from_hundredth_bp(hundredths_bps)?) - } -} - -#[cfg(feature = "no-entrypoint")] -impl FromStr for HundredthBasisPoint { - type Err = ParseHundredthBasisPointError; - - fn from_str(s: &str) -> std::result::Result { - let parsed_s = - s.parse::() - .map_err(|e: ParseFloatError| ParseHundredthBasisPointError { - parsed_data: s.to_string(), - reason: e.to_string(), - })?; - - let result = f64::try_into(parsed_s).map_err(|e: Error| ParseHundredthBasisPointError { - parsed_data: s.to_string(), - reason: e.to_string(), - })?; - - Ok(result) - } -} - -#[cfg(feature = "no-entrypoint")] -#[derive(Debug, Clone, PartialEq, Eq)] -#[non_exhaustive] -pub struct ParseHundredthBasisPointError { - pub parsed_data: String, - pub reason: String, -} - -#[cfg(feature = "no-entrypoint")] -impl Display for ParseHundredthBasisPointError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - format!( - "provided string {} was not a number that being of type f64 and in range of 0 and 1_000_000, reason: {}", - self.parsed_data, self.reason - ).fmt(f) - } -} - -#[cfg(feature = "no-entrypoint")] -impl std::error::Error for ParseHundredthBasisPointError { - #[allow(deprecated)] - fn description(&self) -> &str { - "failed to parse hundredth basis point" - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn hundred_points_from_u32_limit_match() { - HundredthBasisPoint::from_hundredth_bp(1_000_000).unwrap(); - HundredthBasisPoint::from_hundredth_bp(0).unwrap(); - HundredthBasisPoint::from_hundredth_bp(u32::MIN).unwrap(); - HundredthBasisPoint::from_hundredth_bp(42).unwrap(); - HundredthBasisPoint::from_hundredth_bp(999_999).unwrap(); - - HundredthBasisPoint::from_hundredth_bp(1_000_001).unwrap_err(); - HundredthBasisPoint::from_hundredth_bp(u32::MAX).unwrap_err(); - } - - #[test] - fn hundred_points_struct_limit_match() { - HundredthBasisPoint { - hundredth_bps: 1_000_000, - } - .check() - .unwrap(); - HundredthBasisPoint { hundredth_bps: 0 }.check().unwrap(); - HundredthBasisPoint { - hundredth_bps: 1000, - } - .check() - .unwrap(); - HundredthBasisPoint { - hundredth_bps: u32::MAX - 1, - } - .check() - .unwrap_err(); - } - - #[cfg(feature = "no-entrypoint")] - #[test] - fn hundred_points_from_str_limit_match() { - HundredthBasisPoint::from_str("100").unwrap(); - HundredthBasisPoint::from_str("0").unwrap(); - HundredthBasisPoint::from_str("42.23").unwrap(); - HundredthBasisPoint::from_str("99.999").unwrap(); - HundredthBasisPoint::from_str("100.1").unwrap_err(); - HundredthBasisPoint::from_str(&u32::MAX.to_string()).unwrap_err(); - } - - #[test] - fn hundred_points_calculate() { - // have 0% - let calculator = HundredthBasisPoint::from_hundredth_bp(0).unwrap(); - assert_eq!(calculator.apply(100), 0); - assert_eq!(calculator.apply(321), 0); - - // have 10% - let calculator = HundredthBasisPoint::from_hundredth_bp(100_000).unwrap(); - assert_eq!(calculator.apply(100), 10); - assert_eq!(calculator.apply(123456), 12345); - assert_eq!( - calculator.apply(100_000_000_000_000_001), - 10_000_000_000_000_000 - ); - - // have 100% - let calculator = HundredthBasisPoint::MAX_HUNDREDTH_BPS; - assert_eq!(calculator.apply(100), 100); - assert_eq!(calculator.apply(99), 99); - } -} diff --git a/programs/validator-bonds/src/utils/merkle_proof.rs b/programs/validator-bonds/src/utils/merkle_proof.rs index e4e4eafb..a971b98e 100644 --- a/programs/validator-bonds/src/utils/merkle_proof.rs +++ b/programs/validator-bonds/src/utils/merkle_proof.rs @@ -31,34 +31,35 @@ pub fn verify(proof: Vec<[u8; 32]>, root: [u8; 32], leaf: [u8; 32]) -> bool { computed_hash == root } -#[derive(Default, Clone, Eq, Debug, Hash, PartialEq)] +// TODO: the TreeNode implementation should be shared with insurance-engine in a lib +#[derive(Default, Clone, Eq, Debug, PartialEq)] pub struct TreeNode { - pub staker_authority: String, - pub withdrawer_authority: String, + pub stake_authority: String, + pub withdraw_authority: String, pub vote_account: String, pub claim: u64, } impl TreeNode { - fn hash(&self) -> Hash { + pub fn hash(&self) -> Hash { let mut hasher = Hasher::default(); - hasher.hash(self.staker_authority.as_ref()); - hasher.hash(self.withdrawer_authority.as_ref()); + hasher.hash(self.stake_authority.as_ref()); + hasher.hash(self.withdraw_authority.as_ref()); hasher.hash(self.vote_account.as_ref()); hasher.hash(self.claim.to_le_bytes().as_ref()); hasher.result() } } -pub fn tree_node( - staker_authority: Pubkey, - withdrawer_authority: Pubkey, +pub fn tree_node_leaf_hash( + stake_authority: Pubkey, + withdraw_authority: Pubkey, vote_account: Pubkey, claim: u64, ) -> [u8; 32] { let tree_node = TreeNode { - staker_authority: staker_authority.to_string(), - withdrawer_authority: withdrawer_authority.to_string(), + stake_authority: stake_authority.to_string(), + withdraw_authority: withdraw_authority.to_string(), vote_account: vote_account.to_string(), claim, }; @@ -141,7 +142,7 @@ mod tests { assert!(verify( proof, merkle_root, - tree_node(staker_authority, withdrawer_authority, vote_account, claim) + tree_node_leaf_hash(staker_authority, withdrawer_authority, vote_account, claim) )); } } diff --git a/programs/validator-bonds/src/utils/mod.rs b/programs/validator-bonds/src/utils/mod.rs index 69a6fdfe..ff9dbd0b 100644 --- a/programs/validator-bonds/src/utils/mod.rs +++ b/programs/validator-bonds/src/utils/mod.rs @@ -1,7 +1,5 @@ -pub mod basis_points; pub mod merkle_proof; pub mod stake; -pub use basis_points::*; pub use merkle_proof::*; pub use stake::*;