token-vesting: Transaction simulation error

Hi everybody,

I’m working with token-vesting and dealing with this error:

ransaction simulation failed: Error processing Instruction 1: custom program error: 0xc
    Program DLxB9dSQtA4WJ49hWFhxqiQkD9v6m67Yfk9voxpxrBs4 invoke [1]
    Program log: Entrypoint
    Program log: Beginning processing
    Program log: Instruction unpacked
    Program log: Instruction: Init
    Program 11111111111111111111111111111111 invoke [2]
    Program 11111111111111111111111111111111 success
    Program DLxB9dSQtA4WJ49hWFhxqiQkD9v6m67Yfk9voxpxrBs4 consumed 9203 of 200000 compute units
    Program DLxB9dSQtA4WJ49hWFhxqiQkD9v6m67Yfk9voxpxrBs4 success
    Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [1]
    Program log: Error: Invalid instruction
    Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 2162 of 200000 compute units
    Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA failed: custom program error: 0xc
index.browser.esm.js?156f:4598 Uncaught (in promise) Error: failed to send transaction: Transaction simulation failed: Error processing Instruction 1: custom program error: 0xc
    at Connection.sendEncodedTransaction (index.browser.esm.js?156f:7055:1)
    at async Connection.sendRawTransaction (index.browser.esm.js?156f:7014:1)
    at async Connection.sendTransaction (index.browser.esm.js?156f:7004:1)

Any ideas?

Thank you.

Do you mind copy pasting the code that you are running?

Yes sure, I download your source code from here: https://github.com/Bonfida/token-vesting/tree/master/js

import { Connection, PublicKey, Keypair, clusterApiUrl } from "@solana/web3.js";
import fs from "fs";
import {
  Numberu64,
  generateRandomSeed,
  signTransactionInstructions,
} from "./utils";
import { Schedule } from "./state";
import { create, TOKEN_VESTING_PROGRAM_ID } from "./main";

/**
 *
 * Simple example of a linear unlock.
 *
 * This is just an example, please be careful using the vesting contract and test it first with test tokens.
 *
 */

/** Path to your wallet */
const WALLET_PATH = "";
// const wallet = Keypair.fromSecretKey(
//   new Uint8Array(JSON.parse(fs.readFileSync(WALLET_PATH).toString())),
// );

const wallet = Keypair.fromSecretKey(
  Uint8Array.from([
   "my mint wallet secret key"
  ])
);

/** There are better way to generate an array of dates but be careful as it's irreversible */
const DATES = [
  new Date(2022, 12),
  new Date(2023, 1),
  new Date(2023, 2),
  new Date(2023, 3),
  new Date(2023, 4),
  new Date(2023, 5),
  new Date(2023, 6),
  new Date(2023, 7),
  new Date(2023, 8),
  new Date(2023, 9),
  new Date(2023, 10),
  new Date(2023, 11),
  new Date(2024, 12),
  new Date(2024, 2),
  new Date(2024, 3),
  // new Date(2024, 4),
  // new Date(2024, 5),
  // new Date(2024, 6),
  // new Date(2024, 7),
  // new Date(2024, 8),
  // new Date(2024, 9),
  // new Date(2024, 10),
  // new Date(2024, 11),
  // new Date(2024, 12),
];

/** Info about the desintation */
const DESTINATION_OWNER = new PublicKey(
  "7EKy974xtje9WWDLPtoo66C8xKVTfdXAkcmsnABtC76s"
);
const DESTINATION_TOKEN_ACCOUNT = new PublicKey(
  "AJuujvVZCS5dEWb1d42QaqJacp423NxspBa6yLyo1eu7"
);

/** Token info */
const MINT = new PublicKey("7MLr9cizjMtd5TH7nwwoA197mthSjZPnaGmvJznMc2XZ");
const DECIMALS = 9;

/** Info about the source */
const SOURCE_TOKEN_ACCOUNT = new PublicKey(
  "4Efv6G3BGwm7Stanakwh6xez3R3FxEGjejgvCSu18n2T"
);

/** Amount to give per schedule */
const AMOUNT_PER_SCHEDULE = 1;

/** Your RPC connection */
const connection = new Connection(clusterApiUrl("devnet"), "confirmed");

/** Do some checks before sending the tokens */
const checks = async () => {
  const tokenInfo = await connection.getParsedAccountInfo(
    DESTINATION_TOKEN_ACCOUNT
  );

  // @ts-ignore
  const parsed = tokenInfo.value.data.parsed;
  if (parsed.info.mint !== MINT.toBase58()) {
    throw new Error("Invalid mint");
  }
  if (parsed.info.owner !== DESTINATION_OWNER.toBase58()) {
    throw new Error("Invalid owner");
  }
  if (parsed.info.tokenAmount.decimals !== DECIMALS) {
    throw new Error("Invalid decimals");
  }
};

/** Function that locks the tokens */
const lock = async () => {
  await checks();
  const schedules: Schedule[] = [];
  for (let date of DATES) {
    schedules.push(
      new Schedule(
        /** Has to be in seconds */
        new Numberu64(date.getTime() / 1_000),
        /** Don't forget to add decimals */
        new Numberu64(AMOUNT_PER_SCHEDULE * Math.pow(10, DECIMALS))
      )
    );
  }
  const seed = generateRandomSeed();

  console.log(`Seed: ${seed}`);
  console.log("minted wallet", wallet.publicKey.toString());
  const instruction = await create(
    connection,
    TOKEN_VESTING_PROGRAM_ID,
    Buffer.from(seed),
    wallet.publicKey,
    wallet.publicKey,
    SOURCE_TOKEN_ACCOUNT,
    DESTINATION_TOKEN_ACCOUNT,
    MINT,
    schedules
  );
  console.log("instruction", instruction);
  console.log("solana version", await connection.getVersion());

  const tx = await signTransactionInstructions(
    connection,
    [wallet],
    wallet.publicKey,
    instruction
  );

  console.log(`Transaction: ${tx}`);
};

export default lock;

I run this lock() function above.
Thank you for helping.

Given that it seems to be the SPL token CPI that fails I am bit suspicious about this part of the code. Can you confirm it is the wallet holding the tokens and not the secret key used to generate the mint?

this is the wallet.

Can you remove the preflight check so you can send a failed tx, it will be easier to inspect the accounts and data passed in the instructions?

export const signTransactionInstructions = async (
  // sign and send transaction
  connection: Connection,
  signers: Array<Keypair>,
  feePayer: PublicKey,
  txInstructions: Array<TransactionInstruction>,
): Promise<string> => {
  const tx = new Transaction();
  tx.feePayer = feePayer;
  tx.add(...txInstructions);
  return await connection.sendTransaction(tx, signers, {
    preflightCommitment: 'single',
  });
};

You mean the preflightCommitment ? If it is, I still get the same error.

Nope, something like

  return await connection.sendTransaction(tx, signers, {
    skipPreflight: false,
  });

So you can have a tx id that I can inspect on-chain

1 Like

I did add the skipPreflight, but the error remains the same.

So I think it’s this line that causing the error: https://github.com/Bonfida/token-vesting/blob/master/js/src/main.ts#L82

I am a bit confused about what is going on here, I just gave it a try with the code you sent (adapted the keys and mint) and it worked fine: Explorer | Solana

There is nothing else you changed in the JS bindings?

1 Like

Also, can you run this script with node instead of your browser?

1 Like

No, I haven’t changed anything. I just run it in a next js project. Maybe, I will run it with node js instead of client side. Will let you know. I gotta sleep 2:12 AM on my side now. Struggle with solana lol. Thank you for supporting man.

I am not an expert in webpack but can it be a buffer polyfill issue? This could explain the Invalid Instruction error.

Please keep me posted about how things go with node.

1 Like

It’s finally running man. Thank you so much. I have a quick question. You offer 3 methods, lock, unlock, and changeDestination, so after the period of vesting ends, we will offer users a withdrawn function. First we will unlock the vested amount, then changeDestination to users’ Wallet. Is that correct?

Thanks for keeping me posted!

When you create a vesting contract, you pass a DESTINATION_TOKEN_ACCOUNT address, this is the address where the tokens will be sent when they unlock, this is the token address of the beneficiary.

The ChangeDestination instruction can be used by the current beneficiary to change the address to which he wants to receive the tokens. This instruction can only be triggered by the current beneficiary because he is required to sign.

Let me know if that answers your question.

1 Like

So basically, when users press Withdrawn button, we just call unlock function, then they will receive the available amount.
For example, I will send to you 100 spl tokens in 100 days, and each 10 days, I will release 10 spl tokens. Then on the 23rd day, you want to withdraw, you can only be able to get 20 tokens. on 45th day, you will get 40 coins, so on…

Do I understand this unlock process correctly?

The change destination functions is used when the you want to transfer the spl tokens while the vesting is still in process.
For example, you got 20 tokens after 20 days of vesting, you want to transfer the rest of tokens (80 tokens) which are still in vesting period, you will press on ChangeDestination Function, and the result is the vesting process will pass to the new wallet address.
Is that correct?

Than you in advance.

Yes, that’s correct. To be more detailed and link what you said to the code, when you create a vesting account you pass an array of Schedule (the struct is defined here https://github.com/Bonfida/token-vesting/blob/master/program/src/state.rs#L9)

pub struct VestingSchedule {
    pub release_time: u64,
    pub amount: u64,
}

You can see that it’s simply an amount and a timestamp. So when someone triggers the unlock instruction, the program loops through all the previous schedules and if current_time > release_time it triggers the transfer of amount tokens to the destination that was passed when creating the vesting account. This destination is hardcoded in the VestingScheduleHeader https://github.com/Bonfida/token-vesting/blob/master/program/src/state.rs#L16 which makes the unlock instruction permissionless.

If you want to change the destination to which the tokens are sent, i.e edit the VestingScheduleHeader you can use the change_destination instruction. This instruction requires the owner of the current destination_address to sign.

:warning: Note that the destination_address must be a token account address not a SOL address

1 Like

Thank you. You are awesome, man. I will try to test unlock and transfer, I will ask you questions if I face other issues. I’m going to build something like streamflow vesting for the private sale. Crossing my fingers, I can do that. Web 3 is totally new for me. Again, thank you a lot.

2 Likes

Hello, after finished transfer all token to destination address how to close contract addresses for withdraw rent fee? is that possible?

Hey @tobi418 !

Currently there is no close instruction so this will not be possible