Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add ConfidentialVestingWallet/ConfidentialVestingWalletCliff #76

Merged
merged 5 commits into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions contracts/finance/ConfidentialVestingWallet.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// SPDX-License-Identifier: BSD-3-Clause
pragma solidity ^0.8.24;

import "fhevm/lib/TFHE.sol";

import { IConfidentialERC20 } from "../token/ERC20/IConfidentialERC20.sol";

/**
* @title ConfidentialVestingWallet
* @notice This contract offers a simple vesting wallet for ConfidentialERC20 tokens.
* This is based on the VestingWallet.sol contract written by OpenZeppelin.
* see: openzeppelin/openzeppelin-contracts/blob/master/contracts/finance/VestingWallet.sol
* @dev Default implementation is a linear vesting curve.
* To use with the native asset, it is necessary to wrap the native asset to a ConfidentialERC20-like token.
*/
abstract contract ConfidentialVestingWallet {
/// @notice Emitted when tokens are released to the beneficiary address.
event ConfidentialERC20Released();
Copy link
Member

@jatZama jatZama Dec 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why you didn't keep original OZ specs allowing different tokens? this event should then contain an address argument. It is always better to make standard contracts as generalizable as possible imo.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adjusted.


/// @notice Beneficiary address.
address public immutable BENEFICIARY;
PacificYield marked this conversation as resolved.
Show resolved Hide resolved

/// @notice Confidential ERC20.
IConfidentialERC20 public immutable CONFIDENTIAL_ERC20;

/// @notice Duration (in seconds).
uint64 public immutable DURATION;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you instead keep OZ's specs, with private variable and public getter functions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer not to adjust it (for now) because I like that immutable variables that are easy to be spotted. I don't get why OZ does it as such with public getter functions.


/// @notice End timestamp.
uint64 public immutable END_TIMESTAMP;

/// @notice Start timestamp.
uint64 public immutable START_TIMESTAMP;

/// @notice Constant for zero using TFHE.
/// @dev Since it is expensive to compute 0, it is stored instead.
/* solhint-disable var-name-mixedcase*/
euint64 internal immutable _EUINT64_ZERO;

/// @notice Total encrypted amount released (to the beneficiary).
euint64 internal _amountReleased;

/**
* @param beneficiary_ Beneficiary address.
* @param token_ Confidential token address.
* @param startTimestamp_ Start timestamp.
* @param duration_ Duration (in seconds).
*/
constructor(address beneficiary_, address token_, uint64 startTimestamp_, uint64 duration_) {
START_TIMESTAMP = startTimestamp_;
CONFIDENTIAL_ERC20 = IConfidentialERC20(token_);
DURATION = duration_;
END_TIMESTAMP = startTimestamp_ + duration_;
BENEFICIARY = beneficiary_;

/// @dev Store this constant variable in the storage.
_EUINT64_ZERO = TFHE.asEuint64(0);
_amountReleased = _EUINT64_ZERO;

TFHE.allow(_EUINT64_ZERO, beneficiary_);
TFHE.allowThis(_EUINT64_ZERO);
}

/**
* @notice Release the tokens that have already vested.
* @dev Anyone can call this function but the beneficiary receives the tokens.
*/
function release() public virtual {
euint64 amount = _releasable();
euint64 amountReleased = TFHE.add(_amountReleased, amount);
_amountReleased = amountReleased;

TFHE.allow(amountReleased, BENEFICIARY);
TFHE.allowThis(amountReleased);
TFHE.allowTransient(amount, address(CONFIDENTIAL_ERC20));
CONFIDENTIAL_ERC20.transfer(BENEFICIARY, amount);

emit ConfidentialERC20Released();
}

/**
* @notice Return the encrypted amount of total tokens released.
* @dev It is only reencryptable by the owner.
* @return amountReleased Total amount of tokens released.
*/
function released() public view virtual returns (euint64 amountReleased) {
return _amountReleased;
}

/**
* @notice Calculate the amount of tokens that can be released.
* @return releasableAmount Releasable amount.
*/
function _releasable() internal virtual returns (euint64 releasableAmount) {
return TFHE.sub(_vestedAmount(uint64(block.timestamp)), released());
}

/**
* @notice Calculate the amount of tokens that has already vested.
* @return vestedAmount Vested amount.
*/
function _vestedAmount(uint64 timestamp) internal virtual returns (euint64 vestedAmount) {
return _vestingSchedule(TFHE.add(CONFIDENTIAL_ERC20.balanceOf(address(this)), released()), timestamp);
}

/**
* @notice Return the vested amount based on a linear vesting schedule.
* @dev It must be overriden for non-linear schedules.
* @param totalAllocation Total allocation that is vested.
* @param timestamp Current timestamp.
* @return vestedAmount Vested amount.
*/
function _vestingSchedule(
euint64 totalAllocation,
uint64 timestamp
) internal virtual returns (euint64 vestedAmount) {
if (timestamp < START_TIMESTAMP) {
return _EUINT64_ZERO;
} else if (timestamp >= END_TIMESTAMP) {
return totalAllocation;
} else {
return TFHE.div(TFHE.mul(totalAllocation, (timestamp - START_TIMESTAMP)), DURATION);
PacificYield marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
53 changes: 53 additions & 0 deletions contracts/finance/ConfidentialVestingWalletCliff.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// SPDX-License-Identifier: BSD-3-Clause
pragma solidity ^0.8.24;

import "fhevm/lib/TFHE.sol";

import { ConfidentialVestingWallet } from "./ConfidentialVestingWallet.sol";

/**
* @title ConfidentialVestingWalletCliff
* @notice This contract offers a simple vesting wallet with a cliff for ConfidentialERC20 tokens.
* This is based on the VestingWalletCliff.sol contract written by OpenZeppelin.
* see: openzeppelin/openzeppelin-contracts/blob/master/contracts/finance/VestingWalletCliff.sol
* @dev This implementation is a linear vesting curve with a cliff.
* To use with the native asset, it is necessary to wrap the native asset to a ConfidentialERC20-like token.
*/
abstract contract ConfidentialVestingWalletCliff is ConfidentialVestingWallet {
/// @notice Returned if the cliff duration is greater than the vesting duration.
error InvalidCliffDuration(uint64 cliffSeconds, uint64 durationSeconds);

/// @notice Cliff timestamp.
uint64 public immutable CLIFF;

/**
* @param beneficiary_ Beneficiary address.
* @param token_ Confidential token address.
* @param startTimestamp_ Start timestamp.
* @param duration_ Duration (in seconds).
* @param cliffSeconds_ Cliff (in seconds).
*/
constructor(
address beneficiary_,
address token_,
uint64 startTimestamp_,
uint64 duration_,
uint64 cliffSeconds_
) ConfidentialVestingWallet(beneficiary_, token_, startTimestamp_, duration_) {
if (cliffSeconds_ > duration_) {
revert InvalidCliffDuration(cliffSeconds_, duration_);
}

CLIFF = startTimestamp_ + cliffSeconds_;
}

/**
* @notice Return the vested amount based on a linear vesting schedule with a cliff.
* @param totalAllocation Total allocation that is vested.
* @param timestamp Current timestamp.
* @return vestedAmount Vested amount.
*/
function _vestingSchedule(euint64 totalAllocation, uint64 timestamp) internal virtual override returns (euint64) {
return timestamp < CLIFF ? _EUINT64_ZERO : super._vestingSchedule(totalAllocation, timestamp);
}
}
16 changes: 16 additions & 0 deletions contracts/test/finance/TestConfidentialVestingWallet.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity ^0.8.24;

import { ConfidentialVestingWallet } from "../../finance/ConfidentialVestingWallet.sol";
import { SepoliaZamaFHEVMConfig } from "fhevm/config/ZamaFHEVMConfig.sol";

contract TestConfidentialVestingWallet is SepoliaZamaFHEVMConfig, ConfidentialVestingWallet {
constructor(
address beneficiary_,
address token_,
uint64 startTimestamp_,
uint64 duration_
) ConfidentialVestingWallet(beneficiary_, token_, startTimestamp_, duration_) {
//
}
}
17 changes: 17 additions & 0 deletions contracts/test/finance/TestConfidentialVestingWalletCliff.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity ^0.8.24;

import { ConfidentialVestingWalletCliff } from "../../finance/ConfidentialVestingWalletCliff.sol";
import { SepoliaZamaFHEVMConfig } from "fhevm/config/ZamaFHEVMConfig.sol";

contract TestConfidentialVestingWalletCliff is SepoliaZamaFHEVMConfig, ConfidentialVestingWalletCliff {
constructor(
address beneficiary_,
address token_,
uint64 startTimestamp_,
uint64 duration_,
uint64 cliff_
) ConfidentialVestingWalletCliff(beneficiary_, token_, startTimestamp_, duration_, cliff_) {
//
}
}
30 changes: 30 additions & 0 deletions test/finance/ConfidentialVestingWallet.fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Signer } from "ethers";
import { FhevmInstance } from "fhevmjs/node";
import { ethers } from "hardhat";

import type { ConfidentialVestingWallet, TestConfidentialVestingWallet } from "../../types";
import { reencryptEuint64 } from "../reencrypt";

export async function deployConfidentialVestingWalletFixture(
account: Signer,
beneficiaryAddress: string,
token: string,
startTimestamp: bigint,
duration: bigint,
): Promise<TestConfidentialVestingWallet> {
const contractFactory = await ethers.getContractFactory("TestConfidentialVestingWallet");
const contract = await contractFactory.connect(account).deploy(beneficiaryAddress, token, startTimestamp, duration);
await contract.waitForDeployment();
return contract;
}

export async function reencryptReleased(
account: Signer,
instance: FhevmInstance,
vestingWallet: ConfidentialVestingWallet,
vestingWalletAddress: string,
): Promise<bigint> {
const releasedHandled = await vestingWallet.released();
const releasedAmount = await reencryptEuint64(account, instance, releasedHandled, vestingWalletAddress);
return releasedAmount;
}
Loading
Loading