Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/cli/src/governo/load-lockers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {GovernoContext} from "@stabbleorg/rewarder-sdk";
import {useContext} from "../context";
import {parseKey} from "../utils";
import {PublicKey} from "@solana/web3.js";
import BN from "bn.js";

export function loadLockers(program: Command) {
program
Expand All @@ -16,6 +17,11 @@ export function loadLockers(program: Command) {
const lockers = await governoContext.loadLockers(governo, authorityK);
lockers.forEach(locker => console.log({
...locker,
data: {
...locker.data,
lockedAt: new Date(locker.data.lockedAt.mul(new BN(1_000)).toNumber()),
unlocksAt: new Date(locker.data.unlocksAt.mul(new BN(1_000)).toNumber()),
}
}))
});
}
2 changes: 2 additions & 0 deletions packages/cli/src/vesto/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Command } from "commander";
import { createConfig } from "./config-create";
import { fetchConfig } from "./config-fetch";
import { fetchVault } from "./vault-fetch";
import { createPool } from "./pool-create";
import { redeem } from "./redeem";
import { unstake } from "./unstake";
Expand All @@ -9,6 +10,7 @@ import { claim } from "./claim";
export const setupVestoProgram = (program: Command) => {
createConfig(program);
fetchConfig(program);
fetchVault(program);
createPool(program);
redeem(program);
unstake(program);
Expand Down
52 changes: 52 additions & 0 deletions packages/cli/src/vesto/vault-fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { Command } from "commander";
import { PublicKey } from "@solana/web3.js";
import { VestoContext } from "@stabbleorg/rewarder-sdk";
import { useContext } from "../context";
import { parseKey } from "../utils";

export function fetchVault(program: Command) {
program
.command("vesto-vault-fetch")
.description("find vault governance token account for a specific IOU mint")
.requiredOption("--iou-mint-k <string>", "IOU mint key", parseKey)
.action(async ({ iouMintK }: { iouMintK: PublicKey }) => {
const { provider } = useContext();

const vestoContext = new VestoContext(provider);

// Load all vesting pools and find the one with matching IOU mint
const allAccounts = await vestoContext.program.account.vestingPool.all();

const matchingAccounts = allAccounts.filter(
({ account }) => account.iouMint.equals(iouMintK),
);

if (matchingAccounts.length === 0) {
throw new Error(`No vesting pool found for IOU mint: ${iouMintK.toBase58()}`);
}

if (matchingAccounts.length > 1) {
console.warn(
`Warning: Found ${matchingAccounts.length} pools for IOU mint. Using the first one.`,
);
}

const { publicKey: poolAddress, account: poolData } = matchingAccounts[0];

// Load the config
const config = await vestoContext.loadConfig(poolData.config);

// Calculate vault authority and vault gov token address
const vaultAuthority = config.authorityAddress;
const vaultGovToken = config.getAssociatedTokenAddress(
config.governo.govMintAddress,
);

console.log("Pool address:", poolAddress.toBase58());
console.log("Config address:", config.address.toBase58());
console.log("Vault authority:", vaultAuthority.toBase58());
console.log("Vault gov token:", vaultGovToken.toBase58());
console.log("Governance mint:", config.governo.govMintAddress.toBase58());
});
}

52 changes: 52 additions & 0 deletions packages/sdk/src/programs/vesto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,58 @@ export class VestoContext<
VESTO_PROGRAM_ID,
)[0];
}

async frozenRedeem({
frozenIouTokenAddress,
pool,
altAccounts,
priorityLevel,
maxPriorityMicroLamports,
simulate,
}: TransactionArgs<{
frozenIouTokenAddress: PublicKey;
pool: VestingPool;
}>): Promise<TransactionSignature> {
const instructions: TransactionInstruction[] = [];

const { address: freezeAuthorityGovTokenAddress, instruction: createGovTokenIX } =
await this.getOrCreateAssociatedTokenAddressInstruction(
pool.config.governo.govMintAddress,
);
if (createGovTokenIX) {
instructions.push(createGovTokenIX);
}

instructions.push(
await this.program.methods
.frozenRedeem()
.accountsStrict({
freezeAuthority: this.walletAddress,
frozenIouToken: frozenIouTokenAddress,
iouMint: pool.iouMintAddress,
pool: pool.address,
config: pool.config.address,
governo: pool.config.governo.address,
vaultAuthority: pool.config.authorityAddress,
vaultGovToken: pool.config.getAssociatedTokenAddress(
pool.config.governo.govMintAddress,
),
govMint: pool.config.governo.govMintAddress,
freezeAuthorityGovToken: freezeAuthorityGovTokenAddress,
tokenProgram: TOKEN_PROGRAM_ID,
})
.instruction(),
);

return this.sendSmartTransaction(
instructions,
[],
altAccounts,
priorityLevel,
maxPriorityMicroLamports,
simulate,
);
}
}

export class VestoListener {
Expand Down
12 changes: 12 additions & 0 deletions programs/vesto/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,16 @@ use anchor_lang::prelude::*;
pub enum VestoError {
#[msg("The vesting contract has not unlocked yet")]
Locked,
#[msg("Invalid freeze authority")]
InvalidFreezeAuthority,
#[msg("Token account is not frozen")]
TokenAccountNotFrozen,
#[msg("Invalid IOU mint")]
InvalidIouMint,
#[msg("Invalid config")]
InvalidConfig,
#[msg("Invalid governance mint")]
InvalidGovMint,
#[msg("Invalid vault authority")]
InvalidVaultAuthority,
}
4 changes: 4 additions & 0 deletions programs/vesto/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,8 @@ pub mod vesto {
pub fn claim_position<'a, 'b, 'c, 'info>(ctx: Context<'_, '_, '_, 'info, ClaimPosition<'info>>) -> Result<()> {
process_claim_position(ctx)
}

pub fn frozen_redeem(ctx: Context<FrozenRedeem>) -> Result<()> {
process_frozen_redeem(ctx)
}
}
150 changes: 150 additions & 0 deletions programs/vesto/src/processor/frozen_redeem.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
use crate::{constant::*, error::*, state::*};
use anchor_lang::prelude::*;
use anchor_spl::token::{
burn, thaw_account, transfer_checked, Burn, Mint, ThawAccount, Token, TokenAccount,
TransferChecked,
};
use governo::state::Governo;

pub fn process_frozen_redeem(ctx: Context<FrozenRedeem>) -> Result<()> {
// Verify the signer is the freeze authority of the IOU mint
require_keys_eq!(
ctx.accounts.iou_mint.freeze_authority.unwrap(),
ctx.accounts.freeze_authority.key(),
VestoError::InvalidFreezeAuthority
);

// Verify the frozen IOU token account is actually frozen
// The thaw_account instruction will fail if not frozen, but we check here for clarity
require!(
matches!(ctx.accounts.frozen_iou_token.state, anchor_spl::token::spl_token::state::AccountState::Frozen),
VestoError::TokenAccountNotFrozen
);

// Verify the IOU token account's mint matches the pool's IOU mint
require_keys_eq!(
ctx.accounts.frozen_iou_token.mint,
ctx.accounts.pool.iou_mint,
VestoError::InvalidIouMint
);

// Verify the pool's config matches the provided config
require_keys_eq!(
ctx.accounts.pool.config,
ctx.accounts.config.key(),
VestoError::InvalidConfig
);

// Get the amount to redeem (all tokens in the frozen account)
let amount_to_redeem = ctx.accounts.frozen_iou_token.amount;

// Thaw the frozen token account
thaw_account(CpiContext::new(
ctx.accounts.token_program.to_account_info(),
ThawAccount {
account: ctx.accounts.frozen_iou_token.to_account_info(),
mint: ctx.accounts.iou_mint.to_account_info(),
authority: ctx.accounts.freeze_authority.to_account_info(),
},
))?;

// Reload the token account to get updated state
ctx.accounts.frozen_iou_token.reload()?;

// Burn all tokens from the thawed account
burn(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
Burn {
mint: ctx.accounts.iou_mint.to_account_info(),
from: ctx.accounts.frozen_iou_token.to_account_info(),
authority: ctx.accounts.freeze_authority.to_account_info(),
},
),
amount_to_redeem,
)?;

// Transfer equivalent amount of gov tokens from vault to the signer
ctx.accounts.config.authority_seeds(|signer_seed| {
transfer_checked(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
TransferChecked {
from: ctx.accounts.vault_gov_token.to_account_info(),
mint: ctx.accounts.gov_mint.to_account_info(),
to: ctx.accounts.freeze_authority_gov_token.to_account_info(),
authority: ctx.accounts.vault_authority.to_account_info(),
},
)
.with_signer(&[&signer_seed]),
amount_to_redeem,
ctx.accounts.governo.decimals,
)
})?;

Ok(())
}

#[derive(Accounts)]
pub struct FrozenRedeem<'info> {
/// The freeze authority of the IOU mint (must be the signer)
#[account(mut)]
pub freeze_authority: Signer<'info>,

/// The frozen IOU token account to redeem
#[account(mut)]
pub frozen_iou_token: Account<'info, TokenAccount>,

/// The IOU mint account
#[account(
constraint = iou_mint.key() == frozen_iou_token.mint @ VestoError::InvalidIouMint,
constraint = iou_mint.freeze_authority.is_some() @ VestoError::InvalidFreezeAuthority,
constraint = iou_mint.freeze_authority.unwrap() == freeze_authority.key() @ VestoError::InvalidFreezeAuthority
)]
pub iou_mint: Account<'info, Mint>,

/// The vesting pool that uses this IOU mint
#[account(
constraint = pool.iou_mint == iou_mint.key() @ VestoError::InvalidIouMint
)]
pub pool: Account<'info, VestingPool>,

/// The vesting config associated with this pool
#[account(
has_one = governo,
constraint = config.key() == pool.config @ VestoError::InvalidConfig
)]
pub config: Account<'info, VestingConfig>,

/// The governo account
#[account(has_one = gov_mint)]
pub governo: Account<'info, Governo>,

/// The vault authority PDA (derived from config)
#[account(
seeds = [VAULT_AUTHORITY_PREFIX, &config.key().to_bytes()],
bump = config.authority_bump
)]
/// CHECK: OK
pub vault_authority: UncheckedAccount<'info>,

/// The vault governance token account (owned by vault_authority)
#[account(
mut,
constraint = vault_gov_token.mint == gov_mint.key() @ VestoError::InvalidGovMint,
constraint = vault_gov_token.owner == vault_authority.key() @ VestoError::InvalidVaultAuthority
)]
pub vault_gov_token: Account<'info, TokenAccount>,

/// The governance mint ($STB tokens)
/// CHECK: OK
pub gov_mint: UncheckedAccount<'info>,

/// The freeze authority's governance token account (where redeemed tokens will be sent)
/// CHECK: OK - Token account is validated in the instruction logic
#[account(mut)]
pub freeze_authority_gov_token: UncheckedAccount<'info>,

pub token_program: Program<'info, Token>,
}

3 changes: 3 additions & 0 deletions programs/vesto/src/processor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ pub use position_redeem::*;

pub mod position_update;
pub use position_update::*;

pub mod frozen_redeem;
pub use frozen_redeem::*;