Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
LABS
Guides

How to Implement Time-Locked Payments for Milestone Releases

A developer tutorial for creating automated payment streams that unlock funds based on time or verified milestones using on-chain protocols.
Chainscore © 2026
introduction
PROGRAMMABLE PAYMENT STREAMS

How to Implement Time-Locked Payments for Milestone Releases

A technical guide to building secure, automated payment streams that release funds upon meeting predefined conditions, using smart contracts on EVM-compatible chains.

Time-locked payments are a core primitive of programmable finance, enabling conditional fund release based on time or milestone completion. Unlike a simple transfer, these streams use a smart contract as an escrow, holding funds until a predefined release condition is met. This is essential for use cases like vesting schedules, freelance milestone payments, and subscription services, where trust and automation are critical. On-chain, this logic is enforced immutably, removing the need for a trusted intermediary and reducing counterparty risk for both payer and payee.

The most straightforward implementation is a time-based lock. A smart contract is funded and programmed to allow withdrawal only after a specific block.timestamp or block.number. For example, a developer grant could be locked for 90 days to ensure project continuity. A basic Solidity structure involves storing the beneficiary address, total amount, and unlock timestamp, with a release() function that checks require(block.timestamp >= unlockTime, "Lock not expired") before transferring funds. This creates a transparent and tamper-proof delay.

For more complex agreements like software development milestones, you need milestone-based release. This requires an oracle or a permissioned account (e.g., the payer) to attest that a deliverable is complete. A typical pattern uses a state variable like milestoneCompleted and a function like approveMilestone() that can only be called by the payer. Once called, it updates the state and allows the payee to claim the allocated funds. This moves beyond simple timing to incorporate real-world conditions into the payment logic.

Security is paramount when locking value. Common risks include reentrancy attacks on the release function, timestamp manipulation by miners (less relevant post-Merge), and access control flaws in the approval mechanism. Best practices include using the Checks-Effects-Interactions pattern, employing OpenZeppelin's ReentrancyGuard and Ownable libraries, and implementing a multisig or DAO vote for milestone approval in high-value contracts. Always audit the logic that triggers the fund release.

Several established protocols simplify development. Sablier V2 and Superfluid offer robust, audited frameworks for continuous streams and one-off vesting. For custom logic, OpenZeppelin's VestingWallet contract provides a secure, minimal base for linear release schedules. When building, consider gas efficiency on mainnet versus testnet flexibility. A full implementation involves: 1) Designing the release conditions, 2) Writing and testing the Solidity contract, 3) Deploying to a testnet (e.g., Sepolia), and 4) Creating a frontend to interact with the contract.

To get started, fork a template from Sablier's documentation or OpenZeppelin Contracts Wizard. Experiment on a testnet by locking test ETH and simulating milestone approvals. The final system creates transparent, trust-minimized agreements, automating payments in a way that is verifiable by all parties on the blockchain. This shifts contractual execution from legal prose to deterministic code.

prerequisites
TUTORIAL

Prerequisites and Setup

This guide walks through implementing a secure, on-chain time-locked payment contract for milestone-based project funding.

Before writing any code, you need a foundational understanding of smart contract development and the specific tools required. You should be comfortable with Solidity, the primary language for Ethereum smart contracts, and have a working knowledge of concepts like state variables, functions, modifiers, and events. Familiarity with the Ethereum Virtual Machine (EVM) and gas optimization is also beneficial. For development, we will use Hardhat, a popular Ethereum development environment that provides testing, compilation, and deployment tooling.

Set up your local development environment by installing Node.js (v18 or later) and a package manager like npm or yarn. Initialize a new Hardhat project by running npx hardhat init in an empty directory and selecting the TypeScript project template for better type safety. You will also need a Web3 wallet such as MetaMask for interacting with contracts, and test ETH from a faucet (e.g., Sepolia) for deploying to a testnet. Essential dependencies include @openzeppelin/contracts for secure, audited base contracts and dotenv for managing private keys.

The core logic of a time-locked payment contract involves three key states: unlocked, locked, and released. We'll use OpenZeppelin's Ownable contract for access control, ensuring only the project owner can lock funds and only the payee can release them. The contract will store the payee address, the releaseTime (a future UNIX timestamp), and the locked amount. Critical functions include a lockFunds function payable by the owner and a release function that the payee can call only after the block.timestamp exceeds the releaseTime.

Security is paramount. Always use require statements for validation: check that the release time is in the future, that the contract has sufficient balance when locking, and that the caller is authorized for each action. Implement events like FundsLocked and FundsReleased for off-chain monitoring. Avoid using block.timestamp for precise time intervals in seconds due to minor miner manipulation; it is sufficient for day/week-based milestones. Thoroughly test all state transitions and edge cases, such as attempting to release funds early or locking funds twice.

For deployment, write a script in the Hardhat scripts/ directory. Use environment variables (via dotenv) to securely load your deployer's private key and the Infura or Alchemy RPC URL for your target network (e.g., Sepolia). After deploying, verify the contract source code on block explorers like Etherscan using the Hardhat Etherscan plugin. This transparency builds trust with the payee. Finally, interact with the deployed contract by calling lockFunds with the milestone value and the future timestamp, sending the required ETH in the transaction.

protocol-options
TIME-LOCKED PAYMENTS

Core Protocols for Streams

Secure protocols for automating milestone-based payments, ensuring funds are released only when predefined conditions are met.

06

Security Audit Checklist

Before deploying a time-locked payment contract, review these critical security considerations:

  • Access Control: Ensure only authorized parties can trigger releases or cancel streams. Use OpenZeppelin's Ownable or role-based access.
  • Reentrancy Guards: Protect release functions with nonReentrant modifiers.
  • Timestamp Dependence: Avoid block.timestamp for precise milestones; consider using a timestamp oracle for critical deadlines.
  • Fund Recovery: Implement an emergency escape hatch for the sender, with a significant timelock, in case the receiver is non-responsive.
  • Testing: Simulate stream scenarios with forked mainnet state using Foundry or Hardhat.
TIME-LOCKED PAYMENT PROTOCOLS

Sablier vs. Superfluid: Protocol Comparison

A technical comparison of two leading protocols for implementing automated, time-based payments on Ethereum and EVM chains.

Feature / MetricSablier V2Superfluid

Core Payment Model

Locked linear/stepwise streams

Real-time continuous flows

Settlement Finality

On-chain per transaction

Off-chain with on-chain guarantees

Gas Cost for Creation

~150k-250k gas

~450k-600k gas

Supported Assets

Any ERC-20

Wrapped Super Tokens (xSUSHI, xDAI)

Automatic Execution

Yes, via stream contract

Yes, via Constant Flow Agreements (CFA)

Milestone Pause/Clawback

Yes, by sender

No, flows are permissionless to stop

Primary Use Case

Vesting, payroll, grants

Subscriptions, salaries, rewards

Protocol Fee

0% (as of v2.1)

0% on mainnet, varies by host chain

time-based-streams
BUILDING BLOCKS

Step 1: Implementing Simple Time-Based Streams

Learn how to create a foundational time-locked payment contract using Solidity and OpenZeppelin. This guide covers the core logic for releasing funds based on a schedule, a pattern used in milestone-based agreements and vesting.

A time-based stream is a smart contract that holds funds and releases them to a beneficiary according to a predefined schedule. This is the foundational mechanism for milestone payments, where a client or DAO treasury can lock funds for a contractor, releasing portions upon verified completion of work. Unlike a simple transfer, it enforces trust by removing the need for either party to manually trigger payments, reducing counterparty risk. The core logic involves tracking a startTime, a cliff period (if any), a duration, and the total amount locked.

We'll build a basic version using Solidity. The contract needs to store key parameters: the beneficiary address, the total amount deposited, the startTime of the stream, and its duration. We use OpenZeppelin's SafeERC20 for secure token transfers and ReentrancyGuard as a security best practice. The constructor initializes these values, and a deposit function allows the payer to fund the contract with the specified ERC-20 token, like USDC or DAI.

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract TimeLockedStream is ReentrancyGuard {
    using SafeERC20 for IERC20;

    address public beneficiary;
    IERC20 public token;
    uint256 public amount;
    uint256 public startTime;
    uint256 public duration;
    uint256 public released;

    constructor(
        address _beneficiary,
        address _token,
        uint256 _amount,
        uint256 _startTime,
        uint256 _duration
    ) {
        beneficiary = _beneficiary;
        token = IERC20(_token);
        amount = _amount;
        startTime = _startTime;
        duration = _duration;
    }

The essential function is releasableAmount(), which calculates how many tokens the beneficiary can claim at any given moment. It uses a linear vesting formula: (elapsedTime * amount) / duration. If the current time is before the startTime, the releasable amount is zero. After the duration has passed, the full amount is releasable. This calculation must be performed on-chain to ensure transparency and verifiability.

solidity
    function releasableAmount() public view returns (uint256) {
        if (block.timestamp < startTime) {
            return 0;
        }
        uint256 elapsedTime = block.timestamp - startTime;
        if (elapsedTime > duration) {
            elapsedTime = duration;
        }
        return (elapsedTime * amount) / duration - released;
    }

Finally, the release() function allows the beneficiary to withdraw their available funds. It calls releasableAmount(), transfers that amount of tokens to the beneficiary using safeTransfer, and updates the released state variable to prevent double-spending. The nonReentrant modifier is critical here to protect against reentrancy attacks. This pattern gives the beneficiary full control to claim funds as they become available, without requiring action from the payer.

solidity
    function release() external nonReentrant {
        uint256 payment = releasableAmount();
        require(payment > 0, "No tokens to release");
        released += payment;
        token.safeTransfer(beneficiary, payment);
        emit TokensReleased(beneficiary, payment);
    }
}

This simple contract demonstrates the core principles, but production systems require additional features. Consider adding an emergency cancel function for the payer (with potential penalties), the ability for the payer to top up the stream amount, or support for a cliff period where no tokens are released for an initial duration. These are common in real-world vesting schedules for employees or advisors. Always audit such contracts thoroughly, as they manage real value. For more complex, gas-efficient, and audited implementations, developers often use established protocols like Sablier or Superfluid.

milestone-integration
IMPLEMENTATION

Step 2: Integrating Milestone Verification with Oracles

This guide explains how to use decentralized oracles to verify off-chain milestones, enabling secure, automated time-locked payments in your smart contracts.

Time-locked payments require a trustless trigger to release funds. Instead of relying on a single party to confirm a milestone, you can use a decentralized oracle network like Chainlink. Oracles act as a secure middleware, fetching verified off-chain data (e.g., project completion status from an API) and delivering it on-chain. This transforms a subjective agreement into an objective, automated condition. Your smart contract's payment release function will be gated by this verified data feed.

To implement this, you first define the data your contract needs. For a development milestone, this could be a boolean isMilestoneComplete or a numeric completionPercentage from a project management API. You then integrate a Chainlink Data Feed or Custom Oracle Job. For pre-defined metrics, use existing data feeds. For custom verification, you'll create an External Adapter that queries your specific API endpoint, formats the data, and signs it for on-chain use. The oracle network aggregates responses from multiple nodes to ensure data integrity.

Here is a basic Solidity example using a Chainlink oracle to check a boolean condition. The contract stores the oracle job ID, payment amount, and milestone details. When the checkMilestone function is called, it requests data, and the oracle callback function fulfill receives the result, unlocking the payment.

solidity
// Example using Chainlink Oracle (conceptual)
function checkMilestone(bytes32 _jobId) public {
    Chainlink.Request memory req = buildChainlinkRequest(_jobId, address(this), this.fulfill.selector);
    req.add("get", "https://api.your-project.com/milestone/123");
    req.add("path", "completed");
    sendChainlinkRequestTo(ORACLE_ADDRESS, req, FEE);
}

function fulfill(bytes32 _requestId, bool _completionStatus) public {
    if (_completionStatus && !milestonePaid) {
        milestonePaid = true;
        payable(developer).transfer(paymentAmount);
    }
}

Security is paramount when integrating oracles. Key considerations include: Source Authenticity - ensure the API endpoint is secure and owned by a trusted party. Data Freshness - use oracle jobs with regular updates to prevent stale data from triggering payments. Decentralization - leverage networks with multiple independent nodes to avoid a single point of failure or manipulation. Always implement circuit breakers or multi-sig controls for emergency pauses, especially during the initial testing phase of your integration.

For more complex milestone structures, consider using Chainlink Functions or API3 dAPIs. These services simplify the process of calling any web API with decentralized execution. You can pass parameters (like a milestone ID) to your custom logic. This is ideal for verifying deliverables on platforms like GitHub (merged PRs), Trello (completed cards), or custom project dashboards. The cost involves paying LINK tokens for oracle gas and service fees, which should be factored into the contract's funding.

The final step is testing your integration on a testnet like Sepolia. Deploy your contract, fund it with test LINK and ETH, and simulate the oracle response using a Chainlink testnet node. Verify that the payment releases only when the correct data is received. This setup creates a robust, automated escrow system where payments are released based on verifiable real-world outcomes, eliminating disputes and manual intervention in the release process.

clawback-mechanism
IMPLEMENTATION

Step 3: Designing Clawback Conditions

This section details how to programmatically enforce milestone-based fund releases using time-locked conditions in a clawback contract.

A time-locked payment condition is a clawback mechanism that releases funds to a recipient only after a predefined duration has elapsed. This is implemented using a releaseTimestamp state variable. The core logic is simple: the contract's withdraw function checks if block.timestamp >= releaseTimestamp. If the condition is false, the transaction reverts. This creates a transparent and trust-minimized escrow, ideal for milestone-based grants, vesting schedules, or delayed payments where a simple time guarantee is required.

For more complex milestone structures, you can design a contract with multiple, sequential time locks. Instead of a single timestamp, you would store an array of Milestone structs, each containing an amount and a releaseTime. The withdrawal function would then iterate through this array, releasing funds only for milestones whose time lock has expired and haven't been claimed. This pattern is common in project funding agreements where capital is released upon hitting development deadlines over several months.

Here is a basic Solidity example for a single time-locked payment. The key security consideration is ensuring the releaseTimestamp is set in the constructor and cannot be altered after deployment.

solidity
contract TimeLockClawback {
    address public beneficiary;
    uint256 public immutable releaseTimestamp;

    constructor(address _beneficiary, uint256 _releaseDelay) payable {
        beneficiary = _beneficiary;
        // Set release time as current time + delay (e.g., 30 days in seconds)
        releaseTimestamp = block.timestamp + _releaseDelay;
    }

    function withdraw() external {
        require(msg.sender == beneficiary, "Not beneficiary");
        require(block.timestamp >= releaseTimestamp, "Timelock not expired");
        payable(beneficiary).transfer(address(this).balance);
    }
}

When implementing this, you must account for the block timestamp manipulation minor risk. While miners can influence block.timestamp by a few seconds, they cannot roll it back. For delays measured in days or weeks, this is negligible. For highly precise, short-term locks (e.g., < 1 hour), consider using a block number-based condition (e.g., block.number) as it is immune to timestamp manipulation, though less user-friendly for calendar scheduling.

To enhance this pattern, integrate multi-signature control for the clawback function. A common design is a 2-of-3 multisig where signers can trigger an early clawback to the funder if a milestone is missed, but the beneficiary can still claim automatically after the time lock expires. This hybrid approach balances automated enforcement with human oversight for dispute resolution, making it suitable for high-value DAO grants or service contracts.

superfluid-cfa
IMPLEMENTATION

Step 4: Using Superfluid for Constant Flow Agreements

This guide explains how to implement time-locked, milestone-based payments using Superfluid's Constant Flow Agreement (CFA) framework.

A Constant Flow Agreement (CFA) is a Superfluid primitive that enables the continuous, real-time transfer of value (e.g., DAIx, USDCx) from a sender to a receiver. Unlike a one-time transaction, a CFA creates a per-second flow rate. For milestone releases, you can programmatically control this flow by starting, updating, and stopping it based on predefined conditions, such as time locks or off-chain verification. This creates a transparent, automated, and trust-minimized payment stream.

To implement time-locked payments, you need to interact with the Superfluid host contract via the IConstantFlowAgreementV1 interface. The core functions are createFlow, updateFlow, and deleteFlow. A common pattern is to deploy a vesting or milestone smart contract that acts as the sender. This contract holds the super tokens and uses createFlow only after a specific block timestamp or upon receiving a verified proof of work completion from an oracle or a multisig.

Here is a simplified Solidity example for a time-locked vesting contract:

solidity
// SPDX-License-Identifier: MIT
import { ISuperfluid, ISuperToken, IConstantFlowAgreementV1 } from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol";

contract MilestoneVester {
    ISuperfluid private host;
    IConstantFlowAgreementV1 private cfa;
    ISuperToken public vestedToken;
    uint256 public unlockTime;
    address public recipient;

    constructor(ISuperfluid _host, IConstantFlowAgreementV1 _cfa, ISuperToken _token, uint256 _unlockTime, address _recipient) {
        host = _host;
        cfa = _cfa;
        vestedToken = _token;
        unlockTime = _unlockTime;
        recipient = _recipient;
    }

    function startVestingStream(int96 flowRate) external {
        require(block.timestamp >= unlockTime, "Time lock not expired");
        host.callAgreement(
            cfa,
            abi.encodeWithSelector(
                cfa.createFlow.selector,
                vestedToken,
                recipient,
                flowRate,
                new bytes(0)
            ),
            "0x"
        );
    }
}

For more complex logic, such as releasing funds after each milestone, you can structure your contract to have multiple phases. Each phase would have its own unlock condition and flow rate. You can use the updateFlow function to change the payment rate when a new milestone is achieved, or deleteFlow followed by a new createFlow to reset the stream for a new recipient or token. Always ensure the contract is funded with enough super token balance to cover the outgoing flow.

Key considerations for production include: gas optimization by batching agreement calls, implementing emergency stop functions for the contract owner, and ensuring proper access control. You must also handle the super token upgrade process (wrapping plain ERC-20s into their Superfluid-enabled counterparts like DAI to DAIx) before funding the contract. Testing with Superfluid's local development framework, like the Superfluid Metropolis, is crucial before mainnet deployment.

This approach provides a powerful alternative to traditional multi-sig releases or manual transactions. By leveraging the Superfluid protocol, you create payment streams that are auditable on-chain, resistant to unilateral stoppage by the payer, and perfectly aligned with continuous delivery models common in software development, freelance work, and DAO contributor compensation.

TIME-LOCKED PAYMENTS

Common Implementation Errors and Troubleshooting

Implementing time-locked payments for milestone releases involves precise smart contract logic. Common errors stem from timestamp manipulation, access control issues, and incorrect fund handling. This guide addresses frequent pitfalls and their solutions.

This error typically occurs due to block timestamp manipulation or incorrect time unit calculations. The block.timestamp in Solidity is set by the miner/validator and can be slightly inaccurate.

Common causes and fixes:

  • Off-by-one errors: Ensure you use >= not > when checking block.timestamp >= releaseTime. A strict > will fail at the exact second.
  • Timestamp granularity: block.timestamp is in seconds. If you calculated the releaseTime using minutes or hours, you must convert: releaseTime = block.timestamp + (7 days);.
  • Front-running the deadline: Transactions are not instantaneous. A transaction sent at the deadline may be mined in a later block. Add a buffer (e.g., 30 seconds) in your client-side logic.
solidity
// Correct comparison using >=
function release() public {
    require(block.timestamp >= releaseTime, "TooEarly");
    // Release funds
}
TIME-LOCKED PAYMENTS

Frequently Asked Questions

Common technical questions and solutions for implementing secure, automated milestone payments using smart contracts.

A time-locked payment contract is a smart contract that holds funds in escrow and releases them to a predefined recipient only after a specific block timestamp or block number is reached. It automates milestone-based payouts without requiring manual intervention from the payer.

Core Mechanism:

  1. Deployment & Funding: The contract is deployed with the recipient's address, release time, and payment amount. The payer deposits the funds (e.g., ETH, ERC-20 tokens).
  2. Locking: The contract's logic uses a require statement to check the current block.timestamp against the stored releaseTime. Any withdrawal attempt before this time fails.
  3. Release: Once block.timestamp >= releaseTime, the recipient (or sometimes any caller) can execute a release() function. This function transfers the locked funds to the recipient's address.

This pattern is foundational for vesting schedules, escrow services, and deferred compensation in DeFi and DAOs.

conclusion
IMPLEMENTATION SUMMARY

Conclusion and Next Steps

You have learned how to build a secure, on-chain escrow system for milestone-based payments using Solidity. This guide covered core concepts, contract architecture, and deployment steps.

The TimeLockedMilestone contract provides a foundational framework for automating milestone releases. Key features implemented include: a multi-signature approval mechanism, a time-lock delay enforced by block.timestamp, and secure fund withdrawal patterns. This structure mitigates risks like unilateral fund release and ensures all parties must agree before a milestone is paid out. Remember, the contract's security is paramount; always use established libraries like OpenZeppelin for access control and conduct thorough testing on a testnet before mainnet deployment.

For production use, consider extending the contract's functionality. You could integrate Chainlink Automation to trigger time-lock expiries automatically, removing the need for manual calls to releaseMilestone. Adding event emissions for each state change (e.g., MilestoneProposed, MilestoneReleased) improves off-chain monitoring and transparency. If managing multiple concurrent projects, refactor the contract to use a mapping of projectId to Milestone structs, enabling a single contract instance to handle numerous agreements.

Your next step is to build a frontend interface. Using a framework like React with wagmi and viem libraries, you can create a dApp that allows stakeholders to: connect their wallets, propose new milestones, approve pending releases, and execute payments. The frontend should listen for contract events to update the UI in real-time. For a complete reference, review the OpenZeppelin Governor contracts, which demonstrate advanced time-lock and multi-signature logic in a production-grade setting.

Further security enhancements are critical for high-value contracts. Engage a professional auditing firm to review your final code. Implement a pause mechanism controlled by a decentralized multisig for emergency stops. Consider using a proxy upgrade pattern (e.g., UUPS) to allow for future fixes and improvements without migrating funds. Always verify and publish your source code on block explorers like Etherscan to build trust with counterparties.

To experiment, fork the complete example code from the Chainscore Labs GitHub repository and deploy it on a testnet like Sepolia or Holesky. Test all failure paths, including failed approvals and premature release attempts. By implementing and extending this pattern, you can create robust, trust-minimized financial agreements that are native to the blockchain.

How to Implement Time-Locked Payments for Milestone Releases | ChainScore Guides