-
Notifications
You must be signed in to change notification settings - Fork 2
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
Changes from 3 commits
4b53bef
481f54f
e536334
1476a51
73be915
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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(); | ||
|
||
/// @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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||
} | ||
} | ||
} |
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); | ||
} | ||
} |
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_) { | ||
// | ||
} | ||
} |
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_) { | ||
// | ||
} | ||
} |
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; | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adjusted.