Skip to main content
A vesting schedule policy that manages token distribution over time with a 12-month cliff followed by 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": "Enforce Vesting",
  "Description": "This policy is used to enforce vesting and allow transfers for those not in the vesting schedule",
  "PolicyType": "open",
  "CallingFunctions": [
    {
      "Name": "Transfer",
      "FunctionSignature": "transfer(address to, uint256 value)",
      "EncodedValues": "address to, uint256 value, uint256 senderBalance, address sender"
    }
  ],
  "ForeignCalls": [],
  "MappedTrackers": [
    {
      "Name": "VestAmount",
      "KeyType": "address",
      "ValueType": "uint256",
      "InitialKeys": [
        "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
        "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
        "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"
      ],
      "InitialValues": [
        "1000000000000000000000",
        "1000000000000000000000",
        "1000000000000000000000"
      ]
    },
    {
      "Name": "VestCliffEnd",
      "KeyType": "address",
      "ValueType": "uint256",
      "InitialKeys": [
        "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
        "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
        "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"
      ],
      "InitialValues": ["1768172992", "1786446592", "1673435392"]
    },
    {
      "Name": "VestEnd",
      "KeyType": "address",
      "ValueType": "uint256",
      "InitialKeys": [
        "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
        "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
        "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"
      ],
      "InitialValues": ["1862780992", "1881140992", "1768172992"]
    }
  ],
  "Trackers": [],
  "Rules": [
    {
      "Name": "Enforce Vesting (Cliff + Linear)",
      "Description": "12-month cliff, then linear vesting until VestEnd (clamped after end).",
      "Condition": "((GV:BLOCK_TIMESTAMP < TR:VestCliffEnd(sender)) AND (senderBalance < [TR:VestAmount(sender) + value])) OR ((GV:BLOCK_TIMESTAMP >= TR:VestCliffEnd(sender)) AND (senderBalance < [[TR:VestAmount(sender) * [TR:VestEnd(sender) - [GV:BLOCK_TIMESTAMP * [GV:BLOCK_TIMESTAMP < TR:VestEnd(sender)] + TR:VestEnd(sender) * [GV:BLOCK_TIMESTAMP >= TR:VestEnd(sender)]]] / 126144000] + value]))",
      "PositiveEffects": ["revert(\"Transfer exceeds unlocked bal\")"],
      "NegativeEffects": [],
      "CallingFunction": "Transfer"
    }
  ]
}

Vesting Schedule Breakdown

  • Inside Year 1: 0% vested (cliff period)
  • End of Year 1: 25% becomes available (cliff release)
  • Months 13-48: Remaining 75% vests on a per second basis
  • End of Month 48: 100% fully vested

Time Constants

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

Testing

The policy above contains some pre-populated values in the mapped trackers to enable testing out the policy. The wallets used match the first 3 wallets in a local anvil chain. Replace these with dev wallets you control when testing on mainnet or testnet.

Vesting Values Explained

WalletCliff EndVest End
0xf39Fd...22661768172992 ~ Jan 11, 20261862780992 ~ Jan 10, 2029
0x70997...79C81786446592 ~ Aug 11, 20261881140992 ~ Aug 11, 2029
0x3C44C...93BC1673435392 ~ Jan 11, 20231768172992 ~ Jan 11, 2026
Looking at the table above we can expect the first 0xf39Fd... wallet to be able to transfer about 25% of the tokens (assuming the date is January 15, 2026). The 0x7099... wallet is still in the initial 12 month cliff and cannot make any transfers that would drop the balance below 1000 tokens. Finally, the 0x3C44... wallet has completed the vesting period and can transfer all of it’s tokens w/o restrictions.

An Important Token Contract Detail

The above policy relies on two extra values to perform the logic that controls the vesting schedule. These are sender (msg.sender) and senderBalance. Having these available to the Rules Engine requires a few minor adjustments to the token contract. Below you can see the transfer function is also passing the balance and msg.sender to the modifier, which then passes it on to the Rules Engine.
    function transfer(
        address to,
        uint256 value
    )
        public
        override
        checkRulesBeforeTransfer(to, value, balanceOf(msg.sender), msg.sender)
        returns (bool)
    {
        return super.transfer(to, value);
    }
You also need to protect the transferFrom function to ensure users can not circumvent the vesting schedule with that function. The demo repo below contains examples of this, which are left out here for brevity.
You can examine this demo repo for more info and details: https://github.com/thrackle-io/testr-token

How to Initialize Vesting

To set up vesting for an investor, you’ll need to populate 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 the vesting details post token deployment.
  • add amount and start to the policy
  • actually mint the tokens to the wallets
To update the mapped trackers in your policy, you can do the following:
const policyId = YOUR_POLICY_ID;

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

const vestCliffJson = {
  Name: "VestCliff",
  KeyType: "address",
  ValueType: "uint256",
  InitialKeys: [
    "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
    "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
  ],
  InitialValues: ["1768172992", "1786446592"],
};
await RULES_ENGINE.updateMappedTracker(policyId, 2, vestCliffJson);

const vestEndJson = {
  Name: "VestEnd",
  KeyType: "address",
  ValueType: "uint256",
  InitialKeys: [
    "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
    "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
  ],
  InitialValues: ["1862780992", "1881140992"],
};
await RULES_ENGINE.updateMappedTracker(policyId, 3, vestEndJson);
You can later add new vesting allotments in the future by adding new keys to the mapped tracker. Using the same approach as above. It is possible to edit a previously entered vesting allotment 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("VestAmount");
console.log(policy!.MappedTrackers[1].InitialKeys);
console.log(policy!.MappedTrackers[1].InitialValues);
console.log("VestCliffEnd");
console.log(policy!.MappedTrackers[2].InitialKeys);
console.log(policy!.MappedTrackers[2].InitialValues);
console.log("VestEnd");
console.log(policy!.MappedTrackers[3].InitialKeys);
console.log(policy!.MappedTrackers[3].InitialValues);
Take a look at the index.ts file in the demo repo for more info an how to update mapped trackers. https://github.com/thrackle-io/testr-token