Vesting Guide
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
totalAllocationamount
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 leasttotalAllocationLONG tokens - User's wallet must approve the factory contract for
totalAllocationamount - 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 totalmust equaltotalAllocation- 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.,2for 2 tokens). The system automatically converts them to wei (18 decimals) for LONG tokens.- The
tokenfield 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
walletAddressmust have at leasttotalAllocationLONG 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 + tranchesmust equaltotalAllocationbefore 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
| Code | Description |
|---|---|
UNAUTHORIZED | User session is missing or expired. |
INSUFFICIENT_ALLOWANCE | Insufficient token approval for the factory contract. |
INVALID_SIGNATURE | Signature verification failed. |
INVALID_INPUT | Input parameters do not match schema (e.g., Zod validation failed). |
UNSUPPORTED_CHAIN | Chain ID is not supported by MCP. |
Contract-Level Errors
| Error Code | Description |
|---|---|
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
- 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
- Token Requirements:
- The
walletAddressmust have at leasttotalAllocationLONG tokens - The
walletAddressmust approve the factory contract fortotalAllocationamount - Tokens are transferred from
walletAddressto the vesting wallet during deployment - The
walletAddressbecomes the owner of the vesting wallet
- The
- Finalization Requirement: The vesting configuration must be finalized before tokens can be released. This is a one-time operation that makes the schedule immutable.
- Allocation Balance: The sum of
tgeAmount + linearAllocation + tranchesTotalmust exactly equaltotalAllocationfor finalization to succeed. - Owner vs Beneficiary:
- Owner: The
walletAddressthat 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.
- Owner: The
- Function Access Control:
- Owner-only functions:
addTranche()- Add a single trancheaddTranches()- Add multiple tranchesfinalizeTranchesConfiguration()- 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
- Owner-only functions:
- 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. - Tranches: Optional step-based vesting amounts that unlock at specific timestamps. Must be added before finalization.