Skip to content

Commit faf86c9

Browse files
arr00james-toussaintAmxx
authored
Add Vesting Wallet (#91)
* Add Vesting Wallet * fix and add tests * remove unused imports * fix test * add operator * fix test * add changeset * make vesting wallets cloneable * Apply suggestions from code review Co-authored-by: James Toussaint <33313130+james-toussaint@users.noreply.github.com> * add call event * add initializable version of vesting (#95) * add initializable version of vesting * update tests * remove factory mock * rename mock files * fix imports * remove upgradeable dependency * add docs * revert `package-lock.json` changes * fix import paths * fix lint * Update contracts/finance/VestingWalletConfidential.sol Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com> * fix tests * add reentrancy protection * extract executor into extension * forge install: openzeppelin-contracts-upgradeable v5.3.0 * update package * fix overflow risk * Add vesting wallet namespace storage (#96) * Add vesting wallet namespace storage * update pragmas * reorder functions * fix lint and inline getting storage --------- Co-authored-by: Arr00 <13561405+arr00@users.noreply.github.com> * update docs * update cliff seconds param size * remove upgradeable file * add docs * Update .changeset/cold-nails-go.md Co-authored-by: James Toussaint <33313130+james-toussaint@users.noreply.github.com> * update comments --------- Co-authored-by: James Toussaint <33313130+james-toussaint@users.noreply.github.com> Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com>
1 parent 38445dc commit faf86c9

25 files changed

+558
-16
lines changed

.changeset/cold-nails-go.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'openzeppelin-confidential-contracts': minor
3+
---
4+
5+
`VestingWalletConfidential`: A vesting wallet that releases confidential tokens owned by it according to a defined vesting schedule.
6+
`VestingWalletCliffConfidential`: A variant of `VestingWalletConfidential` which adds a cliff period to the vesting schedule.
7+
`VestingWalletExecutorConfidential`: A variant of `VestingWalletConfidential` which allows a trusted executor to execute arbitrary calls from the vesting wallet.

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
[submodule "lib/openzeppelin-contracts"]
22
path = lib/openzeppelin-contracts
33
url = https://github.com/OpenZeppelin/openzeppelin-contracts
4+
[submodule "lib/openzeppelin-contracts-upgradeable"]
5+
path = lib/openzeppelin-contracts-upgradeable
6+
url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable

contracts/finance/README.adoc

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
2+
= Finance
3+
4+
[.readme-notice]
5+
NOTE: This document is better viewed at https://docs.openzeppelin.com/confidential-contracts/api#finance
6+
7+
This directory includes primitives for on-chain confidential financial systems:
8+
9+
- {VestingWalletConfidential}: Handles the vesting of confidential tokens for a given beneficiary. Custody of multiple tokens can be given to this contract, which will release the token to the beneficiary following a given, customizable, vesting schedule.
10+
- {VestingWalletCliffConfidential}: Variant of {VestingWalletConfidential} which adds a cliff period to the vesting schedule.
11+
- {VestingWalletExecutorConfidential}: Extension of {VestingWalletConfidential} that adds an executor role able to perform arbitrary calls on behalf of the vesting wallet (e.g. to vote, stake, or perform other management operations).
12+
13+
== Contracts
14+
{{VestingWalletConfidential}}
15+
{{VestingWalletCliffConfidential}}
16+
{{VestingWalletExecutorConfidential}}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.24;
3+
4+
import {euint64} from "@fhevm/solidity/lib/FHE.sol";
5+
import {VestingWalletConfidential} from "./VestingWalletConfidential.sol";
6+
7+
/**
8+
* @dev An extension of {VestingWalletConfidential} that adds a cliff to the vesting schedule. The cliff is `cliffSeconds` long and
9+
* starts at the vesting start timestamp (see {VestingWalletConfidential}).
10+
*/
11+
abstract contract VestingWalletCliffConfidential is VestingWalletConfidential {
12+
/// @custom:storage-location erc7201:openzeppelin.storage.VestingWalletCliffConfidential
13+
struct VestingWalletCliffStorage {
14+
uint64 _cliff;
15+
}
16+
17+
// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.VestingWalletCliffConfidential")) - 1)) & ~bytes32(uint256(0xff))
18+
// solhint-disable-next-line const-name-snakecase
19+
bytes32 private constant VestingWalletCliffStorageLocation =
20+
0x3c715f77db997bdb68403fafb54820cd57dedce553ed6315028656b0d601c700;
21+
22+
function _getVestingWalletCliffStorage() private pure returns (VestingWalletCliffStorage storage $) {
23+
assembly {
24+
$.slot := VestingWalletCliffStorageLocation
25+
}
26+
}
27+
28+
/// @dev The specified cliff duration is larger than the vesting duration.
29+
error InvalidCliffDuration(uint64 cliffSeconds, uint64 durationSeconds);
30+
31+
/**
32+
* @dev Set the duration of the cliff, in seconds. The cliff starts at the vesting
33+
* start timestamp (see {VestingWalletConfidential-start}) and ends `cliffSeconds` later.
34+
*/
35+
// solhint-disable-next-line func-name-mixedcase
36+
function __VestingWalletCliffConfidential_init(uint48 cliffSeconds) internal onlyInitializing {
37+
if (cliffSeconds > duration()) {
38+
revert InvalidCliffDuration(cliffSeconds, duration());
39+
}
40+
_getVestingWalletCliffStorage()._cliff = start() + cliffSeconds;
41+
}
42+
43+
/// @dev The timestamp at which the cliff ends.
44+
function cliff() public view virtual returns (uint64) {
45+
return _getVestingWalletCliffStorage()._cliff;
46+
}
47+
48+
/**
49+
* @dev This function returns the amount vested, as a function of time, for
50+
* an asset given its total historical allocation. Returns 0 if the {cliff} timestamp is not met.
51+
*
52+
* IMPORTANT: The cliff not only makes the schedule return 0, but it also ignores every possible side
53+
* effect from calling the inherited implementation (i.e. `super._vestingSchedule`). Carefully consider
54+
* this caveat if the overridden implementation of this function has any (e.g. writing to memory or reverting).
55+
*/
56+
function _vestingSchedule(euint64 totalAllocation, uint64 timestamp) internal virtual override returns (euint64) {
57+
return timestamp < cliff() ? euint64.wrap(0) : super._vestingSchedule(totalAllocation, timestamp);
58+
}
59+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.24;
3+
4+
import {FHE, ebool, euint64} from "@fhevm/solidity/lib/FHE.sol";
5+
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
6+
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
7+
import {ReentrancyGuardTransient} from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol";
8+
import {IConfidentialFungibleToken} from "./../interfaces/IConfidentialFungibleToken.sol";
9+
import {TFHESafeMath} from "./../utils/TFHESafeMath.sol";
10+
11+
/**
12+
* @dev A vesting wallet is an ownable contract that can receive ConfidentialFungibleTokens, and release these
13+
* assets to the wallet owner, also referred to as "beneficiary", according to a vesting schedule.
14+
*
15+
* Any assets transferred to this contract will follow the vesting schedule as if they were locked from the beginning.
16+
* Consequently, if the vesting has already started, any amount of tokens sent to this contract will (at least partly)
17+
* be immediately releasable.
18+
*
19+
* By setting the duration to 0, one can configure this contract to behave like an asset timelock that holds tokens for
20+
* a beneficiary until a specified time.
21+
*
22+
* NOTE: Since the wallet is `Ownable`, and ownership can be transferred, it is possible to sell unvested tokens.
23+
*
24+
* NOTE: When using this contract with any token whose balance is adjusted automatically (i.e. a rebase token), make
25+
* sure to account the supply/balance adjustment in the vesting schedule to ensure the vested amount is as intended.
26+
*
27+
* WARNING: The aggregate value of a single token sent to the vesting wallet must not exceed `type(uint64).max`. Sending
28+
* in excess of this value will result in unexpected behavior and may result in loss of funds.
29+
*/
30+
abstract contract VestingWalletConfidential is OwnableUpgradeable, ReentrancyGuardTransient {
31+
/// @custom:storage-location erc7201:openzeppelin.storage.VestingWalletConfidential
32+
struct VestingWalletStorage {
33+
mapping(address token => euint64) _tokenReleased;
34+
uint64 _start;
35+
uint64 _duration;
36+
}
37+
38+
// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.VestingWalletConfidential")) - 1)) & ~bytes32(uint256(0xff))
39+
// solhint-disable-next-line const-name-snakecase
40+
bytes32 private constant VestingWalletStorageLocation =
41+
0x78ce9ee9eb65fa0cf5bf10e861c3a95cb7c3c713c96ab1e5323a21e846796800;
42+
43+
function _getVestingWalletStorage() private pure returns (VestingWalletStorage storage $) {
44+
assembly {
45+
$.slot := VestingWalletStorageLocation
46+
}
47+
}
48+
49+
event VestingWalletConfidentialTokenReleased(address indexed token, euint64 amount);
50+
51+
error VestingWalletConfidentialInvalidDuration();
52+
53+
/**
54+
* @dev Initializes the vesting wallet for a given `beneficiary` with a start time of `startTimestamp`
55+
* and an end time of `startTimestamp + durationSeconds`.
56+
*/
57+
// solhint-disable-next-line func-name-mixedcase
58+
function __VestingWalletConfidential_init(
59+
address beneficiary,
60+
uint48 startTimestamp,
61+
uint48 durationSeconds
62+
) internal onlyInitializing {
63+
__Ownable_init(beneficiary);
64+
VestingWalletStorage storage $ = _getVestingWalletStorage();
65+
$._start = startTimestamp;
66+
$._duration = durationSeconds;
67+
}
68+
69+
/// @dev Timestamp at which the vesting starts.
70+
function start() public view virtual returns (uint64) {
71+
return _getVestingWalletStorage()._start;
72+
}
73+
74+
/// @dev Duration of the vesting in seconds.
75+
function duration() public view virtual returns (uint64) {
76+
return _getVestingWalletStorage()._duration;
77+
}
78+
79+
/// @dev Timestamp at which the vesting ends.
80+
function end() public view virtual returns (uint64) {
81+
return start() + duration();
82+
}
83+
84+
/// @dev Amount of token already released
85+
function released(address token) public view virtual returns (euint64) {
86+
return _getVestingWalletStorage()._tokenReleased[token];
87+
}
88+
89+
/**
90+
* @dev Getter for the amount of releasable `token` tokens. `token` should be the address of an
91+
* {IConfidentialFungibleToken} contract.
92+
*/
93+
function releasable(address token) public virtual returns (euint64) {
94+
return FHE.sub(vestedAmount(token, uint64(block.timestamp)), released(token));
95+
}
96+
97+
/**
98+
* @dev Release the tokens that have already vested.
99+
*
100+
* Emits a {VestingWalletConfidentialTokenReleased} event.
101+
*/
102+
function release(address token) public virtual nonReentrant {
103+
euint64 amount = releasable(token);
104+
FHE.allowTransient(amount, token);
105+
euint64 amountSent = IConfidentialFungibleToken(token).confidentialTransfer(owner(), amount);
106+
107+
euint64 newReleasedAmount = FHE.add(released(token), amountSent);
108+
FHE.allow(newReleasedAmount, owner());
109+
FHE.allowThis(newReleasedAmount);
110+
_getVestingWalletStorage()._tokenReleased[token] = newReleasedAmount;
111+
emit VestingWalletConfidentialTokenReleased(token, amountSent);
112+
}
113+
114+
/// @dev Calculates the amount of tokens that has already vested. Default implementation is a linear vesting curve.
115+
function vestedAmount(address token, uint64 timestamp) public virtual returns (euint64) {
116+
return
117+
_vestingSchedule(
118+
// Will overflow if the aggregate value of a single token sent to the vesting wallet exceeds `type(uint64).max`.
119+
FHE.add(IConfidentialFungibleToken(token).confidentialBalanceOf(address(this)), released(token)),
120+
timestamp
121+
);
122+
}
123+
124+
/// @dev This returns the amount vested, as a function of time, for an asset given its total historical allocation.
125+
function _vestingSchedule(euint64 totalAllocation, uint64 timestamp) internal virtual returns (euint64) {
126+
if (timestamp < start()) {
127+
return euint64.wrap(0);
128+
} else if (timestamp >= end()) {
129+
return totalAllocation;
130+
} else {
131+
return FHE.div(FHE.mul(totalAllocation, (timestamp - start())), duration());
132+
}
133+
}
134+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.24;
3+
4+
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
5+
import {VestingWalletConfidential} from "./VestingWalletConfidential.sol";
6+
7+
/**
8+
* @dev Extension of {VestingWalletConfidential} that adds an {executor} role able to perform arbitrary
9+
* calls on behalf of the vesting wallet (e.g. to vote, stake, or perform other management operations).
10+
*/
11+
abstract contract VestingWalletExecutorConfidential is VestingWalletConfidential {
12+
/// @custom:storage-location erc7201:openzeppelin.storage.VestingWalletExecutorConfidential
13+
struct VestingWalletExecutorStorage {
14+
address _executor;
15+
}
16+
17+
// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.VestingWalletExecutorConfidential")) - 1)) & ~bytes32(uint256(0xff))
18+
// solhint-disable-next-line const-name-snakecase
19+
bytes32 private constant VestingWalletExecutorStorageLocation =
20+
0x165c39f99e134d4ac22afe0db4de9fbb73791548e71f117f46b120e313690700;
21+
22+
function _getVestingWalletExecutorStorage() private pure returns (VestingWalletExecutorStorage storage $) {
23+
assembly {
24+
$.slot := VestingWalletExecutorStorageLocation
25+
}
26+
}
27+
28+
event VestingWalletExecutorConfidentialCallExecuted(address indexed target, uint256 value, bytes data);
29+
30+
/// @dev Thrown when a non-executor attempts to call {call}.
31+
error VestingWalletExecutorConfidentialOnlyExecutor();
32+
33+
// solhint-disable-next-line func-name-mixedcase
34+
function __VestingWalletExecutorConfidential_init(address executor_) internal onlyInitializing {
35+
_getVestingWalletExecutorStorage()._executor = executor_;
36+
}
37+
38+
/// @dev Trusted address that is able to execute arbitrary calls from the vesting wallet via {call}.
39+
function executor() public view virtual returns (address) {
40+
return _getVestingWalletExecutorStorage()._executor;
41+
}
42+
43+
/**
44+
* @dev Execute an arbitrary call from the vesting wallet. Only callable by the {executor}.
45+
*
46+
* Emits a {VestingWalletExecutorConfidentialCallExecuted} event.
47+
*/
48+
function call(address target, uint256 value, bytes memory data) public virtual {
49+
require(msg.sender == executor(), VestingWalletExecutorConfidentialOnlyExecutor());
50+
_call(target, value, data);
51+
}
52+
53+
/// @dev Internal function for executing an arbitrary call from the vesting wallet.
54+
function _call(address target, uint256 value, bytes memory data) internal virtual {
55+
(bool success, bytes memory res) = target.call{value: value}(data);
56+
Address.verifyCallResult(success, res);
57+
58+
emit VestingWalletExecutorConfidentialCallExecuted(target, value, data);
59+
}
60+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.24;
3+
4+
import {SepoliaConfig} from "@fhevm/solidity/config/ZamaConfig.sol";
5+
import {VestingWalletCliffConfidential} from "../../finance/VestingWalletCliffConfidential.sol";
6+
7+
abstract contract VestingWalletCliffConfidentialMock is VestingWalletCliffConfidential, SepoliaConfig {}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.24;
3+
4+
import {SepoliaConfig} from "@fhevm/solidity/config/ZamaConfig.sol";
5+
import {VestingWalletConfidential} from "../../finance/VestingWalletConfidential.sol";
6+
7+
abstract contract VestingWalletConfidentialMock is VestingWalletConfidential, SepoliaConfig {}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.24;
3+
4+
import {SepoliaConfig} from "@fhevm/solidity/config/ZamaConfig.sol";
5+
import {VestingWalletExecutorConfidential} from "../../finance/VestingWalletExecutorConfidential.sol";
6+
7+
abstract contract VestingWalletExecutorConfidentialMock is VestingWalletExecutorConfidential, SepoliaConfig {}

contracts/mocks/import.sol

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";

0 commit comments

Comments
 (0)