Skip to content

Commit

Permalink
fix(sdk-coin-sol): update partial unstake SOL
Browse files Browse the repository at this point in the history
rent should be sent to the unstaking address when partial unstake

solana-labs/solana#33300

EA-2492

TICKET: EA-2492
  • Loading branch information
noel-bitgo committed Mar 14, 2024
1 parent a71ea8f commit 4b2a941
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 17 deletions.
11 changes: 6 additions & 5 deletions modules/sdk-coin-sol/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,12 @@ export const stakingDeactivateInstructionsIndexes = {

/** Const to check the order of the Partial Staking Deactivate instructions when decoded */
export const stakingPartialDeactivateInstructionsIndexes = {
Allocate: 0,
Assign: 1,
Split: 2,
Deactivate: 3,
Memo: 4,
Transfer: 0,
Allocate: 1,
Assign: 2,
Split: 3,
Deactivate: 4,
Memo: 5,
} as const;

/** Const to check the order of the Staking Withdraw instructions when decode */
Expand Down
27 changes: 26 additions & 1 deletion modules/sdk-coin-sol/src/lib/instructionParamsFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
AuthorizeStakeParams,
CreateAccountParams,
DeactivateStakeParams,
DecodedTransferInstruction,
DelegateStakeParams,
InitializeStakeParams,
SplitStakeParams,
Expand Down Expand Up @@ -395,6 +396,20 @@ function parseStakingDeactivateInstructions(
});
}
break;

case ValidInstructionTypesEnum.Transfer:
if (
unstakingInstructions.length > 0 &&
unstakingInstructions[unstakingInstructions.length - 1].transfer === undefined
) {
unstakingInstructions[unstakingInstructions.length - 1].transfer =
SystemInstruction.decodeTransfer(instruction);
} else {
unstakingInstructions.push({
transfer: SystemInstruction.decodeTransfer(instruction),
});
}
break;
}
}

Expand Down Expand Up @@ -423,12 +438,18 @@ interface UnstakingInstructions {
assign?: AssignParams;
split?: SplitStakeParams;
deactivate?: DeactivateStakeParams;
transfer?: DecodedTransferInstruction;
}

function validateUnstakingInstructions(unstakingInstructions: UnstakingInstructions) {
if (!unstakingInstructions.deactivate) {
throw new NotSupported('Invalid deactivate stake transaction, missing deactivate stake account instruction');
} else if (unstakingInstructions.allocate || unstakingInstructions.assign || unstakingInstructions.split) {
} else if (
unstakingInstructions.allocate ||
unstakingInstructions.assign ||
unstakingInstructions.split ||
unstakingInstructions.transfer
) {
if (!unstakingInstructions.allocate) {
throw new NotSupported(
'Invalid partial deactivate stake transaction, missing allocate unstake account instruction'
Expand Down Expand Up @@ -464,6 +485,10 @@ function validateUnstakingInstructions(unstakingInstructions: UnstakingInstructi
throw new NotSupported(
'Invalid partial deactivate stake transaction, the unstaking account must be different from the Stake Account'
);
} else if (!unstakingInstructions.transfer) {
throw new NotSupported(
'Invalid partial deactivate stake transaction, missing funding of unstake address instruction'
);
}
}
}
Expand Down
16 changes: 13 additions & 3 deletions modules/sdk-coin-sol/src/lib/stakingDeactivateBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import assert from 'assert';

import { BuildTransactionError, TransactionType } from '@bitgo/sdk-core';
import { InstructionBuilderTypes } from './constants';
import { StakingDeactivate } from './iface';
import { StakingDeactivate, Transfer } from './iface';
import { Transaction } from './transaction';
import { TransactionBuilder } from './transactionBuilder';
import { isValidStakingAmount, validateAddress } from './utils';
Expand All @@ -12,6 +12,7 @@ export class StakingDeactivateBuilder extends TransactionBuilder {
protected _stakingAddress: string;
protected _stakingAddresses: string[];
protected _amount?: string;
protected _fundUnstakeAddress = 2282880;
protected _unstakingAddress: string;

constructor(_coinConfig: Readonly<CoinConfig>) {
Expand Down Expand Up @@ -135,12 +136,21 @@ export class StakingDeactivateBuilder extends TransactionBuilder {
'When partially unstaking the unstaking address must be set before building the transaction'
);
}

this._instructionsData = [];
if (this._unstakingAddress) {
assert(
this._amount,
'If an unstaking address is given then a partial amount to unstake must also be set before building the transaction'
);
const stakingFundUnstakeAddress: Transfer = {
type: InstructionBuilderTypes.Transfer,
params: {
fromAddress: this._sender,
amount: this._fundUnstakeAddress.toString(),
toAddress: this._unstakingAddress,
},
};
this._instructionsData.push(stakingFundUnstakeAddress);
}

const stakingDeactivateData: StakingDeactivate = {
Expand All @@ -152,7 +162,7 @@ export class StakingDeactivateBuilder extends TransactionBuilder {
unstakingAddress: this._unstakingAddress,
},
};
this._instructionsData = [stakingDeactivateData];
this._instructionsData.push(stakingDeactivateData);
}
return await super.buildImplementation();
}
Expand Down
17 changes: 10 additions & 7 deletions modules/sdk-coin-sol/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,13 +265,16 @@ export function getTransactionType(transaction: SolTransaction): TransactionType
return TransactionType.StakingAuthorizeRaw;
}
validateIntructionTypes(instructions);
for (const instruction of instructions) {
const instructionType = getInstructionType(instruction);
if (
instructionType === ValidInstructionTypesEnum.Transfer ||
instructionType === ValidInstructionTypesEnum.TokenTransfer
) {
return TransactionType.Send;
// check if deactivate instruction does not exist because deactivate can be include a transfer instruction
if (instructions.filter((instruction) => getInstructionType(instruction) === 'Deactivate').length == 0) {
for (const instruction of instructions) {
const instructionType = getInstructionType(instruction);
if (
instructionType === ValidInstructionTypesEnum.Transfer ||
instructionType === ValidInstructionTypesEnum.TokenTransfer
) {
return TransactionType.Send;
}
}
}
if (matchTransactionTypeByInstructionsOrder(instructions, walletInitInstructionIndexes)) {
Expand Down
2 changes: 1 addition & 1 deletion modules/sdk-coin-sol/test/resources/sol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export const STAKING_MULTI_DELEGATE_SIGNED_TX =
'ARlBQmd/nHoyBEFrQYaWit7K/DDGfUHBTqi94bEVP85IBSZa2OBp7tMp5QtjK/aVNxMH2JXZDB4IkIsuHQrahAwBAAUIReV5vPklPPaLR9/x+zo6XCwhusWyPAmuEqbgVWvwi0Fiey+6ASdh+bkZvPlMu0ydyAUdnwkymTFNOUkjMmi96ei34u0+Js6HJSmz3nbL/iQ7OtCrUXASumOwh8mHoyopsfl5eJocR9E8SNh6LycXEOt/kyYJURcxYpC9bCna9dkGodgXkTdUKpg0N73+KnqyVX9TXIp4citopJ3AAAAAAAah2BelAgULaAeR5s5tuI4eW3FQ9h/GeQpOtNEAAAAABqfVFxjHdMkoVmOYaR1etoteuKObS21cc1VbIQAAAAAGp9UXGTWE0P7tm7NDHRMga+VEKBtXuFZsxTdf9AAAAOMy2vkvq+zotj/3pEAF5f39mvoVh1a2HFqV+QSzuNCBAgQGAQMGBwUABAIAAAAEBgIDBgcFAAQCAAAA';

export const STAKING_PARTIAL_DEACTIVATE_SIGNED_TX =
'AvFKse62LaYOlwErP8geaXC89KdL4oOu2QIOzaIZJoAxyExvLg6GjQ+Q6IHTq1Yl/k1av4fBK0ypSz+CqT6xJg4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgADBkXlebz5JTz2i0ff8fs6OlwsIbrFsjwJrhKm4FVr8ItB6Lfi7T4mzoclKbPedsv+JDs60KtRcBK6Y7CHyYejKiliey+6ASdh+bkZvPlMu0ydyAUdnwkymTFNOUkjMmi96QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABqHYF5E3VCqYNDe9/ip6slV/U1yKeHIraKSdwAAAAAAGp9UXGMd0yShWY5hpHV62i164o5tLbVxzVVshAAAAAOMy2vkvq+zotj/3pEAF5f39mvoVh1a2HFqV+QSzuNCBBAMBAQwIAAAAyAAAAAAAAAADAQEkAQAAAAah2BeRN1QqmDQ3vf4qerJVf1NcinhyK2ikncAAAAAABAMCAQAMAwAAAKCGAQAAAAAABAMBBQAEBQAAAA==';
'AsXl8GRXfj2YfeAW6IsO6qHeNgMcNJ4kTW2jXBHMsc4paBn54pl3nRBCzUd5CrdpTw1BhZ+f4Pk2TCqmZHxRggoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgADBkXlebz5JTz2i0ff8fs6OlwsIbrFsjwJrhKm4FVr8ItB6Lfi7T4mzoclKbPedsv+JDs60KtRcBK6Y7CHyYejKiliey+6ASdh+bkZvPlMu0ydyAUdnwkymTFNOUkjMmi96QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABqHYF5E3VCqYNDe9/ip6slV/U1yKeHIraKSdwAAAAAAGp9UXGMd0yShWY5hpHV62i164o5tLbVxzVVshAAAAAOMy2vkvq+zotj/3pEAF5f39mvoVh1a2HFqV+QSzuNCBBQMCAAEMAgAAAIDVIgAAAAAAAwEBDAgAAADIAAAAAAAAAAMBASQBAAAABqHYF5E3VCqYNDe9/ip6slV/U1yKeHIraKSdwAAAAAAEAwIBAAwDAAAAoIYBAAAAAAAEAwEFAAQFAAAA';

export const STAKING_DEACTIVATE_SIGNED_TX_WITH_MEMO =
'AeTmxB2u4ZimCO5xY3MZujxpifmALh/huWguQyojBCerRAUHX+z3atYMIDLPVSmPNdhfznnHZzkZPsCwfQW1mg4BAAMFReV5vPklPPaLR9/x+zo6XCwhusWyPAmuEqbgVWvwi0Fiey+6ASdh+bkZvPlMu0ydyAUdnwkymTFNOUkjMmi96QVKU1qZKSEGTSTocWDaOHx8NbXdvJK7geQfqEBBBUSNBqHYF5E3VCqYNDe9/ip6slV/U1yKeHIraKSdwAAAAAAGp9UXGMd0yShWY5hpHV62i164o5tLbVxzVVshAAAAAOMy2vkvq+zotj/3pEAF5f39mvoVh1a2HFqV+QSzuNCBAgMDAQQABAUAAAACAA9UZXN0IGRlYWN0aXZhdGU=';
Expand Down
61 changes: 61 additions & 0 deletions modules/sdk-coin-sol/test/unit/instructionParamsFactory.staking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,59 @@ describe('Instruction Parser Staking Tests: ', function () {
'Invalid partial deactivate stake transaction, missing split stake account instruction'
);
});
it('Should throw an error if the transfer instruction is missing for partial', () => {
const fromAccount = new PublicKey(testData.authAccount.pub);
const nonceAccount = testData.nonceAccount.pub;
const stakingAccount = new PublicKey(testData.stakeAccount.pub);
const splitStakeAccount = new PublicKey(testData.splitStakeAccount.pub);
const memo = 'test memo';

// Instructions
const nonceAdvanceInstruction = SystemProgram.nonceAdvance({
noncePubkey: new PublicKey(nonceAccount),
authorizedPubkey: fromAccount,
});

const allocateInstruction = SystemProgram.allocate({
accountPubkey: splitStakeAccount,
space: StakeProgram.space,
});

const splitInstructions = StakeProgram.split({
stakePubkey: stakingAccount,
authorizedPubkey: fromAccount,
splitStakePubkey: splitStakeAccount,
lamports: 100000,
}).instructions;

const assignInstruction = SystemProgram.assign({
accountPubkey: splitStakeAccount,
programId: StakeProgram.programId,
});

const stakingDeactivateInstructions = StakeProgram.deactivate({
authorizedPubkey: fromAccount,
stakePubkey: splitStakeAccount,
}).instructions;

const memoInstruction = new TransactionInstruction({
keys: [],
programId: new PublicKey(MEMO_PROGRAM_PK),
data: Buffer.from(memo),
});

const instructions = [
nonceAdvanceInstruction,
allocateInstruction,
assignInstruction,
...splitInstructions,
...stakingDeactivateInstructions,
memoInstruction,
];
should(() => instructionParamsFactory(TransactionType.StakingDeactivate, instructions)).throw(
'Invalid partial deactivate stake transaction, missing funding of unstake address instruction'
);
});

it('Should throw an error if the allocated account does not match the assigned account', () => {
const fromAccount = new PublicKey(testData.authAccount.pub);
Expand Down Expand Up @@ -894,6 +947,13 @@ describe('Instruction Parser Staking Tests: ', function () {
authorizedPubkey: fromAccount,
});

// transfer
const transferInstruction = SystemProgram.transfer({
fromPubkey: new PublicKey(fromAccount),
toPubkey: new PublicKey(splitStakeAccount),
lamports: parseInt((2282880).toString(), 10),
});

const allocateInstruction = SystemProgram.allocate({
accountPubkey: splitStakeAccount,
space: StakeProgram.space,
Expand Down Expand Up @@ -945,6 +1005,7 @@ describe('Instruction Parser Staking Tests: ', function () {

const instructions = [
nonceAdvanceInstruction,
transferInstruction,
allocateInstruction,
assignInstruction,
...splitInstructions,
Expand Down

0 comments on commit 4b2a941

Please sign in to comment.