Vesting Wallet

Time-locked vesting wallet with fully encrypted token amounts. Implements linear vesting for ERC7984 tokens where the schedule, amounts, and releases are computed via FHE, keeping progress hidden from observers.

To run this example correctly, make sure the files are placed in the following directories:

  • .sol file → <your-project-root-dir>/contracts/

  • .ts file → <your-project-root-dir>/test/

This ensures Hardhat can compile and test your contracts as expected.

🔐 FHE API Reference (13 items)

Types: ebool · euint128 · euint64

Functions:

  • FHE.add() - Homomorphic addition: result = a + b (overflow wraps)

  • FHE.allow() - Grants PERMANENT permission for address to decrypt/use value

  • FHE.allowThis() - Grants contract permission to operate on ciphertext

  • FHE.allowTransient() - Grants TEMPORARY permission (expires at tx end)

  • FHE.asEuint64() - Encrypts a plaintext uint64 value into euint64

  • FHE.div() - Homomorphic division: result = a / b (plaintext divisor only)

  • FHE.ge() - Encrypted greater-or-equal: returns ebool(a >= b)

  • FHE.mul() - Homomorphic multiplication: result = a * b

  • FHE.select() - Encrypted if-then-else: select(cond, a, b) → returns a if true, b if false

  • FHE.sub() - Homomorphic subtraction: result = a - b (underflow wraps)

// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity ^0.8.27;

import {FHE, ebool, euint64, euint128} from "@fhevm/solidity/lib/FHE.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {
    ReentrancyGuardTransient
} from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol";
import {ZamaEthereumConfig} from "@fhevm/solidity/config/ZamaConfig.sol";
import {
    IERC7984
} from "@openzeppelin/confidential-contracts/interfaces/IERC7984.sol";

/**
 * @notice Time-locked vesting wallet with fully encrypted token amounts.
 *         Implements linear vesting for ERC7984 tokens where the schedule, amounts,
 *         and releases are computed via FHE, keeping progress hidden from observers.

 * @dev Timeline: |--START--|---VESTING---|--END--| (0% → linear → 100%)
 *      All vesting calculations performed on encrypted values using FHE.
 *      ⚡ Gas: FHE.div/mul operations are expensive (~200k gas each)
 */
contract VestingWalletExample is
    Ownable,
    ReentrancyGuardTransient,
    ZamaEthereumConfig
{
    mapping(address token => euint128) private _tokenReleased;
    uint64 private _start;
    uint64 private _duration;

    /// @notice Emitted when vested tokens are released to beneficiary
    /// @param token The ERC7984 token address
    /// @param amount The encrypted amount released
    event VestingWalletConfidentialTokenReleased(
        address indexed token,
        euint64 amount
    );

    /// @notice Creates a new vesting wallet for a beneficiary
    /// @param beneficiary Address that will receive vested tokens
    /// @param startTimestamp Unix timestamp when vesting begins
    /// @param durationSeconds Duration of the vesting period in seconds
    constructor(
        address beneficiary,
        uint48 startTimestamp,
        uint48 durationSeconds
    ) Ownable(beneficiary) {
        _start = startTimestamp;
        _duration = durationSeconds;
    }

    function start() public view virtual returns (uint64) {
        return _start;
    }

    function duration() public view virtual returns (uint64) {
        return _duration;
    }

    function end() public view virtual returns (uint64) {
        return start() + duration();
    }

    /// @notice Encrypted amount already released for token
    function released(address token) public view virtual returns (euint128) {
        return _tokenReleased[token];
    }

    // ==================== CORE LOGIC ====================

    /// @notice Calculate how much can be released now
    /// @dev Returns encrypted amount - no one knows the actual value
    function releasable(address token) public virtual returns (euint64) {
        euint128 vestedAmount_ = vestedAmount(token, uint48(block.timestamp));
        euint128 releasedAmount = released(token);

        // Encrypted comparison: if vested >= released → return difference, else 0
        ebool canRelease = FHE.ge(vestedAmount_, releasedAmount);
        return
            FHE.select(
                canRelease,
                FHE.asEuint64(FHE.sub(vestedAmount_, releasedAmount)),
                FHE.asEuint64(0)
            );
    }

    function release(address token) public virtual nonReentrant {
        euint64 amount = releasable(token);

        // Transfer encrypted amount using allowTransient (cheaper!)
        FHE.allowTransient(amount, token);
        euint64 amountSent = IERC7984(token).confidentialTransfer(
            owner(),
            amount
        );

        // Update released amount (encrypted)
        euint128 newReleasedAmount = FHE.add(released(token), amountSent);
        FHE.allow(newReleasedAmount, owner());
        FHE.allowThis(newReleasedAmount);
        _tokenReleased[token] = newReleasedAmount;

        emit VestingWalletConfidentialTokenReleased(token, amountSent);
    }

    /// @notice Calculate vested amount at timestamp
    function vestedAmount(
        address token,
        uint48 timestamp
    ) public virtual returns (euint128) {
        // Total = released + current balance
        euint128 totalAllocation = FHE.add(
            released(token),
            IERC7984(token).confidentialBalanceOf(address(this))
        );
        return _vestingSchedule(totalAllocation, timestamp);
    }

    /// @dev Linear vesting: (total * elapsed) / duration
    function _vestingSchedule(
        euint128 totalAllocation,
        uint48 timestamp
    ) internal virtual returns (euint128) {
        if (timestamp < start()) {
            // Before start: 0% vested
            return euint128.wrap(0);
        } else if (timestamp >= end()) {
            // After end: 100% vested
            return totalAllocation;
        } else {
            // ⚡ Gas warning: FHE.mul + FHE.div cost ~400k gas combined!
            return
                FHE.div(
                    FHE.mul(totalAllocation, (timestamp - start())),
                    duration()
                );
        }
    }
}

Last updated