Skip to main content
A vesting schedule policy that manages token distribution over time with a 12-month cliff continuous release for the remaining 3 years. This is accomplished by enforcing a minimum balance on the wallet consistent with the unvested amount remaining.

Policy JSON

{
  "Policy": "Vesting Schedule",
  "Description": "4-year vesting schedule with 12-month cliff and monthly distribution",
  "PolicyType": "open",
  "CallingFunctions": [
    {
      "Name": "Transfer",
      "FunctionSignature": "transfer(address to, uint256 value)",
      "EncodedValues": "address to, uint256 value, uint256 senderBalance"
    },
    {
      "Name": "TransferFrom",
      "FunctionSignature": "transferFrom(address from, address to, uint256 value)",
      "EncodedValues": "address from, address to, uint256 value, uint256 senderBalance"
    }
  ],
  "ForeignCalls": [],
  "MappedTrackers": [
    {
      "Name": "VestAmount",
      "KeyType": "address",
      "ValueType": "uint256",
      "InitialKeys": [],
      "InitialValues": []
    },
    {
      "Name": "VestStart",
      "KeyType": "address",
      "ValueType": "uint256",
      "InitialKeys": [],
      "InitialValues": []
    }
  ],
  "Trackers": [],
  "Rules": [
    {
      "Name": "Enforce Vesting",
      "Description": "Ensure 12-month cliff has passed",
      "Condition": "(GV:BLOCK_TIMESTAMP - TR:VestStart(GV:MSG_SENDER) > 31536000) AND (senderBalance - value) > ((TR:VestAmount(GV:MSG_SENDER) * ((TR:VestStart(GV:MSG_SENDER) + 126144000) - GV:BLOCK_TIMESTAMP) / 126144000))",
      "PositiveEffects": [],
      "NegativeEffects": ["revert(\"Still in cliff period\")"],
      "CallingFunction": "Transfer"
    },
    {
      "Name": "Enforce Vesting TransferFrom",
      "Description": "Ensure 12-month cliff has passed",
      "Condition": "(GV:BLOCK_TIMESTAMP - TR:VestStart(from) > 31536000) AND (senderBalance - value) > ((TR:VestAmount(from) * ((TR:VestStart(from) + 126144000) - GV:BLOCK_TIMESTAMP) / 126144000))",
      "PositiveEffects": [],
      "NegativeEffects": ["revert(\"Still in cliff period\")"],
      "CallingFunction": "TransferFrom"
    }
  ]
}

Vesting Schedule Breakdown

  • Year 1: 0% vested (cliff period)
  • Month 12: 25% becomes available (cliff release)
  • Months 13-48: Remaining 75% vests per second
  • Month 48: 100% fully vested

Time Constants

  • Cliff period: 31,536,000 seconds (12 months)
  • Total vesting period: 126,144,000 seconds (4 years)

How to Initialize Vesting

To set up vesting for an investor, you’ll need to initialize the mapped trackers. If you already know the vesting amounts and wallets upfront, you could include them in the initial policy setup. The more likely scenario is that you’ll deploy the token and policy first, and then configure that vesting details post token deployement.
  • add amount and start to the policy
  • actually mint the tokens to the wallets
Setup a vesting file that links wallets to amount and vesting start timestamp like so:
[
  {
    "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266": {
      "start": "1759294800",
      "amount": "2000000000000000000000"
    }
  },
  {
    "0x70997970C51812dc3A010C7d01b50e0d17dc79C8": {
      "start": "1761973200",
      "amount": "1000000000000000000000"
    }
  }
]
In the example above wallet 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 is granted 2000 tokens with a vesting start date of October 1st, 2025. The next wallet is granted 1000 tokens with a start date of November 1, 2025. This JSON will need to be parsed into the appropriate format for mapped trackers. Note that the format above is arbitrary and you can structure however works best for your situation. To update the mapped trackers in your policy to match the above you will need to do the following:
const policyId = YOUR_POLICY_ID;

const vestAmountJson = {
  Name: "VestAmount",
  KeyType: "address",
  ValueType: "uint256",
  InitialKeys: [
    "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
    "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
  ],
  InitialValues: ["2000000000000000000000", "1000000000000000000000"],
};
await RULES_ENGINE.updateMappedTracker(policyId, 1, vestAmountJson);

const vestStartJson = {
  Name: "VestStart",
  KeyType: "address",
  ValueType: "uint256",
  InitialKeys: [
    "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
    "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
  ],
  InitialValues: ["1759294800", "1761973200"],
};
await RULES_ENGINE.updateMappedTracker(policyId, 2, vestStartJson);
You can later add new vesting allotments in the future by adding new keys to the mapped tracker. For example, to add a new token grant of 1000 tokens to wallet 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC beginning on December 1, 2025 you would update the mapped trackers like so:
const policyId = YOUR_POLICY_ID;

const vestAmountJson = {
  Name: "VestAmount",
  KeyType: "address",
  ValueType: "uint256",
  InitialKeys: ["0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"],
  InitialValues: ["1000000000000000000000"],
};
await RULES_ENGINE.updateMappedTracker(policyId, 1, vestAmountJson);

const vestStartJson = {
  Name: "VestStart",
  KeyType: "address",
  ValueType: "uint256",
  InitialKeys: ["0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"],
  InitialValues: ["1759294800"],
};
await RULES_ENGINE.updateMappedTracker(policyId, 2, vestStartJson);
It is possible to edit a previously entered tracker by submitting the same key with a new value. If the key is not matched, then it will be added to the existing set.

Get Current Tracker Values

You can retreive the entire policy and then log out the tracker values like so:
const policy = await RULES_ENGINE.getPolicy(policyId);

console.log(policy!.MappedTrackers[0].InitialKeys);
console.log(policy!.MappedTrackers[0].InitialValues);
console.log(policy!.MappedTrackers[1].InitialKeys);
console.log(policy!.MappedTrackers[1].InitialValues);