Skip to content

๐Ÿ”’ Time-Lock Contract Example โ€‹

Lock ERG or tokens until a specific date/time

This example demonstrates a single-interaction transaction pattern where funds are locked until a specific timestamp.

Overview โ€‹

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    TIME-LOCK FLOW                               โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                 โ”‚
โ”‚   DEPOSIT PHASE                 WITHDRAWAL PHASE                โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”               โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                โ”‚
โ”‚   โ”‚   User      โ”‚               โ”‚   User      โ”‚                โ”‚
โ”‚   โ”‚   Wallet    โ”‚               โ”‚   Wallet    โ”‚                โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜               โ””โ”€โ”€โ”€โ”€โ”€โ”€โ–ฒโ”€โ”€โ”€โ”€โ”€โ”€โ”˜                โ”‚
โ”‚          โ”‚                             โ”‚                        โ”‚
โ”‚          โ”‚ Lock funds                  โ”‚ Unlock after date      โ”‚
โ”‚          โ–ผ                             โ”‚                        โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”               โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”                โ”‚
โ”‚   โ”‚  TimeLock   โ”‚   TIME PASSES โ”‚  TimeLock   โ”‚                โ”‚
โ”‚   โ”‚   Box       โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ โ”‚   Box       โ”‚                โ”‚
โ”‚   โ”‚ ๐Ÿ”’ LOCKED   โ”‚               โ”‚ ๐Ÿ”“ UNLOCKED โ”‚                โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜               โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                โ”‚
โ”‚                                                                 โ”‚
โ”‚   Before: unlockTime            After: unlockTime               โ”‚
โ”‚   โŒ Cannot withdraw            โœ… Can withdraw                 โ”‚
โ”‚                                                                 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The Smart Contract โ€‹

scala
{
  // Time-Lock Contract
  // Funds can only be spent after unlockTime by the owner
  
  // Constants (injected at compile time)
  val unlockTime: Long = $unlockTime
  val ownerPk: GroupElement = $ownerPk
  
  // Conditions
  val timeCondition = CONTEXT.preHeader.timestamp >= unlockTime
  val ownerCondition = proveDlog(ownerPk)
  
  // Both must be satisfied
  sigmaProp(timeCondition) && ownerCondition
}

Complete Implementation โ€‹

typescript
import { 
  TransactionBuilder, 
  OutputBuilder,
  ErgoAddress,
  SAFE_MIN_BOX_VALUE,
  type Box
} from "@fleet-sdk/core";
import { compile } from "@fleet-sdk/compiler";

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// ๐Ÿ“ TIME-LOCK CONTRACT
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

const TIME_LOCK_CONTRACT = `
{
  // Time-Lock Contract v1.0
  // Funds locked until specific timestamp
  
  val unlockTime = $unlockTime
  val ownerPk = $ownerPk
  
  // Can only spend after unlock time
  val isUnlocked = CONTEXT.preHeader.timestamp >= unlockTime
  
  // Must be signed by owner
  val isOwner = proveDlog(ownerPk)
  
  sigmaProp(isUnlocked) && isOwner
}
`;

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// ๐Ÿ”ง HELPER FUNCTIONS
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

interface TimeLockParams {
  ownerAddress: string;
  lockAmount: string;
  unlockDate: Date;
}

/**
 * Compile time-lock contract with specific parameters
 */
function compileTimeLockContract(
  ownerPk: string,
  unlockTimestamp: bigint
): string {
  const compiled = compile(TIME_LOCK_CONTRACT, {
    map: {
      unlockTime: unlockTimestamp,
      ownerPk: ownerPk
    }
  });
  
  return compiled.toHex();
}

/**
 * Get public key from Ergo address
 */
function getPublicKeyFromAddress(address: string): string {
  const ergoAddress = ErgoAddress.fromBase58(address);
  const publicKeys = ergoAddress.getPublicKeys();
  
  if (publicKeys.length === 0) {
    throw new Error("Address has no public key (might be P2S address)");
  }
  
  return publicKeys[0];
}

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// ๐Ÿ”’ LOCK FUNDS
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

/**
 * Create a time-locked box
 * 
 * @param inputs - User's input boxes
 * @param params - Lock parameters
 * @param currentHeight - Current blockchain height
 * @returns Unsigned transaction
 */
async function createTimeLockBox(
  inputs: Box<string>[],
  params: TimeLockParams,
  currentHeight: number
) {
  const { ownerAddress, lockAmount, unlockDate } = params;
  
  // 1. Get owner's public key
  const ownerPk = getPublicKeyFromAddress(ownerAddress);
  
  // 2. Convert date to timestamp
  const unlockTimestamp = BigInt(unlockDate.getTime());
  
  // 3. Compile contract
  const ergoTree = compileTimeLockContract(ownerPk, unlockTimestamp);
  
  // 4. Get contract address
  const contractAddress = ErgoAddress.fromErgoTree(ergoTree).toString();
  
  console.log("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•");
  console.log("๐Ÿ”’ Creating Time-Lock Box");
  console.log("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•");
  console.log(`Owner: ${ownerAddress}`);
  console.log(`Amount: ${BigInt(lockAmount) / 1000000000n} ERG`);
  console.log(`Unlock Date: ${unlockDate.toISOString()}`);
  console.log(`Contract: ${contractAddress}`);
  console.log("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•");
  
  // 5. Build transaction
  const unsignedTx = new TransactionBuilder(currentHeight)
    .from(inputs)
    .to(
      new OutputBuilder(lockAmount, contractAddress)
        .setAdditionalRegisters({
          // Store unlock time in R4 for easy reading
          R4: unlockTimestamp.toString(),
          // Store owner address in R5 for reference
          R5: ownerAddress
        })
    )
    .sendChangeTo(ownerAddress)
    .payFee("1100000")
    .build();

  return {
    transaction: unsignedTx,
    contractAddress,
    unlockTimestamp
  };
}

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// ๐Ÿ”“ UNLOCK FUNDS
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

/**
 * Withdraw from time-lock box (after unlock time)
 */
async function withdrawFromTimeLock(
  timeLockBox: Box<string>,
  ownerAddress: string,
  currentHeight: number,
  currentTimestamp: number
) {
  // 1. Check if unlocked
  const unlockTime = BigInt(timeLockBox.additionalRegisters.R4 || "0");
  
  if (BigInt(currentTimestamp) < unlockTime) {
    const unlockDate = new Date(Number(unlockTime));
    throw new Error(
      `Box is still locked! Unlocks at: ${unlockDate.toISOString()}`
    );
  }
  
  console.log("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•");
  console.log("๐Ÿ”“ Withdrawing from Time-Lock");
  console.log("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•");
  console.log(`Box ID: ${timeLockBox.boxId}`);
  console.log(`Amount: ${BigInt(timeLockBox.value) / 1000000000n} ERG`);
  console.log(`To: ${ownerAddress}`);
  console.log("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•");
  
  // 2. Build withdrawal transaction
  const withdrawAmount = BigInt(timeLockBox.value) - BigInt("1100000");
  
  const unsignedTx = new TransactionBuilder(currentHeight)
    .from([timeLockBox])
    .to(
      new OutputBuilder(withdrawAmount.toString(), ownerAddress)
    )
    .payFee("1100000")
    .build();

  return unsignedTx;
}

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// ๐Ÿ“‹ USAGE EXAMPLE
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

async function main() {
  // User's address
  const ownerAddress = "9f4QF8AD1nQ3nJahQVkMj8hFSVVzVom77b52JU7EW71Zexg6N8y";
  
  // Mock input box (in real app, fetch from blockchain)
  const userInputBox: Box<string> = {
    boxId: "abc123def456...",
    value: "10000000000", // 10 ERG
    ergoTree: "0008cd...",
    creationHeight: 1000000,
    assets: [],
    additionalRegisters: {},
    transactionId: "tx123...",
    index: 0
  };

  // Lock until New Year 2026
  const unlockDate = new Date("2026-01-01T00:00:00Z");
  
  // Create time-lock box
  const { transaction, contractAddress } = await createTimeLockBox(
    [userInputBox],
    {
      ownerAddress,
      lockAmount: "5000000000", // Lock 5 ERG
      unlockDate
    },
    1100000
  );

  console.log("\n๐Ÿ“ฆ Transaction created!");
  console.log(`Outputs: ${transaction.outputs.length}`);
  console.log(`Contract address: ${contractAddress}`);
  
  // After signing and submitting...
  // Later, when time passes:
  
  // const timeLockBox = await fetchBox(contractAddress);
  // const withdrawTx = await withdrawFromTimeLock(
  //   timeLockBox,
  //   ownerAddress,
  //   currentHeight,
  //   Date.now()
  // );
}

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// ๐Ÿงช TESTS
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

import { describe, it, expect } from "vitest";

describe("Time-Lock Contract", () => {
  const mockAddress = "9f4QF8AD1nQ3nJahQVkMj8hFSVVzVom77b52JU7EW71Zexg6N8y";
  
  it("should compile time-lock contract", () => {
    const mockPk = "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798";
    const unlockTime = BigInt(Date.now() + 86400000); // 1 day from now
    
    const ergoTree = compileTimeLockContract(mockPk, unlockTime);
    
    expect(ergoTree).toBeTruthy();
    expect(ergoTree.length).toBeGreaterThan(0);
  });

  it("should reject withdrawal before unlock time", async () => {
    const timeLockBox: Box<string> = {
      boxId: "test123",
      value: "1000000000",
      ergoTree: "0008cd...",
      creationHeight: 100000,
      assets: [],
      additionalRegisters: {
        R4: (Date.now() + 86400000).toString() // Locked for 1 more day
      },
      transactionId: "tx123",
      index: 0
    };

    await expect(
      withdrawFromTimeLock(timeLockBox, mockAddress, 110000, Date.now())
    ).rejects.toThrow("Box is still locked");
  });

  it("should allow withdrawal after unlock time", async () => {
    const pastTime = Date.now() - 86400000; // 1 day ago
    
    const timeLockBox: Box<string> = {
      boxId: "test123",
      value: "1000000000",
      ergoTree: "0008cd...",
      creationHeight: 100000,
      assets: [],
      additionalRegisters: {
        R4: pastTime.toString() // Already unlocked
      },
      transactionId: "tx123",
      index: 0
    };

    const tx = await withdrawFromTimeLock(
      timeLockBox, 
      mockAddress, 
      110000, 
      Date.now()
    );
    
    expect(tx).toBeTruthy();
    expect(tx.outputs.length).toBe(1);
  });
});

export {
  createTimeLockBox,
  withdrawFromTimeLock,
  compileTimeLockContract,
  TIME_LOCK_CONTRACT
};

Use Cases โ€‹

Use CaseDescription
VestingRelease employee tokens over time
SavingsLock funds to prevent impulse spending
EscrowRelease funds after delivery date
ICOLock team tokens for specified period
InheritanceRelease to heirs after certain date

Try It Yourself โ€‹

Open in StackBlitz

Next Steps โ€‹

Released under the MIT License.