Skip to content

Unstake and Exit

This tutorial covers all the ways to exit your KAT staking positions, including the cooldown period, exit fee schedule, and rage quit option.

Goal

By the end of this tutorial, you’ll understand:

  • The 45-day cooldown and exit fee decay curve
  • How to begin, complete, or cancel a withdrawal
  • The rage quit option for immediate exit
  • How to exit an avKAT position
  • How exit fees are calculated

Prerequisites

  • A vKAT NFT or avKAT tokens

Stabilization Window (Day 0-60)

Period Max Exit Fee Rationale
Day 0-14 80% Full stabilization. Price discovery protected.
Day 15-30 60% Easing. Still prohibitive for short-term exits.
Day 31-45 45% Continued easing.
Day 46-60 30% Approaching steady state.
Day 61+ (steady state discovery) Steady state will be defined.

Exit fees collected during this window are accumulated and distributed to Founding Stakers after Day 60. From Day 61 onward, exit fees are distributed in real-time to all active vKAT holders.

avKAT holders: During the stabilization window, avKAT is tradeable on the DEX, the rate users will receive will be based on market demand.

Steady-State Exit Fee Schedule (Day 61+ after steady state is discovered)

After the stabilization window (sometime after day 61), exiting a vKAT position incurs a fee that decays linearly over a 45-day cooldown period. The earlier you withdraw, the higher the fee.

Timing Fee Example (1,000 KAT staked)
Immediate (rage quit) 25% Receive 750 KAT
Day 7 ~21.5% Receive ~785 KAT
Day 15 ~16.7% Receive ~833 KAT
Day 30 ~7.5% Receive ~925 KAT
Day 45 (full cooldown) 2.5% Receive 975 KAT

The fee formula:

feePercent = 25% - ((25% - 2.5%) × daysWaited / 45)
received = staked × (1 - feePercent)

Key constants:

const EXIT_FEE = {
  MIN_FEE_BPS: 250n,            // 2.5% (after 45 days)
  MAX_FEE_BPS: 2500n,           // 25% (immediate)
  COOLDOWN_PERIOD: 3_888_000,   // 45 days in seconds
  BPS_DENOMINATOR: 10_000n,
};
graph LR
    A["Begin Withdrawal"] --> B["45-Day Cooldown"]
    B --> C["Withdraw (2.5% fee)"]
    A --> D["Rage Quit (25% fee)"]
    B --> E["Cancel Withdrawal"]
    E --> F["Resume Staking"]

Contract Addresses

const VOTING_ESCROW = "0x4d6fC15Ca6258b168225D283262743C623c13Ead";
const GAUGE_VOTER = "0x5e755A3C5dc81A79DE7a7cEF192FFA60964c9352";
const VAULT = "0x7231dbaCdFc968E07656D12389AB20De82FbfCeB";
const EXIT_QUEUE = "0x6dE9cAAb658C744aD337Ca5d92D084c97ffF578d";

ABIs

VotingEscrow ABI (click to expand)
const votingEscrowAbi = [
  {
    name: "beginWithdrawal",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [{ name: "tokenId", type: "uint256" }],
    outputs: [],
  },
  {
    name: "withdraw",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [{ name: "tokenId", type: "uint256" }],
    outputs: [],
  },
  {
    name: "cancelWithdrawalRequest",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [{ name: "tokenId", type: "uint256" }],
    outputs: [],
  },
  {
    name: "resetVotesAndBeginWithdrawal",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [{ name: "tokenId", type: "uint256" }],
    outputs: [],
  },
  {
    name: "isVoting",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "tokenId", type: "uint256" }],
    outputs: [{ type: "bool" }],
  },
  {
    name: "locked",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "tokenId", type: "uint256" }],
    outputs: [
      { name: "amount", type: "uint256" },
      { name: "start", type: "uint256" },
    ],
  },
  {
    name: "ownedTokens",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "owner", type: "address" }],
    outputs: [{ type: "uint256[]" }],
  },
] as const;
Exit Queue ABI (click to expand)
const exitQueueAbi = [
  {
    name: "getQueueEntry",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "tokenId", type: "uint256" }],
    outputs: [
      { name: "holder", type: "address" },
      { name: "queuedAt", type: "uint256" },
      { name: "cooldown", type: "uint256" },
    ],
  },
] as const;
GaugeVoter ABI (click to expand)
const gaugeVoterAbi = [
  {
    name: "reset",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [{ name: "tokenId", type: "uint256" }],
    outputs: [],
  },
] as const;
avKAT Vault ABI (click to expand)
const vaultAbi = [
  {
    name: "balanceOf",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "account", type: "address" }],
    outputs: [{ type: "uint256" }],
  },
  {
    name: "convertToAssets",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "shares", type: "uint256" }],
    outputs: [{ type: "uint256" }],
  },
  {
    name: "withdrawTokenId",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "assets", type: "uint256" },
      { name: "receiver", type: "address" },
      { name: "owner", type: "address" },
    ],
    outputs: [{ type: "uint256" }],
  },
  {
    name: "previewRedeem",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "shares", type: "uint256" }],
    outputs: [{ type: "uint256" }],
  },
] as const;

Exit Path 1: Standard Withdrawal (vKAT)

The standard exit process has three steps: reset votes, begin cooldown, and withdraw after 45 days.

Step 1: Reset Votes (If Voting)

If your vKAT is actively voting on gauges, you must reset your votes before beginning withdrawal. You can either reset votes separately or use the combined function:

Option A: Reset and begin withdrawal in one transaction:

import { formatEther, parseEther } from "viem";

const tokenId = 42n; // Your vKAT token ID

const hash = await walletClient.writeContract({
  address: VOTING_ESCROW,
  abi: votingEscrowAbi,
  functionName: "resetVotesAndBeginWithdrawal",
  args: [tokenId],
});

await publicClient.waitForTransactionReceipt({ hash });
console.log("Votes reset and cooldown started");

Option B: Reset votes first, then begin withdrawal:

// Check if voting
const isVoting = await publicClient.readContract({
  address: VOTING_ESCROW,
  abi: votingEscrowAbi,
  functionName: "isVoting",
  args: [tokenId],
});

if (isVoting) {
  const resetHash = await walletClient.writeContract({
    address: GAUGE_VOTER,
    abi: gaugeVoterAbi,
    functionName: "reset",
    args: [tokenId],
  });
  await publicClient.waitForTransactionReceipt({ hash: resetHash });
  console.log("Votes reset");
}

Step 2: Begin Withdrawal

Start the 45-day cooldown:

const beginHash = await walletClient.writeContract({
  address: VOTING_ESCROW,
  abi: votingEscrowAbi,
  functionName: "beginWithdrawal",
  args: [tokenId],
});

await publicClient.waitForTransactionReceipt({ hash: beginHash });
console.log("Cooldown started");

Step 3: Check Withdrawal Status

Monitor your cooldown progress:

const [holder, queuedAt, cooldown] = await publicClient.readContract({
  address: EXIT_QUEUE,
  abi: exitQueueAbi,
  functionName: "getQueueEntry",
  args: [tokenId],
});

const cooldownEnd = new Date((Number(queuedAt) + Number(cooldown)) * 1000);
const now = Date.now();
const remaining = cooldownEnd.getTime() - now;

console.log(`Queued at: ${new Date(Number(queuedAt) * 1000).toLocaleDateString()}`);
console.log(`Cooldown ends: ${cooldownEnd.toLocaleDateString()}`);

if (remaining > 0) {
  const daysLeft = Math.ceil(remaining / (1000 * 60 * 60 * 24));
  console.log(`${daysLeft} days remaining`);
} else {
  console.log("Cooldown complete — ready to withdraw!");
}

Step 4: Complete Withdrawal

After the cooldown period (or earlier, with a higher fee):

const withdrawHash = await walletClient.writeContract({
  address: VOTING_ESCROW,
  abi: votingEscrowAbi,
  functionName: "withdraw",
  args: [tokenId],
});

await publicClient.waitForTransactionReceipt({ hash: withdrawHash });
console.log("Withdrawal complete! KAT returned to your wallet.");

Cancel a Withdrawal

If you change your mind during the cooldown, you can cancel and resume staking:

const cancelHash = await walletClient.writeContract({
  address: VOTING_ESCROW,
  abi: votingEscrowAbi,
  functionName: "cancelWithdrawalRequest",
  args: [tokenId],
});

await publicClient.waitForTransactionReceipt({ hash: cancelHash });
console.log("Withdrawal cancelled. Voting power restored.");

Exit Path 2: Rage Quit (Immediate, 25% Fee)

If you need your KAT immediately and are willing to pay the maximum 25% fee, you can begin withdrawal and immediately complete it in two transactions:

// Step 1: Begin withdrawal (or resetVotesAndBeginWithdrawal if voting)
const beginHash = await walletClient.writeContract({
  address: VOTING_ESCROW,
  abi: votingEscrowAbi,
  functionName: "beginWithdrawal",
  args: [tokenId],
});
await publicClient.waitForTransactionReceipt({ hash: beginHash });

// Step 2: Immediately withdraw (25% fee applies)
const withdrawHash = await walletClient.writeContract({
  address: VOTING_ESCROW,
  abi: votingEscrowAbi,
  functionName: "withdraw",
  args: [tokenId],
});
await publicClient.waitForTransactionReceipt({ hash: withdrawHash });

console.log("Rage quit complete. KAT returned (minus 25% fee).");

Warning

Rage quit incurs the maximum exit fee of 25%. For 1,000 KAT staked, you would receive only 750 KAT. Consider whether waiting for the cooldown to reduce fees is worthwhile for your situation.

Exit Path 3: avKAT to KAT

If you hold avKAT vault tokens, there are two exit strategies:

Option A: Sell on a DEX (Instant, No Cooldown)

avKAT is an ERC-4626 vault token, which means it implements the full ERC-20 interface. It is transferable, tradeable, and compatible with any DEX that has liquidity. No cooldown or exit fee applies, but you are subject to market slippage.

Option B: Redeem Through the Vault (45-Day Cooldown)

This converts your avKAT tokens into a new vKAT NFT and starts the standard cooldown process:

// Step 1: Check your avKAT balance and its KAT value
const avkatTokens = await publicClient.readContract({
  address: VAULT,
  abi: vaultAbi,
  functionName: "balanceOf",
  args: [account.address],
});

const katValue = await publicClient.readContract({
  address: VAULT,
  abi: vaultAbi,
  functionName: "convertToAssets",
  args: [avkatTokens],
});

console.log(`avKAT tokens: ${formatEther(avkatTokens)}`);
console.log(`Underlying KAT: ${formatEther(katValue)}`);

// Step 2: Withdraw avKAT tokens as a new vKAT NFT
const withdrawHash = await walletClient.writeContract({
  address: VAULT,
  abi: vaultAbi,
  functionName: "withdrawTokenId",
  args: [katValue, account.address, account.address],
});

const receipt = await publicClient.waitForTransactionReceipt({
  hash: withdrawHash,
});
console.log("Received vKAT NFT!", receipt);

// Step 3: Begin cooldown on the new vKAT NFT
// (Get the new token ID from the transaction logs, then begin withdrawal)
const newTokenId = /* extract from receipt logs */;

const beginHash = await walletClient.writeContract({
  address: VOTING_ESCROW,
  abi: votingEscrowAbi,
  functionName: "beginWithdrawal",
  args: [newTokenId],
});
await publicClient.waitForTransactionReceipt({ hash: beginHash });
console.log("Cooldown started on new vKAT NFT");

// Step 4: Wait 45 days, then withdraw
// (after cooldown)
const finalHash = await walletClient.writeContract({
  address: VOTING_ESCROW,
  abi: votingEscrowAbi,
  functionName: "withdraw",
  args: [newTokenId],
});
await publicClient.waitForTransactionReceipt({ hash: finalHash });
console.log("KAT returned to wallet");

Calculating Exit Fees

You can calculate the expected exit fee at any point during the cooldown:

function calculateExitFee(
  amount: bigint,
  secondsInCooldown: bigint
): { fee: bigint; netAmount: bigint; feePercentage: number } {
  const MIN_FEE_BPS = 250n;
  const MAX_FEE_BPS = 2500n;
  const COOLDOWN_SECONDS = 3_888_000n; // 45 days
  const BPS_DENOMINATOR = 10_000n;

  // Clamp to cooldown period
  const elapsed = secondsInCooldown > COOLDOWN_SECONDS
    ? COOLDOWN_SECONDS
    : secondsInCooldown;

  // Linear interpolation: fee decreases from MAX to MIN over cooldown
  const feeBps = MAX_FEE_BPS -
    ((MAX_FEE_BPS - MIN_FEE_BPS) * elapsed) / COOLDOWN_SECONDS;

  const fee = (amount * feeBps) / BPS_DENOMINATOR;
  const netAmount = amount - fee;
  const feePercentage = Number(feeBps) / 100;

  return { fee, netAmount, feePercentage };
}

// Example usage:
const amount = parseEther("1000");
const daysWaited = 15n;
const secondsWaited = daysWaited * 24n * 60n * 60n;

const { fee, netAmount, feePercentage } = calculateExitFee(amount, secondsWaited);

console.log(`After ${daysWaited} days:`);
console.log(`  Fee: ${formatEther(fee)} KAT (${feePercentage}%)`);
console.log(`  You receive: ${formatEther(netAmount)} KAT`);

Reading Your Full Position

Get a complete summary of all your KAT positions:

const KAT_ADDRESS = "0x7f1f4b4b29f5058fa32cc7a97141b8d7e5abdc2d";

// KAT balance
const katBalance = await publicClient.readContract({
  address: KAT_ADDRESS,
  abi: [{ name: "balanceOf", type: "function", stateMutability: "view",
    inputs: [{ name: "account", type: "address" }], outputs: [{ type: "uint256" }] }],
  functionName: "balanceOf",
  args: [account.address],
});

// vKAT NFTs
const tokenIds = await publicClient.readContract({
  address: VOTING_ESCROW,
  abi: votingEscrowAbi,
  functionName: "ownedTokens",
  args: [account.address],
});

// avKAT tokens
const avkatTokens = await publicClient.readContract({
  address: VAULT,
  abi: vaultAbi,
  functionName: "balanceOf",
  args: [account.address],
});

const avkatValue = await publicClient.readContract({
  address: VAULT,
  abi: vaultAbi,
  functionName: "convertToAssets",
  args: [avkatTokens],
});

console.log(`KAT Balance: ${formatEther(katBalance)}`);
console.log(`vKAT NFTs: ${tokenIds.length}`);
console.log(`avKAT Tokens: ${formatEther(avkatTokens)}`);
console.log(`avKAT Value: ${formatEther(avkatValue)} KAT`);

Using wagmi (React)

Here’s a React component showing the unstaking flow:

import { useWriteContract, useWaitForTransactionReceipt, useReadContract } from "wagmi";

const VOTING_ESCROW = "0x4d6fC15Ca6258b168225D283262743C623c13Ead";

function UnstakeVKAT({ tokenId }: { tokenId: bigint }) {
  const { writeContract, data: hash, isPending } = useWriteContract();
  const { isLoading, isSuccess } = useWaitForTransactionReceipt({ hash });

  // Check if voting
  const { data: isVoting } = useReadContract({
    address: VOTING_ESCROW,
    abi: votingEscrowAbi,
    functionName: "isVoting",
    args: [tokenId],
  });

  const handleResetAndBegin = () => {
    writeContract({
      address: VOTING_ESCROW,
      abi: votingEscrowAbi,
      functionName: "resetVotesAndBeginWithdrawal",
      args: [tokenId],
    });
  };

  const handleBeginWithdrawal = () => {
    writeContract({
      address: VOTING_ESCROW,
      abi: votingEscrowAbi,
      functionName: "beginWithdrawal",
      args: [tokenId],
    });
  };

  const handleWithdraw = () => {
    writeContract({
      address: VOTING_ESCROW,
      abi: votingEscrowAbi,
      functionName: "withdraw",
      args: [tokenId],
    });
  };

  const handleCancel = () => {
    writeContract({
      address: VOTING_ESCROW,
      abi: votingEscrowAbi,
      functionName: "cancelWithdrawalRequest",
      args: [tokenId],
    });
  };

  return (
    <div>
      {isVoting ? (
        <button onClick={handleResetAndBegin} disabled={isPending}>
          Reset Votes & Begin Withdrawal
        </button>
      ) : (
        <button onClick={handleBeginWithdrawal} disabled={isPending}>
          Begin Withdrawal
        </button>
      )}
      <button onClick={handleWithdraw} disabled={isPending}>
        Complete Withdrawal
      </button>
      <button onClick={handleCancel} disabled={isPending}>
        Cancel
      </button>
    </div>
  );
}

Summary

Exit Path Time Fee Steps
vKAT → Standard 45 days 2.5% Reset votes → Begin → Wait → Withdraw
vKAT → Rage Quit Instant 25% Begin + Withdraw immediately
vKAT → Early Exit 1–44 days 25%–2.5% Begin → Wait partially → Withdraw
avKAT → DEX Instant Market slippage Sell on DEX
avKAT → Vault Redeem 45 days 2.5% Redeem as vKAT → Standard exit