Belong.net Logo
Crypto

Vesting Guide

End-to-end vesting operations for LONG tokens on EVM chains using MCP tools.

Overview

This guide provides instructions for deploying and managing vesting contracts for LONG tokens on EVM-compatible blockchains (e.g., BSC, Ethereum, Polygon) using MCP (Model Context Protocol) tools. The process involves deploying a vesting wallet with customizable schedules (cliff, TGE, and linear vesting) and releasing vested tokens.

Important: The deploy-vesting-wallet tool returns transaction parameters that must be executed on the frontend. The user's wallet must have sufficient LONG tokens and approve the factory contract before deployment.

📋 Prerequisites:

  • Ensure MCP integration is set up with your API key
  • User wallet must have sufficient LONG tokens (at least totalAllocation)
  • User wallet must approve the factory contract for totalAllocation amount

Process Flow

flowchart TD;

A[Start: User Authentication] --> B[Get Deployment Parameters<br/>from Backend via MCP]
B --> C[Check LONG Token Balance]
C --> D{Has Enough<br/>Tokens?}
D -->|No| E[Error: NotEnoughFundsToVest]
D -->|Yes| F[Approve Factory Contract]
F --> G[Execute Transaction<br/>on Frontend]
G --> H[Deploy Vesting Wallet]
H --> I{Configuration<br/>Finalized?}
I -->|No| J[Add Tranches if needed]
J --> K[Finalize Configuration<br/>Owner Only]
K --> L{Allocation<br/>Balanced?}
L -->|No| M[Error: AllocationNotBalanced]
L -->|Yes| N[Release Vested Tokens]
I -->|Yes| N
N --> O[End: Tokens Released]

style A fill:#e1f5fe
style O fill:#c8e6c9
style F fill:#fff3e0
style K fill:#ffeb3b
style E fill:#f44336
style M fill:#f44336

MCP Tools

1. Deploy Vesting Wallet

Tool: deploy-vesting-wallet

Description:
Returns transaction parameters for deploying a time-locked vesting contract for LONG tokens. The transaction must be executed on the frontend by the user's wallet. The user must have sufficient LONG tokens and approve the factory contract before calling this tool.

Important Requirements:

  • User's wallet (walletAddress) must have at least totalAllocation LONG tokens
  • User's wallet must approve the factory contract for totalAllocation amount
  • The transaction is executed on the frontend, not by the backend
  • Backend only provides the signature and transaction parameters

Input:

{
  vestingWalletInfo: {
    startTimestamp: number,           // Start time of vesting (Unix timestamp)
    cliffDurationSeconds: number,     // Cliff period duration in seconds
    durationSeconds: number,          // Total vesting duration in seconds
    beneficiary: string,              // Address receiving vested tokens
    totalAllocation: number,          // Total tokens to vest (human-readable format, e.g., 2 for 2 tokens)
    tgeAmount: number,                // Tokens released at TGE (human-readable format, e.g., 0.5)
    linearAllocation: number,         // Tokens released linearly (human-readable format, e.g., 1.5)
    description: string,              // Description of vesting contract
    // Note: token address is automatically resolved from LONG token on the specified chain
  },
  chainId: number,                    // EVM chain ID
  walletAddress: string,               // User's wallet address (will be owner and msg.sender)
}

Output:

{
  address: string,                    // Factory contract address
  address_token: string,             // LONG token address
  method: object,                    // ABI method details for deployVestingWallet
  params: [                          // Transaction parameters
    string,                          // walletAddress (owner)
    {
      startTimestamp: string,       // Start timestamp (BigInt as string)
      cliffDurationSeconds: string,  // Cliff duration (BigInt as string)
      durationSeconds: string,      // Duration (BigInt as string)
      token: string,                 // Token address
      beneficiary: string,           // Beneficiary address
      totalAllocation: string,       // Total allocation (BigInt as string, in wei)
      tgeAmount: string,             // TGE amount (BigInt as string, in wei)
      linearAllocation: string,      // Linear allocation (BigInt as string, in wei)
      description: string,          // Description
    },
    {
      nonce: string,                 // Signature nonce (BigInt as string)
      deadline: string,              // Signature deadline (BigInt as string)
      signature: string,             // Backend signature (hex string)
    }
  ],
  totalAllocationApprove: string,    // Total allocation in wei (for approve check)
}

Note:

  • The response contains transaction parameters that must be executed on the frontend
  • All BigInt values are converted to strings for JSON compatibility
  • The backend creates a signature, but the transaction is sent from the user's wallet
  • The user's wallet must have tokens and approve before executing the transaction

Contract Functions

The following functions are called directly on the smart contracts (not MCP tools):

Finalize Vesting Configuration

Description:
Before releasing tokens, the vesting configuration must be finalized. This ensures that TGE + linear + tranches == totalAllocation. Only the owner can finalize the configuration.

Requirements:

  • Must be called by the vesting wallet owner
  • TGE amount + linear allocation + tranches total must equal totalAllocation
  • Can only be called once (configuration becomes immutable after finalization)

Implementation:

import { getContract } from "thirdweb";
import { prepareContractCall, sendTransaction } from "thirdweb";

async function finalizeTranchesConfiguration() {
  // Get contract instance
  const contract = getContract({
    client,
    chain: 56, // BSC
    address: vestingWalletAddress,
  });

  // Check if owner
  const owner = await contract.call({
    functionName: "owner",
  });

  if (owner !== currentAddress) {
    throw new Error("Unauthorized: Only owner can finalize");
  }

  // Check if already finalized
  const isFinalized = await contract.call({
    functionName: "tranchesConfigurationFinalized",
  });

  if (!isFinalized) {
    // Prepare and send finalization transaction
    const transaction = prepareContractCall({
      contract,
      method: "function finalizeTranchesConfiguration()",
    });

    await sendTransaction({
      transaction,
      account,
    });
  }
}

Release Vested Tokens

Description: Releases vested LONG tokens to the beneficiary according to the vesting schedule. This is a contract-level function called directly on the vesting wallet. Requires the configuration to be finalized first.

Important: This function can be called by any user (not just the owner). Once the configuration is finalized, anyone can trigger the release of vested tokens to the beneficiary.

Requirements:

  • Configuration must be finalized (tranchesConfigurationFinalized == true)
  • Anyone can call release() - no owner privileges required
  • There must be vested tokens available to release

Implementation:

import { getContract } from "thirdweb";
import { prepareContractCall, sendTransaction } from "thirdweb";

async function release() {
  // Get contract instance
  const contract = getContract({
    client,
    chain: 56, // BSC
    address: vestingWalletAddress,
  });

  // Verify finalization status
  const isFinalized = await contract.call({
    functionName: "tranchesConfigurationFinalized",
  });

  if (!isFinalized) {
    throw new Error(
      "VestingNotFinalized: Configuration must be finalized before release",
    );
  }

  // Prepare and send release transaction
  const transaction = prepareContractCall({
    contract,
    method: "function release()",
  });

  await sendTransaction({
    transaction,
    account,
  });
}

Usage Examples

Deploy Vesting Wallet

import { getContract } from "thirdweb";
import { prepareContractCall, sendTransaction } from "thirdweb";

async function deployVestingWallet() {
  // 1. Get deployment parameters from backend
  const { address, address_token, method, params, totalAllocationApprove } =
    await mcp.tools.call("deploy-vesting-wallet", {
      chainId: 56, // BSC
      walletAddress: userAddress, // User's wallet address
      vestingWalletInfo: {
        startTimestamp: Math.floor(Date.now() / 1000),
        cliffDurationSeconds: 60 * 60 * 24 * 30, // 30 days
        durationSeconds: 60 * 60 * 24 * 365, // 1 year
        beneficiary: "0xBeneficiaryAddress",
        totalAllocation: 2, // 2 tokens (human-readable)
        tgeAmount: 0.5, // 0.5 tokens at TGE
        linearAllocation: 1.5, // 1.5 tokens linearly
        description: "Team vesting wallet",
      },
    });

  // 2. Get token contract instance
  const tokenContract = getContract({
    client,
    chain: 56, // BSC
    address: address_token,
  });

  // 3. Check token balance
  const tokenBalance = await tokenContract.call({
    functionName: "balanceOf",
    params: [userAddress],
  });

  const totalAllocationBigInt = BigInt(totalAllocationApprove);
  if (tokenBalance < totalAllocationBigInt) {
    throw new Error(
      `Insufficient balance: need ${totalAllocationBigInt.toString()}`,
    );
  }

  // 4. Check and approve if needed
  const allowance = await tokenContract.call({
    functionName: "allowance",
    params: [userAddress, address],
  });

  if (allowance < totalAllocationBigInt) {
    const approveTransaction = prepareContractCall({
      contract: tokenContract,
      method: "function approve(address spender, uint256 amount)",
      params: [address, totalAllocationBigInt],
    });

    await sendTransaction({
      transaction: approveTransaction,
      account,
    });
  }

  // 5. Get factory contract instance
  const factoryContract = getContract({
    client,
    chain: 56, // BSC
    address: address,
  });

  // 6. Convert string params to BigInt for transaction
  const convertedParams = [
    params[0], // walletAddress (string)
    {
      ...params[1],
      startTimestamp: BigInt(params[1].startTimestamp),
      cliffDurationSeconds: BigInt(params[1].cliffDurationSeconds),
      durationSeconds: BigInt(params[1].durationSeconds),
      totalAllocation: BigInt(params[1].totalAllocation),
      tgeAmount: BigInt(params[1].tgeAmount),
      linearAllocation: BigInt(params[1].linearAllocation),
    },
    {
      ...params[2],
      nonce: BigInt(params[2].nonce),
      deadline: BigInt(params[2].deadline),
    },
  ];

  // 7. Execute deployment transaction
  const deployTransaction = prepareContractCall({
    contract: factoryContract,
    method:
      "function deployVestingWallet(address _owner, (uint64 startTimestamp, uint64 cliffDurationSeconds, uint64 durationSeconds, address token, address beneficiary, uint256 totalAllocation, uint256 tgeAmount, uint256 linearAllocation, string description) vestingWalletInfo, (uint256 nonce, uint256 deadline, bytes signature) protection)",
    params: convertedParams,
  });

  await sendTransaction({
    transaction: deployTransaction,
    account,
  });

  // 8. Get deployed vesting wallet address
  const vestingWallets = await factoryContract.call({
    functionName: "getVestingWalletInstanceInfos",
    params: [beneficiaryAddress],
  });

  const vestingWalletAddress =
    vestingWallets[vestingWallets.length - 1].vestingWallet;
  console.log("Deployed vesting wallet:", vestingWalletAddress);
}

Important:

  • Token amounts (totalAllocation, tgeAmount, linearAllocation) should be specified in human-readable format (e.g., 2 for 2 tokens). The system automatically converts them to wei (18 decimals) for LONG tokens.
  • The token field is automatically resolved from the LONG token address on the specified chain and should not be included in the request.
  • Requires admin authentication (UserRole.admin).
  • User Requirements: The walletAddress must have at least totalAllocation LONG tokens and must approve the factory contract.
  • Transaction Execution: The transaction is executed on the frontend from the user's wallet, not by the backend.
  • Allocation Balance: tgeAmount + linearAllocation + tranches must equal totalAllocation before finalization.

Get Vesting Wallet Info by Beneficiary

import { getContract } from "thirdweb";

// Get all vesting wallets for a beneficiary (direct contract call, not MCP tool)
const factoryAddress = "0xFactoryAddress"; // Factory contract address
const beneficiary = "0xBeneficiaryAddress";

// Get factory contract instance
const factoryContract = getContract({
  client,
  chain: 56, // BSC
  address: factoryAddress,
});

// Call the function
const vestingWallets = await factoryContract.call({
  functionName: "getVestingWalletInstanceInfos",
  params: [beneficiary],
});

// Returns array of vesting wallet instances for this beneficiary
console.log(vestingWallets);

Finalize Configuration (Owner Only)

import { getContract } from "thirdweb";
import { prepareContractCall, sendTransaction } from "thirdweb";

// Get vesting wallet contract instance
const contract = getContract({
  client,
  chain: 56, // BSC
  address: vestingWalletAddress,
});

// Before releasing tokens, owner must finalize the configuration
const owner = await contract.call({
  functionName: "owner",
});

if (owner !== currentAddress) {
  throw new Error("Unauthorized: Only owner can finalize");
}

// Check if already finalized
const isFinalized = await contract.call({
  functionName: "tranchesConfigurationFinalized",
});

if (!isFinalized) {
  // Prepare finalization transaction
  // This will fail if TGE + linear + tranches != totalAllocation
  const transaction = prepareContractCall({
    contract,
    method: "function finalizeTranchesConfiguration()",
  });

  // Send transaction
  await sendTransaction({
    transaction,
    account,
  });
}

Release Vested Tokens

import { getContract } from "thirdweb";
import { prepareContractCall, sendTransaction } from "thirdweb";

// Get vesting wallet contract instance
const contract = getContract({
  client,
  chain: 56, // BSC
  address: vestingWalletAddress,
});

// Verify finalization before release
const isFinalized = await contract.call({
  functionName: "tranchesConfigurationFinalized",
});

if (!isFinalized) {
  throw new Error("VestingNotFinalized: Configuration must be finalized first");
}

// Prepare release transaction
// IMPORTANT: Anyone can call release() - no owner privileges required
// The function only requires that the configuration is finalized
const transaction = prepareContractCall({
  contract,
  method: "function release()",
});

// Send transaction
await sendTransaction({
  transaction,
  account,
});

Errors

MCP tools return errors in the standard JSON format:

{
  "error": "UNAUTHORIZED",
  "message": "Unauthorized: user session not found or expired."
}

Common Error Codes

MCP Tool Errors

CodeDescription
UNAUTHORIZEDUser session is missing or expired.
INSUFFICIENT_ALLOWANCEInsufficient token approval for the factory contract.
INVALID_SIGNATURESignature verification failed.
INVALID_INPUTInput parameters do not match schema (e.g., Zod validation failed).
UNSUPPORTED_CHAINChain ID is not supported by MCP.

Contract-Level Errors

Error CodeDescription
NotEnoughFundsToVest()The walletAddress (msg.sender) does not have enough LONG tokens. Must have at least totalAllocation.
VestingNotFinalized()Configuration has not been finalized. Call finalizeTranchesConfiguration() first (owner only).
VestingFinalized()Configuration is already finalized and cannot be modified.
Unauthorized()Caller is not the owner of the vesting wallet. Only owner can finalize or add tranches.
AllocationNotBalanced()TGE + linear + tranches does not equal totalAllocation. Cannot finalize until balanced.
OverAllocation()TGE + linear + tranches exceeds totalAllocation. Cannot add more tranches.
NothingToRelease()No vested tokens are available to release at the current time.
TrancheBeforeStart()Tranche timestamp is before the vesting start timestamp.
TrancheAfterEnd()Tranche timestamp is after the vesting end timestamp.
NonMonotonic()Tranche timestamps must be in non-decreasing order.

Important Notes

  1. Backend Signature, Frontend Execution:
    • The backend creates a signature for the vesting wallet deployment
    • The transaction is executed on the frontend from the user's wallet (walletAddress)
    • The user's wallet must have tokens and approve the factory contract
    • This allows separation of concerns: backend handles authorization, frontend handles execution
  2. Token Requirements:
    • The walletAddress must have at least totalAllocation LONG tokens
    • The walletAddress must approve the factory contract for totalAllocation amount
    • Tokens are transferred from walletAddress to the vesting wallet during deployment
    • The walletAddress becomes the owner of the vesting wallet
  3. Finalization Requirement: The vesting configuration must be finalized before tokens can be released. This is a one-time operation that makes the schedule immutable.
  4. Allocation Balance: The sum of tgeAmount + linearAllocation + tranchesTotal must exactly equal totalAllocation for finalization to succeed.
  5. Owner vs Beneficiary:
    • Owner: The walletAddress that deployed the vesting wallet. Only owner can finalize configuration and add tranches.
    • Beneficiary: The address that receives vested tokens when release() is called. Can be different from the owner.
  6. Function Access Control:
    • Owner-only functions:
      • addTranche() - Add a single tranche
      • addTranches() - Add multiple tranches
      • finalizeTranchesConfiguration() - Finalize the vesting configuration
    • Public functions (anyone can call):
      • release() - Release vested tokens to beneficiary (requires finalized configuration)
      • View functions (vestedAmount(), releasable(), etc.) - Read-only, anyone can call
  7. Release Access: Once finalized, anyone can call release() to transfer vested tokens to the beneficiary. The beneficiary does not need to call it themselves, and the owner is not required to trigger releases.
  8. Tranches: Optional step-based vesting amounts that unlock at specific timestamps. Must be added before finalization.
Copyright © 2026