Skip to content

Commit

Permalink
feat: supporting unlocked with withdrawal period (#6)
Browse files Browse the repository at this point in the history
The underlying codebase requires the user to decide at lockup the Date
of withdrawal.
The intended behaviour, is that the user can decide to lock the tokens
in an "unlocked with withdrawal period" mode, in which it would require
the user to first "initiate the withdrawal", to be able to actually
withdraw the tokens (e.g.) 7 days later.

This is achieved by:
- reverting on withdrawal when locking Date is 0
- adding a new method, identical in arguments to "unlock", that would
revert if Date != 0
- when user calls this method, the locking Date should be `now() + 7
days`
  • Loading branch information
wei3erHase authored Sep 25, 2024
1 parent abec526 commit f387724
Show file tree
Hide file tree
Showing 9 changed files with 553 additions and 187 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ js/lib

/node_modules

js/dist
js/dist
**/wallet.json
50 changes: 10 additions & 40 deletions js/src/example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,32 +20,7 @@ const wallet = Keypair.fromSecretKey(
);

/** 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),
];
const DATE = new Date(2022, 12);

/** Info about the desintation */
const DESTINATION_OWNER = new PublicKey('');
Expand Down Expand Up @@ -86,19 +61,14 @@ const checks = async () => {
/** 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 */
// @ts-ignore
new Numberu64(date.getTime() / 1_000),
/** Don't forget to add decimals */
// @ts-ignore
new Numberu64(AMOUNT_PER_SCHEDULE * Math.pow(10, DECIMALS)),
),
);
}
const schedule: Schedule = new Schedule(
/** Has to be in seconds */
// @ts-ignore
new Numberu64(DATE.getTime() / 1_000),
/** Don't forget to add decimals */
// @ts-ignore
new Numberu64(AMOUNT_PER_SCHEDULE * Math.pow(10, DECIMALS)),
);
const seed = generateRandomSeed();

console.log(`Seed: ${seed}`);
Expand All @@ -112,7 +82,7 @@ const lock = async () => {
SOURCE_TOKEN_ACCOUNT,
DESTINATION_TOKEN_ACCOUNT,
MINT,
schedules,
schedule,
);

const tx = await signAndSendInstructions(connection, [], wallet, instruction);
Expand Down
59 changes: 51 additions & 8 deletions js/src/instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,11 @@ export function createInitInstruction(
vestingProgramId: PublicKey,
payerKey: PublicKey,
vestingAccountKey: PublicKey,
seeds: Array<Buffer | Uint8Array>,
numberOfSchedules: number,
seeds: Array<Buffer | Uint8Array>
): TransactionInstruction {
let buffers = [
Buffer.from(Int8Array.from([0]).buffer),
Buffer.concat(seeds),
// @ts-ignore
new Numberu32(numberOfSchedules).toBuffer(),
];

const data = Buffer.concat(buffers);
Expand Down Expand Up @@ -62,7 +59,7 @@ export function createCreateInstruction(
sourceTokenAccountKey: PublicKey,
destinationTokenAccountKey: PublicKey,
mintAddress: PublicKey,
schedules: Array<Schedule>,
schedule: Schedule,
seeds: Array<Buffer | Uint8Array>,
): TransactionInstruction {
let buffers = [
Expand All @@ -72,9 +69,7 @@ export function createCreateInstruction(
destinationTokenAccountKey.toBuffer(),
];

schedules.forEach(s => {
buffers.push(s.toBuffer());
});
buffers.push(schedule.toBuffer());

const data = Buffer.concat(buffers);
const keys = [
Expand Down Expand Up @@ -158,3 +153,51 @@ export function createUnlockInstruction(
data,
});
}

export function createInitializeUnlockInstruction(
vestingProgramId: PublicKey,
tokenProgramId: PublicKey,
clockSysvarId: PublicKey,
vestingAccountKey: PublicKey,
vestingTokenAccountKey: PublicKey,
destinationTokenAccountKey: PublicKey,
seeds: Array<Buffer | Uint8Array>,
): TransactionInstruction {
const data = Buffer.concat([
Buffer.from(Int8Array.from([3]).buffer),
Buffer.concat(seeds),
]);

const keys = [
{
pubkey: tokenProgramId,
isSigner: false,
isWritable: false,
},
{
pubkey: clockSysvarId,
isSigner: false,
isWritable: false,
},
{
pubkey: vestingAccountKey,
isSigner: false,
isWritable: true,
},
{
pubkey: vestingTokenAccountKey,
isSigner: false,
isWritable: true,
},
{
pubkey: destinationTokenAccountKey,
isSigner: false,
isWritable: true,
},
];
return new TransactionInstruction({
keys,
programId: vestingProgramId,
data,
});
}
54 changes: 49 additions & 5 deletions js/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
createCreateInstruction,
createInitInstruction,
createUnlockInstruction,
createInitializeUnlockInstruction,
} from './instructions';
import { ContractInfo, Schedule } from './state';
import { assert } from 'console';
Expand All @@ -36,7 +37,7 @@ export const TOKEN_VESTING_PROGRAM_ID = new PublicKey(
* @param possibleSourceTokenPubkey The source token account (i.e where locked tokens are originating from), if null it defaults to the ATA
* @param destinationTokenPubkey The destination token account i.e where unlocked tokens will be transfered
* @param mintAddress The mint of the tokens being vested
* @param schedules The array of vesting schedules
* @param schedule The vesting schedule
* @returns An array of `TransactionInstruction`
*/
export async function create(
Expand All @@ -48,7 +49,7 @@ export async function create(
possibleSourceTokenPubkey: PublicKey | null,
destinationTokenPubkey: PublicKey,
mintAddress: PublicKey,
schedules: Array<Schedule>,
schedule: Schedule,
): Promise<Array<TransactionInstruction>> {
// If no source token account was given, use the associated source account
if (possibleSourceTokenPubkey == null) {
Expand Down Expand Up @@ -92,8 +93,7 @@ export async function create(
programId,
payer,
vestingAccountKey,
[seedWord],
schedules.length,
[seedWord]
),
createAssociatedTokenAccountInstruction(
payer,
Expand All @@ -110,7 +110,7 @@ export async function create(
possibleSourceTokenPubkey,
destinationTokenPubkey,
mintAddress,
schedules,
schedule,
[seedWord],
),
];
Expand Down Expand Up @@ -161,6 +161,50 @@ export async function unlock(
return instruction;
}

/**
* This function can be used to initialize the unlock of vested tokens
* @param connection The Solana RPC connection object
* @param programId The token vesting program ID
* @param seedWord Seed words used to derive the vesting account
* @param mintAddress The mint of the vested tokens
* @returns An array of `TransactionInstruction`
*/
export async function initializeUnlock(
connection: Connection,
programId: PublicKey,
seedWord: Buffer | Uint8Array,
mintAddress: PublicKey,
): Promise<Array<TransactionInstruction>> {
seedWord = seedWord.slice(0, 31);
const [vestingAccountKey, bump] = await PublicKey.findProgramAddress(
[seedWord],
programId,
);
seedWord = Buffer.from(seedWord.toString('hex') + bump.toString(16), 'hex');

const vestingTokenAccountKey = await getAssociatedTokenAddress(
mintAddress,
vestingAccountKey,
true,
);

const vestingInfo = await getContractInfo(connection, vestingAccountKey);

let instruction = [
createInitializeUnlockInstruction(
programId,
TOKEN_PROGRAM_ID,
SYSVAR_CLOCK_PUBKEY,
vestingAccountKey,
vestingTokenAccountKey,
vestingInfo.destinationAddress,
[seedWord],
),
];

return instruction;
}

/**
* This function can be used retrieve information about a vesting account
* @param connection The Solana RPC connection object
Expand Down
24 changes: 24 additions & 0 deletions program/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions program/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ arbitrary = { version = "0.4", features = ["derive"], optional = true }
honggfuzz = { version = "0.5", optional = true }

[dev-dependencies]
solana-sdk = "1.5.6"
solana-program-test = "1.5.6"
solana-sdk = "1.18.23"
solana-program-test = "1.18.23"
solana-test-framework = { git = "https://github.com/halbornteam/solana-test-framework", branch = "solana1.18" }
tokio = { version = "1.0", features = ["macros"]}

[lib]
Expand Down
Loading

0 comments on commit f387724

Please sign in to comment.