// 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 {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import {ZamaEthereumConfig} from "@fhevm/solidity/config/ZamaConfig.sol"; import {IERC7984} from "@openzeppelin/confidential-contracts/interfaces/IERC7984.sol"; /// @title VestingWalletExample - Confidential Token Vesting /// @notice A vesting wallet that releases ERC7984 tokens according to a linear schedule /// @dev Vested amounts remain encrypted, preserving privacy of the vesting arrangement contract VestingWalletExample is Ownable, ReentrancyGuard, ZamaEthereumConfig { // ============ Errors ============ /// @dev Vesting has not started yet error VestingNotStarted(); /// @dev No tokens available to release error NothingToRelease(); // ============ Events ============ /// @notice Emitted when vested tokens are released event TokensReleased(address indexed token, euint64 amount); /// @notice Emitted when vesting schedule is updated event VestingScheduleSet(uint64 start, uint64 duration); // ============ State ============ /// @dev Amount of each token already released (encrypted) mapping(address token => euint128 released) private _tokenReleased; /// @dev Vesting start timestamp uint64 private _start; /// @dev Vesting duration in seconds uint64 private _duration; // ============ Constructor ============ /// @param beneficiaryAddress The address that will receive vested tokens /// @param startTimestamp When vesting begins /// @param durationSeconds How long the vesting period lasts constructor( address beneficiaryAddress, uint64 startTimestamp, uint64 durationSeconds ) Ownable(beneficiaryAddress) { _start = startTimestamp; _duration = durationSeconds; emit VestingScheduleSet(startTimestamp, durationSeconds); } // ============ Vesting Schedule ============ /// @notice Get the vesting start timestamp function start() public view returns (uint64) { return _start; } /// @notice Get the vesting duration function duration() public view returns (uint64) { return _duration; } /// @notice Get the vesting end timestamp function end() public view returns (uint64) { return _start + _duration; } /// @notice Get the amount already released for a token function released(address token) public view returns (euint128) { return _tokenReleased[token]; } // ============ Release Functions ============ /// @notice Calculate releasable amount for a token /// @param token The ERC7984 token address /// @return The encrypted releasable amount function releasable(address token) public returns (euint64) { euint128 vested = vestedAmount(token, uint64(block.timestamp)); euint128 alreadyReleased = released(token); // releasable = vested - released (if vested >= released, else 0) ebool canRelease = FHE.ge(vested, alreadyReleased); euint128 diff = FHE.sub(vested, alreadyReleased); return FHE.select(canRelease, FHE.asEuint64(diff), FHE.asEuint64(0)); } /// @notice Release vested tokens to the beneficiary /// @param token The ERC7984 token to release function release(address token) external nonReentrant { if (block.timestamp < _start) revert VestingNotStarted(); euint64 amount = releasable(token); // Transfer to beneficiary FHE.allowTransient(amount, token); euint64 amountSent = IERC7984(token).confidentialTransfer(owner(), amount); // Update released amount euint128 newReleased = FHE.add(released(token), amountSent); FHE.allow(newReleased, owner()); FHE.allowThis(newReleased); _tokenReleased[token] = newReleased; emit TokensReleased(token, amountSent); } // ============ Vesting Calculation ============ /// @notice Calculate the vested amount at a given timestamp /// @param token The token address /// @param timestamp The timestamp to calculate for /// @return The encrypted vested amount function vestedAmount(address token, uint64 timestamp) public returns (euint128) { // Total allocation = released + current balance euint128 totalAllocation = FHE.add( released(token), IERC7984(token).confidentialBalanceOf(address(this)) ); return _vestingSchedule(totalAllocation, timestamp); } /// @notice Cliff vesting schedule calculation /// @param totalAllocation Total tokens allocated for vesting /// @param timestamp Current timestamp /// @return Vested amount (0 before cliff, 100% after) /// @dev Uses cliff vesting since FHE doesn't support division for linear vesting function _vestingSchedule( euint128 totalAllocation, uint64 timestamp ) internal view returns (euint128) { if (timestamp < _start + _duration) { // Before cliff: 0 vested return euint128.wrap(0); } else { // After cliff: 100% vested return totalAllocation; } } // ============ View Functions ============ /// @notice Get the beneficiary address function beneficiary() external view returns (address) { return owner(); } /// @notice Calculate vesting progress percentage (0-100) function vestingProgress() external view returns (uint256) { if (block.timestamp < _start) return 0; if (block.timestamp >= end()) return 100; uint256 elapsed = block.timestamp - _start; return (elapsed * 100) / _duration; } /// @notice Check if vesting has started function hasStarted() external view returns (bool) { return block.timestamp >= _start; } /// @notice Check if vesting has ended function hasEnded() external view returns (bool) { return block.timestamp >= end(); } }