How to Build a Vesting Smart Contract on Cardano
Implement a vesting Cardano smart contract using Aiken and the Mesh TypeScript blockchain SDK. Lock funds with time-based withdrawal for dApp development.
A vesting contract is a Cardano smart contract that locks funds until a specified time, then allows a beneficiary to withdraw them. This is commonly used for employee compensation, token distribution schedules, and escrow arrangements in Cardano dApp development.
This guide walks you through the complete implementation—from writing the Aiken validator to building deposit and withdrawal transactions with the Mesh Cardano SDK.
What Is the On-Chain Vesting Logic?
Define the datum shape to configure vesting parameters.
pub type VestingDatum {
/// POSIX time in milliseconds, e.g. 1672843961000
lock_until: Int,
/// Owner's credentials
owner: ByteArray,
/// Beneficiary's credentials
beneficiary: ByteArray,
}The VestingDatum contains:
lock_until— POSIX timestamp (ms) for lock expirationowner— Owner credentials (public key hash)beneficiary— Beneficiary credentials (public key hash)
The Validator Logic
Define the spend validator that enforces withdrawal rules:
use aiken/transaction.{ScriptContext, Spend}
use vesting/types.{VestingDatum}
use vodka_extra_signatories.{key_signed}
use vodka_validity_range.{valid_after}
validator {
pub fn vesting(datum: VestingDatum, _redeemer: Data, ctx: ScriptContext) {
// In principle, scripts can be used for different purpose (e.g. minting
// assets). Here we make sure it's only used when 'spending' from a eUTxO
when ctx.purpose is {
Spend(_) -> or {
key_signed(ctx.transaction.extra_signatories, datum.owner),
and {
key_signed(ctx.transaction.extra_signatories, datum.beneficiary),
valid_after(ctx.transaction.validity_range, datum.lock_until),
},
}
_ -> False
}
}
}The validator allows withdrawal when either:
- The owner signs the transaction (can withdraw anytime)
- The beneficiary signs AND the current time is after
lock_until
This uses Cardano's validity interval system—the ledger verifies time bounds before script execution, maintaining determinism while enabling time-based logic.
How Do You Test the Contract?
Run comprehensive tests with aiken check. Key test cases:
- Owner can withdraw at any time
- Beneficiary can withdraw after lock period expires
- Beneficiary cannot withdraw before lock period (should fail)
- Unsigned transactions are rejected (should fail)
How Do You Compile the Contract?
Build the Aiken project to generate the Plutus blueprint:
aiken buildThis generates a CIP-0057 Plutus blueprint (plutus.json) that the Mesh TypeScript blockchain SDK can use to interact with the contract.
How Do You Deposit Funds?
The owner creates a transaction that sends funds to the script address with an inline datum specifying the lock period and beneficiary:
const assets: Asset[] = [
{
unit: "lovelace",
quantity: "10000000",
},
];
const lockUntilTimeStamp = new Date();
lockUntilTimeStamp.setMinutes(lockUntilTimeStamp.getMinutes() + 1);
const beneficiary =
"addr_test1qpvx0sacufuypa2k4sngk7q40zc5c4npl337uusdh64kv0uafhxhu32dys6pvn6wlw8dav6cmp4pmtv7cc3yel9uu0nq93swx9";This deposits 10 ADA locked for 1 minute (for testing). Prepare the transaction variables:
const { utxos, walletAddress } = await getWalletInfoForTx();
const { scriptAddr } = getScript();
const { pubKeyHash: ownerPubKeyHash } = deserializeAddress(walletAddress);
const { pubKeyHash: beneficiaryPubKeyHash } = deserializeAddress(beneficiary);Build the deposit transaction using the Mesh Cardano SDK's MeshTxBuilder:
const txBuilder = new MeshTxBuilder({
fetcher: provider,
submitter: provider,
});
await txBuilder
.txOut(scriptAddr, amount)
.txOutInlineDatumValue(
mConStr0([lockUntilTimeStampMs, ownerPubKeyHash, beneficiaryPubKeyHash])
)
.changeAddress(walletAddress)
.selectUtxosFrom(utxos)
.complete();
const unsignedTx = txBuilder.txHex;Sign and submit the transaction:
const signedTx = await wallet.signTx(unsignedTx);
const txHash = await wallet.submitTx(signedTx);Save the returned transaction hash—you'll need it for withdrawal.
How Do You Withdraw Funds?
After the lock period expires, the beneficiary (or owner) can withdraw. First, fetch the UTxO containing the locked funds:
const txHashFromDesposit =
"ede9f8176fe41f0c84cfc9802b693dedb5500c0cbe4377b7bb0d57cf0435200b";
const utxos = await provider.fetchUTxOs(txHash);
const vestingUtxo = utxos[0];Prepare transaction variables and calculate the validity interval (must be after lock expiration):
const { utxos, walletAddress, collateral } = await getWalletInfoForTx();
const { input: collateralInput, output: collateralOutput } = collateral;
const { scriptAddr, scriptCbor } = getScript();
const { pubKeyHash } = deserializeAddress(walletAddress);
const datum = deserializeDatum<VestingDatum>(vestingUtxo.output.plutusData!);
const invalidBefore =
unixTimeToEnclosingSlot(
Math.min(datum.fields[0].int as number, Date.now() - 15000),
SLOT_CONFIG_NETWORK.preprod
) + 1;Build the withdrawal transaction with the script, collateral, and validity interval:
const txBuilder = new MeshTxBuilder({
fetcher: provider,
submitter: provider,
});
await txBuilder
.spendingPlutusScriptV2()
.txIn(
vestingUtxo.input.txHash,
vestingUtxo.input.outputIndex,
vestingUtxo.output.amount,
scriptAddr
)
.spendingReferenceTxInInlineDatumPresent()
.spendingReferenceTxInRedeemerValue("")
.txInScript(scriptCbor)
.txOut(walletAddress, [])
.txInCollateral(
collateralInput.txHash,
collateralInput.outputIndex,
collateralOutput.amount,
collateralOutput.address
)
.invalidBefore(invalidBefore)
.requiredSignerHash(pubKeyHash)
.changeAddress(walletAddress)
.selectUtxosFrom(utxos)
.complete();
const unsignedTx = txBuilder.txHex;Sign with partial signing enabled (true) since we're unlocking from a validator:
const signedTx = await wallet.signTx(unsignedTx, true);
const txHash = await wallet.submitTx(signedTx);The funds are now transferred to the beneficiary's wallet.
What Should You Explore Next?
- View a successful withdrawal transaction on CardanoScan
- Explore the full source code in the aiken-vesting example
- Learn more about smart contract transactions
How to Run Standalone Cardano Scripts with TypeScript
Execute TypeScript scripts directly to interact with the Cardano blockchain using the Mesh Cardano SDK and tsx for JavaScript Web3 development.
How to Fix Node.js Buffer and Polyfill Errors in Browser Projects
Fix 'Buffer is not defined' and Node.js polyfill errors in React, Vue, Svelte, and Angular when using the Mesh Cardano SDK for dApp development.