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

Launching a Vesting Contract with Upgradeable Logic

A technical guide to implementing a token vesting contract with upgradeable logic using proxy patterns. Covers UUPS and Transparent Proxy implementations, security considerations, and managing upgrades without disrupting vesting schedules.
Chainscore © 2026
introduction
TUTORIAL

Introduction to Upgradeable Vesting Contracts

Learn how to deploy a secure, future-proof token vesting contract using the OpenZeppelin Upgrades Plugins and UUPS proxy pattern.

Token vesting is a critical mechanism for aligning long-term incentives in Web3 projects, used for team allocations, investor cliffs, and advisor grants. A traditional, immutable VestingWallet contract locks the logic permanently upon deployment. An upgradeable vesting contract separates the storage (the beneficiary, schedule, and released amounts) from the logic. This allows project administrators to fix bugs, add new features like early release options, or adjust schedules in response to unforeseen events, without disrupting the vested funds or requiring beneficiaries to migrate to a new contract.

The upgrade is managed via a proxy pattern. We will use the UUPS (Universal Upgradeable Proxy Standard) model, where the upgrade logic is embedded in the implementation contract itself. Compared to the Transparent Proxy pattern, UUPS is more gas-efficient. The core components are: the Proxy Contract (holds the state and delegates calls), the Implementation Contract (contains the vesting logic), and a ProxyAdmin (manages upgrade authorization). Security is paramount; only a designated owner (often a multi-sig wallet) should have upgrade rights to prevent malicious logic changes.

To build one, start with a standard OpenZeppelin VestingWallet and modify it for upgradeability. First, install the required packages: @openzeppelin/contracts-upgradeable, @openzeppelin/hardhat-upgrades, and hardhat. Your contract must: 1) Inherit from Initializable and the UUPS upgradeability contract, 2) Use the initializer modifier instead of a constructor, and 3) Include the _authorizeUpgrade function to restrict upgrade calls. Here's the basic skeleton:

solidity
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
contract UpgradeableVesting is Initializable, VestingWalletUpgradeable, UUPSUpgradeable {
    function initialize(address beneficiary, uint64 startTimestamp, uint64 durationSeconds) public initializer {
        __VestingWallet_init(beneficiary, startTimestamp, durationSeconds);
    }
    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}

Deployment and management are handled through scripts using the Hardhat Upgrades plugin. To deploy, you call deployProxy which creates the UUPS proxy, the implementation contract, and runs the initializer. A subsequent upgrade is performed with upgradeProxy, which deploys a new implementation and points the proxy to it, preserving all state. Always test upgrades thoroughly on a testnet (like Sepolia or Goerli) using a dry-run to simulate the process. Key verification steps include ensuring the beneficiary address and vesting schedule remain unchanged and that the new logic functions as intended.

While upgradeability offers flexibility, it introduces centralization and security risks. The onlyOwner role controlling _authorizeUpgrade is a single point of failure. Mitigate this by using a multi-signature wallet (e.g., Safe) or a DAO vote as the owner. Furthermore, you must follow best practices: maintain storage layout compatibility across upgrades, never leave an implementation contract uninitialized, and consider adding a timelock to the upgrade function to give stakeholders time to react to proposed changes. Always audit the new implementation code before an upgrade.

Real-world use cases for an upgradeable vesting contract include adding a clawback mechanism for terminated employees, integrating with a new staking protocol to allow vested tokens to be staked directly, or enabling partial early releases based on milestones. By implementing upgradeability from the start, you future-proof your token distribution strategy, allowing it to evolve alongside your project without the complexity and cost of manual migration for all participants.

prerequisites
SETUP

Prerequisites

Before deploying a vesting contract with upgradeable logic, you must configure your development environment and understand the core architectural components.

You need a local development environment with Node.js (v18 or later) and npm or yarn installed. This guide uses Hardhat as the development framework and OpenZeppelin contracts for the upgradeable standard. Start by initializing a new Hardhat project and installing the required dependencies: @openzeppelin/contracts-upgradeable, @openzeppelin/hardhat-upgrades, and @nomicfoundation/hardhat-ethers.

The architecture relies on the Transparent Proxy Pattern or UUPS Proxies. The user interacts with a Proxy contract, which delegates all calls to a Logic contract holding the business rules. A separate ProxyAdmin contract manages upgrade authorizations. This separation allows you to deploy a new Logic contract and point the Proxy to it, upgrading the system without migrating user data or tokens.

You must understand the roles involved: the contract owner (deployer/admin), beneficiaries who receive vested tokens, and optionally a revoker. The owner deploys the proxy, schedules upgrades via the ProxyAdmin, and can manage vesting schedules. Security is paramount; ensure the initializer function (replacing the constructor) is properly secured and can only be called once.

For testing, configure your hardhat.config.js to use the @openzeppelin/hardhat-upgrades plugin. You will write and run tests that verify: the initial deployment, the vesting schedule logic, and a simulated upgrade to a new logic contract version. Use a local Hardhat network for rapid iteration.

Finally, have a clear plan for the upgrade process. Determine who holds the admin keys, establish a multi-sig or timelock for production upgrades, and prepare the new logic contract with any added or modified functions. Never change the storage layout of existing variables in an upgrade, as this will corrupt the contract state.

key-concepts-text
TUTORIAL

Key Concepts: Proxy Patterns for Upgradeability

Learn how to deploy a secure, upgradeable token vesting contract using the Transparent Proxy Pattern, a standard for managing smart contract logic upgrades.

Smart contracts are immutable by default, which poses a challenge for long-term projects like token vesting that may require bug fixes or feature additions. Proxy patterns solve this by separating a contract's storage and logic. A proxy contract holds the state (like beneficiary addresses and vesting schedules), while a separate logic contract contains the executable code. The proxy delegates all function calls to the logic contract via the delegatecall opcode. This architecture allows you to deploy a new logic contract and point the proxy to it, effectively upgrading the system's behavior without migrating user data or funds.

The Transparent Proxy Pattern is a widely adopted standard (EIP-1967) that mitigates function selector clashes between the proxy and logic contract. It introduces an admin role; when the admin calls the proxy, they can execute upgrade functions, but when any other address calls it, calls are transparently delegated to the logic contract. This prevents a malicious actor from accidentally or intentionally invoking an admin function on the proxy. For implementation, we use OpenZeppelin's libraries: @openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol and @openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol.

To launch an upgradeable vesting contract, you first write and deploy your initial logic contract. This contract, for example VestingV1, contains the core vesting logic but must not define a constructor. Instead, you use an initializer function (e.g., initialize) marked with the initializer modifier from @openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol. This function sets initial state, such as the token address and beneficiary details. It's crucial because a constructor's code is part of the logic contract's bytecode and is not executed in the context of the proxy's storage during a delegatecall.

Next, deploy a ProxyAdmin contract, which will own and manage the proxy. Then, deploy the TransparentUpgradeableProxy. Its constructor takes three arguments: the address of the logic contract (VestingV1), the address of the ProxyAdmin, and the encoded call data for the initialize function. This single transaction deploys the proxy and sets up the vesting schedule. Users and applications will interact solely with the proxy address. The ProxyAdmin can later call upgrade to point the proxy to a new logic contract, like VestingV2, and changeAdmin to transfer upgrade rights.

When preparing an upgrade, follow a strict process to ensure safety. First, thoroughly test the new logic contract (VestingV2) in a forked testnet environment. Second, use OpenZeppelin's Upgrades Plugins for Hardhat or Foundry to validate storage layout compatibility; the new logic contract must not modify the order, type, or meaning of existing storage variables. Finally, execute the upgrade via the ProxyAdmin. Post-upgrade, all existing vesting schedules and balances remain intact, as storage lives in the proxy. This pattern provides a robust framework for maintaining and evolving decentralized applications over time.

PROXY PATTERN COMPARISON

UUPS vs. Transparent Proxy for Vesting

A technical comparison of the two primary upgrade patterns for implementing a vesting contract with upgradeable logic.

Feature / MetricTransparent ProxyUUPS (ERC-1967)

Proxy Admin Overhead

Implementation Contract Size

~2.5KB larger

~0.5KB larger

Gas Cost for User Tx

~44k gas overhead

No overhead

Gas Cost for Upgrade

~100k gas

~200k gas

Upgrade Authorization

External ProxyAdmin contract

Logic contract function

Security Risk Profile

ProxyAdmin compromise

Implementation self-destruct

OpenZeppelin Wizard Support

Recommended Use Case

Multi-admin teams, high security

Gas-optimized, single admin

step-1-setup-and-initialization
FOUNDATION

Step 1: Project Setup and Initial Contract

This guide walks through initializing a Hardhat project and deploying a basic, upgradeable ERC20 token vesting contract using OpenZeppelin libraries.

Begin by creating a new project directory and initializing a Node.js environment. We'll use Hardhat as our development framework for its robust testing and deployment tooling. Run npm init -y followed by npm install --save-dev hardhat to install it. Initialize a new Hardhat project with npx hardhat init, selecting the TypeScript project template for better type safety. This creates the foundational structure with hardhat.config.ts, contracts/, scripts/, and test/ directories.

Next, install the necessary OpenZeppelin contracts and plugins. Our vesting contract will be built using the UUPS upgradeable pattern, which stores logic and data in separate contracts. Install the required packages: npm install @openzeppelin/contracts-upgradeable @openzeppelin/hardhat-upgrades. Also, add the @nomicfoundation/hardhat-toolbox package for a complete development environment. Ensure your hardhat.config.ts imports and configures the @nomicfoundation/hardhat-toolbox and @openzeppelin/hardhat-upgrades plugins.

Now, create the initial smart contract. In contracts/, create a file named TokenVestingV1.sol. This will be our first implementation. Start by importing OpenZeppelin's upgradeable contracts: @openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol and @openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol. Your contract should inherit from Initializable. Define state variables for the beneficiary address, start timestamp, cliff duration, and total vesting period. Remember, in upgradeable contracts, you must not use constructors; instead, you initialize state in an initialize function.

The initialize function is critical. It acts as the replacement for the constructor and should be called only once upon the first deployment of the proxy. It must include the initializer modifier. A typical signature is initialize(address beneficiary_, uint256 start_, uint256 cliff_, uint256 duration_) initializer public. Inside, set the contract's state variables. This function establishes the core vesting schedule parameters that will be stored in the proxy's storage, separate from the logic contract's code.

Finally, write a simple release function for this first version. Implement a release function that allows the beneficiary to claim their vested tokens. It should calculate the vested amount based on the elapsed time since the start, respecting the cliff period. Use block.timestamp for the current time. For V1, we can keep logic simple, assuming the contract holds the ERC20 tokens. Later steps will add more complex features and tests. This establishes a working, deployable base for our upgradeable vesting system.

step-2-implementing-uups-vesting
DEVELOPER TUTORIAL

Step 2: Implementing a UUPS Upgradeable Vesting Contract

This guide walks through building a secure, upgradeable token vesting contract using the UUPS proxy pattern with OpenZeppelin libraries.

We begin by setting up the contract structure. The core of our implementation is the VestingWallet contract from OpenZeppelin, which we will make upgradeable. Instead of deploying it directly, we will deploy it behind a UUPS proxy. First, install the required packages: @openzeppelin/contracts-upgradeable and @openzeppelin/hardhat-upgrades. Initialize your Hardhat project and import the upgradeable versions of the necessary contracts, including OwnableUpgradeable and ERC20Upgradeable for handling the vested token.

The key is to separate the initialization logic from the constructor. In UUPS, the proxy delegates calls to the logic contract, but storage is held in the proxy. Therefore, we replace the constructor with an initialize function. This function must be called atomically after deployment to set the initial state, such as the beneficiary address, start timestamp, and duration. It's critical to use the initializer modifier to prevent re-initialization attacks. Here is the basic function signature:

solidity
function initialize(address beneficiary_, uint64 startTimestamp_, uint64 durationSeconds_) public initializer {
    __VestingWallet_init(beneficiary_, startTimestamp_, durationSeconds_);
    __Ownable_init(msg.sender);
}

To enable future upgrades, the logic contract must include the UUPSUpgradeable contract and implement the _authorizeUpgrade function. This function acts as a security gate, defining which addresses (typically a multi-sig or DAO) are permitted to propose an upgrade. A common implementation is to restrict this to the contract owner:

solidity
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
contract UpgradeableVesting is Initializable, OwnableUpgradeable, VestingWalletUpgradeable, UUPSUpgradeable {
    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}

This ensures that upgrade logic is part of the business logic contract itself, which is the defining characteristic of the UUPS pattern.

Deployment requires a specific script using the Upgrades plugin. You do not deploy the UpgradeableVesting contract directly. Instead, you deploy it as a UUPS proxy using deployProxy. This creates three contracts: your logic contract, a proxy admin (optional for UUPS), and the proxy itself which users interact with. A typical Hardhat deployment script looks like this:

javascript
const { ethers, upgrades } = require("hardhat");
async function main() {
  const Vesting = await ethers.getContractFactory("UpgradeableVesting");
  const vesting = await upgrades.deployProxy(Vesting, [beneficiary, start, duration], { kind: 'uups' });
  await vesting.waitForDeployment();
  console.log("Vesting proxy deployed to:", await vesting.getAddress());
}

The kind: 'uups' parameter is essential. The returned vesting object is the address of the proxy, which points to the logic contract's code.

After deployment, you can verify the setup. Check that the proxy's implementation address points to your logic contract using await upgrades.erc1967.getImplementationAddress(proxyAddress). To perform an upgrade later, you would develop a new version of UpgradeableVestingV2, ensure it inherits from the previous version correctly, and then execute upgradeProxy from your scripts, pointing to the proxy address and the new logic contract. The state (like beneficiary info and released amounts) is preserved in the proxy's storage. Always test upgrades thoroughly on a testnet, using tools like OpenZeppelin's Upgrades Plugin to validate storage layout compatibility and prevent critical errors.

step-3-implementing-transparent-proxy-vesting
UPGRADEABLE LOGIC

Step 3: Implementing a Transparent Proxy Vesting Contract

Deploy a secure, upgradeable vesting contract using the OpenZeppelin Transparent Proxy pattern to separate storage from logic.

The Transparent Proxy Pattern is the industry standard for secure, upgradeable smart contracts. It separates your contract into two components: a Proxy contract that holds the state (storage) and a Logic contract that contains the executable code. When a user calls the proxy, it delegates the call to the current logic contract using a delegatecall. This pattern, defined in EIP-1967, ensures storage layout consistency across upgrades and prevents selector clashes between the proxy and logic contract.

To implement this, you will use OpenZeppelin's contracts. First, write your initial vesting logic. This VestingLogicV1 contract should inherit from OpenZeppelin's ERC20, Ownable, and crucially, the UUPSUpgradeable contract. The UUPSUpgradeable standard (EIP-1822) places the upgrade authorization logic within the implementation contract itself, making it more gas-efficient. Your contract's initialize function acts as the constructor for upgradeable contracts, setting the initial token address and admin.

Here is a simplified structure for the initial logic contract:

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

contract VestingLogicV1 is Initializable, ERC20Upgradeable, OwnableUpgradeable, UUPSUpgradeable {
    IERC20 public vestedToken;
    
    function initialize(address _tokenAddress, address initialOwner) public initializer {
        __ERC20_init("Vested Token", "VTK");
        __Ownable_init(initialOwner);
        __UUPSUpgradeable_init();
        vestedToken = IERC20(_tokenAddress);
    }
    // ... vesting schedule logic ...
    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}

Deployment is a two-step process handled by a script. First, you deploy the VestingLogicV1 implementation contract. Then, you deploy an ERC1967Proxy, passing the logic contract's address and the encoded call to its initialize function as constructor arguments. This creates the proxy, which is the address you will share with users. All interactions happen with the proxy, which delegates to your logic. Use a tool like Hardhat Upgrades or OpenZeppelin Defender to manage this process, as they handle safety checks for storage layout compatibility.

The key security benefit is the transparent nature of the proxy. It includes a built-in admin address (initially the deployer). If the caller is the admin, the proxy will not delegate any calls, preventing the admin from accidentally triggering a logic contract function that shares a selector with the proxy's own functions. For all other users, calls are seamlessly delegated. This prevents a critical class of vulnerabilities known as function selector clashes.

When you need to fix a bug or add a feature, you deploy a new VestingLogicV2 contract. As the admin (via the onlyOwner modifier in _authorizeUpgrade), you call upgradeTo(address(VestingLogicV2)) on the proxy. The proxy's storage pointer is updated, and all subsequent calls use the new logic, while the user's token balances and vesting schedules remain intact. Always test upgrades on a testnet using a full suite of integration tests to ensure state persistence and new functionality work as expected.

step-4-security-and-upgrade-procedure
UPGRADEABLE VESTING

Step 4: Security Considerations and Upgrade Procedure

This section details the critical security model and step-by-step process for upgrading the logic of a deployed vesting contract using the UUPS (Universal Upgradeable Proxy Standard) pattern.

The primary security consideration for an upgradeable contract is proxy admin control. In the UUPS pattern, the upgrade logic is embedded in the implementation contract itself, not a separate admin contract. This means the ability to upgrade is a function callable by the contract's owner. Securing the owner's private keys is paramount, as a compromise would allow an attacker to deploy malicious logic. For production, consider using a multisig wallet or a DAO governance contract as the owner to decentralize this critical privilege and implement timelocks for upgrades.

Before executing an upgrade, you must thoroughly test the new implementation. Deploy the new VestingV2 contract to a testnet first. Write and run integration tests that simulate the upgrade process, ensuring that: all existing user vesting schedules are preserved, the totalLockedAmount state is correctly migrated, and the new logic functions as intended. Use tools like Hardhat's upgrades plugin (@openzeppelin/hardhat-upgrades) to validate storage layout compatibility, which prevents critical errors where new variables overwrite existing data.

The on-chain upgrade is performed by calling the upgradeTo(address newImplementation) function on the proxy contract. This function is only callable by the owner. The call must point to the newly deployed VestingV2 implementation address. Once executed, all future calls to the proxy will be delegated to the new logic, while the contract's address and all stored data (like beneficiaries mapping and totalLocked) remain unchanged. It is a destructive operation—there is no automatic rollback, so verification is essential.

After the upgradeTo transaction is confirmed, you must verify and publish the source code for the new implementation contract on the block explorer (Etherscan, Blockscout). This provides transparency for users and auditors. Next, re-run your key functional tests against the live, upgraded contract address to confirm everything operates correctly. Finally, announce the upgrade to your users through official channels, detailing the changes, the new contract address for verification, and the security rationale behind the update.

UPGRADEABLE CONTRACT PATTERNS

Vesting Contract Storage Layout

Comparison of storage layout strategies for upgradeable vesting contracts, detailing trade-offs between complexity, safety, and flexibility.

Storage PatternImplementationStorage SafetyGas CostComplexity

EIP-1967 Transparent Proxy

OpenZeppelin

High

Low

UUPS (EIP-1822)

Manual Implementation

Medium

High

Diamond Standard (EIP-2535)

Multiple Logic Contracts

Very High

Very High

Storage Gaps

Reserved Slots

Low

Medium

Unstructured Storage

Manual Slot Mapping

Low

High

Inherited Storage

Base Contract

Medium

Low

UPGRADEABLE VESTING

Frequently Asked Questions

Common questions and solutions for developers implementing or interacting with upgradeable token vesting contracts using proxy patterns.

An upgradeable vesting contract uses a proxy pattern to separate storage from logic. The core mechanism involves two main contracts:

  • Proxy Contract: Holds the actual state (beneficiaries, schedules, released amounts) and user funds. It delegates all function calls to a logic contract address.
  • Logic Contract (Implementation): Contains the executable code for functions like release() or vestedAmount(). This contract holds no state and stores no funds.

When you call a function on the proxy, it uses delegatecall to execute the code from the current logic contract in the context of its own storage. This allows you to deploy a new version of the logic contract and update the proxy's pointer to it, upgrading the functionality without migrating funds or disrupting the existing vesting schedule. Popular standards include EIP-1967 for transparent proxies and EIP-1822 for universal upgradeable proxies.

conclusion
IMPLEMENTATION SUMMARY

Conclusion and Next Steps

You have successfully deployed and configured a secure, upgradeable vesting contract. This guide covered the core concepts and steps to manage token distribution with controlled logic evolution.

The primary advantage of the upgradeable proxy pattern is the separation of storage and logic. Your deployed VestingV1 contract holds the user vesting schedules and token balances, while the logic for releasing tokens resides in a separate implementation contract. Using a Transparent Proxy from OpenZeppelin prevents clashes between admin and user calls, a critical security feature. To upgrade, you deploy a new implementation contract (e.g., VestingV2) and point the proxy to it via the upgradeTo function, preserving all user data.

For production deployments, rigorous testing is non-negotiable. Beyond unit tests for core functions like release() and vestedAmount(), you must write integration tests for the upgrade process itself. Use a forked mainnet environment with tools like Hardhat or Foundry to simulate the upgrade, ensuring no storage layout conflicts occur and that all user funds remain accessible. Always verify your contracts on block explorers like Etherscan using the TransparentUpgradeableProxy verification plugin.

Consider these next steps to enhance your system: implement a timelock for upgrade proposals to give users a safety window, add multi-signature wallet requirements for the proxy admin role, or integrate a vesting schedule factory to deploy many contracts efficiently. For ongoing management, monitor events like TokensReleased and Upgraded using subgraphs or dedicated indexing services. The complete code and further examples are available in the Chainscore Labs GitHub repository.

How to Launch a Vesting Contract with Upgradeable Logic | ChainScore Guides