Skip to content

Commit 5c0af65

Browse files
arr00james-toussaintAmxxernestognw
committed
Fund multiple VestingWalletConfidential in batch (#102)
* Fund multiple `VestingWalletConfidential` in batch * Deploy full vesting wallets from factory * Increase pragma in factory * fix types in `_vestingSchedule` * Update changeset Co-authored-by: Arr00 <13561405+arr00@users.noreply.github.com> * Add more context to events * Update doc & comments * Format doc * Check cliff in batcher * Remove total transfered amount computation in batcher * Set factory as non abstract * Lighten vesting struct & check beneficiary from batcher * up * `ERC7821WithExecutor` instead of `VestingWalletExecutorConfidential` (#104) * Init executor with ERC7821 * Update `ERC7821WithExecutor` * rename executor file * update tests * move `ERC7821WithExecutor` test * fix tests * update test * use vanilla helpers * disable slither for locking ether --------- Co-authored-by: James Toussaint <33313130+james-toussaint@users.noreply.github.com> Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com> * clean * fix imports order * Apply suggestions from code review * up * update comments * remove constructor * fix function ordering * add changeset * Update .changeset/tricky-boxes-train.md Co-authored-by: Ernesto García <ernestognw@gmail.com> * `VestingWalletConfidentialFactory` -> `VestingWalletCliffExecutorConfidentialFactory` * Duration and Cliff per vesting plan (#105) * Update .changeset/poor-colts-glow.md * upgrade pragmas * fix docs --------- Co-authored-by: James Toussaint <33313130+james-toussaint@users.noreply.github.com> Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com> Co-authored-by: Ernesto García <ernestognw@gmail.com>
1 parent 4be6583 commit 5c0af65

16 files changed

+909
-191
lines changed

.changeset/poor-colts-glow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-confidential-contracts': minor
3+
---
4+
5+
`ERC7821WithExecutor`: Add an abstract contract that inherits from `ERC7821` and adds an `executor` role.

.changeset/tricky-boxes-train.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-confidential-contracts': minor
3+
---
4+
5+
`VestingWalletCliffExecutorConfidentialFactory`: Fund multiple `VestingWalletCliffExecutorConfidential` in batch.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
5+
import {ERC7821} from "@openzeppelin/contracts/account/extensions/draft-ERC7821.sol";
6+
7+
/**
8+
* @dev Extension of `ERC7821` that adds an {executor} address that is able to execute arbitrary calls via `ERC7821.execute`.
9+
*/
10+
abstract contract ERC7821WithExecutor is Initializable, ERC7821 {
11+
/// @custom:storage-location erc7201:openzeppelin.storage.ERC7821WithExecutor
12+
struct ERC7821WithExecutorStorage {
13+
address _executor;
14+
}
15+
16+
// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.ERC7821WithExecutor")) - 1)) & ~bytes32(uint256(0xff))
17+
// solhint-disable-next-line const-name-snakecase
18+
bytes32 private constant ERC7821WithExecutorStorageLocation =
19+
0x246106ffca67a7d3806ba14f6748826b9c39c9fa594b14f83fe454e8e9d0dc00;
20+
21+
/// @dev Trusted address that is able to execute arbitrary calls from the vesting wallet via `ERC7821.execute`.
22+
function executor() public view virtual returns (address) {
23+
return _getERC7821WithExecutorStorage()._executor;
24+
}
25+
26+
// solhint-disable-next-line func-name-mixedcase
27+
function __ERC7821WithExecutor_init(address executor_) internal onlyInitializing {
28+
_getERC7821WithExecutorStorage()._executor = executor_;
29+
}
30+
31+
/// @inheritdoc ERC7821
32+
function _erc7821AuthorizedExecutor(
33+
address caller,
34+
bytes32 mode,
35+
bytes calldata executionData
36+
) internal view virtual override returns (bool) {
37+
return caller == executor() || super._erc7821AuthorizedExecutor(caller, mode, executionData);
38+
}
39+
40+
function _getERC7821WithExecutorStorage() private pure returns (ERC7821WithExecutorStorage storage $) {
41+
assembly {
42+
$.slot := ERC7821WithExecutorStorageLocation
43+
}
44+
}
45+
}

contracts/finance/README.adoc

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,16 @@ This directory includes primitives for on-chain confidential financial systems:
88

99
- {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.
1010
- {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).
11+
12+
For convenience, this directory also includes:
13+
14+
- {VestingWalletCliffExecutorConfidential}: A Default implementation of `VestingWalletConfidential` which implements both `VestingWalletCliffConfidential` and execution via {ERC7821WithExecutor}.
15+
- {VestingWalletCliffExecutorConfidentialFactory}: A factory which allows creating `VestingWalletCliffExecutorConfidential` in batch.
16+
1217

1318
== Contracts
1419
{{VestingWalletConfidential}}
1520
{{VestingWalletCliffConfidential}}
16-
{{VestingWalletExecutorConfidential}}
21+
{{VestingWalletCliffExecutorConfidentialFactory}}
22+
{{VestingWalletCliffExecutorConfidential}}
23+
{{ERC7821WithExecutor}}

contracts/finance/VestingWalletCliffConfidential.sol

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// SPDX-License-Identifier: MIT
2-
pragma solidity ^0.8.24;
2+
pragma solidity ^0.8.27;
33

44
import {euint128} from "@fhevm/solidity/lib/FHE.sol";
55
import {VestingWalletConfidential} from "./VestingWalletConfidential.sol";
@@ -19,30 +19,26 @@ abstract contract VestingWalletCliffConfidential is VestingWalletConfidential {
1919
bytes32 private constant VestingWalletCliffStorageLocation =
2020
0x3c715f77db997bdb68403fafb54820cd57dedce553ed6315028656b0d601c700;
2121

22-
function _getVestingWalletCliffStorage() private pure returns (VestingWalletCliffStorage storage $) {
23-
assembly {
24-
$.slot := VestingWalletCliffStorageLocation
25-
}
26-
}
27-
2822
/// @dev The specified cliff duration is larger than the vesting duration.
29-
error InvalidCliffDuration(uint64 cliffSeconds, uint64 durationSeconds);
23+
error VestingWalletCliffConfidentialInvalidCliffDuration(uint64 cliffSeconds, uint64 durationSeconds);
24+
25+
/// @dev The timestamp at which the cliff ends.
26+
function cliff() public view virtual returns (uint64) {
27+
return _getVestingWalletCliffStorage()._cliff;
28+
}
3029

3130
/**
3231
* @dev Set the duration of the cliff, in seconds. The cliff starts at the vesting
3332
* start timestamp (see {VestingWalletConfidential-start}) and ends `cliffSeconds` later.
3433
*/
3534
// solhint-disable-next-line func-name-mixedcase
3635
function __VestingWalletCliffConfidential_init(uint48 cliffSeconds) internal onlyInitializing {
37-
if (cliffSeconds > duration()) {
38-
revert InvalidCliffDuration(cliffSeconds, duration());
39-
}
40-
_getVestingWalletCliffStorage()._cliff = start() + cliffSeconds;
41-
}
36+
require(
37+
cliffSeconds <= duration(),
38+
VestingWalletCliffConfidentialInvalidCliffDuration(cliffSeconds, duration())
39+
);
4240

43-
/// @dev The timestamp at which the cliff ends.
44-
function cliff() public view virtual returns (uint64) {
45-
return _getVestingWalletCliffStorage()._cliff;
41+
_getVestingWalletCliffStorage()._cliff = start() + cliffSeconds;
4642
}
4743

4844
/**
@@ -56,4 +52,10 @@ abstract contract VestingWalletCliffConfidential is VestingWalletConfidential {
5652
function _vestingSchedule(euint128 totalAllocation, uint64 timestamp) internal virtual override returns (euint128) {
5753
return timestamp < cliff() ? euint128.wrap(0) : super._vestingSchedule(totalAllocation, timestamp);
5854
}
55+
56+
function _getVestingWalletCliffStorage() private pure returns (VestingWalletCliffStorage storage $) {
57+
assembly {
58+
$.slot := VestingWalletCliffStorageLocation
59+
}
60+
}
5961
}
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.27;
3+
4+
import {FHE, euint64, externalEuint64, euint128} from "@fhevm/solidity/lib/FHE.sol";
5+
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
6+
import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
7+
import {IConfidentialFungibleToken} from "./../interfaces/IConfidentialFungibleToken.sol";
8+
import {ERC7821WithExecutor} from "./ERC7821WithExecutor.sol";
9+
import {VestingWalletCliffConfidential} from "./VestingWalletCliffConfidential.sol";
10+
import {VestingWalletConfidential} from "./VestingWalletConfidential.sol";
11+
12+
/**
13+
* @dev This factory enables creating {VestingWalletCliffExecutorConfidential} in batch.
14+
*
15+
* Confidential vesting wallets created inherit both {VestingWalletCliffConfidential} for vesting cliffs
16+
* and {ERC7821WithExecutor} to allow for arbitrary calls to be executed from the vesting wallet.
17+
*/
18+
contract VestingWalletCliffExecutorConfidentialFactory {
19+
struct VestingPlan {
20+
address beneficiary;
21+
externalEuint64 encryptedAmount;
22+
uint48 startTimestamp;
23+
uint48 durationSeconds;
24+
uint48 cliffSeconds;
25+
}
26+
27+
address private immutable _vestingImplementation = address(new VestingWalletCliffExecutorConfidential());
28+
29+
event VestingWalletConfidentialFunded(
30+
address indexed vestingWalletConfidential,
31+
address indexed beneficiary,
32+
address indexed confidentialFungibleToken,
33+
euint64 encryptedAmount,
34+
uint48 startTimestamp,
35+
uint48 durationSeconds,
36+
uint48 cliffSeconds,
37+
address executor
38+
);
39+
event VestingWalletConfidentialCreated(
40+
address indexed vestingWalletConfidential,
41+
address indexed beneficiary,
42+
uint48 startTimestamp,
43+
uint48 durationSeconds,
44+
uint48 cliffSeconds,
45+
address indexed executor
46+
);
47+
48+
/**
49+
* @dev Batches the funding of multiple confidential vesting wallets.
50+
*
51+
* Funds are sent to deterministic wallet addresses. Wallets can be created either
52+
* before or after this operation.
53+
*
54+
* Emits a {VestingWalletConfidentialFunded} event for each funded vesting plan.
55+
*/
56+
function batchFundVestingWalletConfidential(
57+
address confidentialFungibleToken,
58+
VestingPlan[] calldata vestingPlans,
59+
address executor,
60+
bytes calldata inputProof
61+
) public virtual {
62+
uint256 vestingPlansLength = vestingPlans.length;
63+
for (uint256 i = 0; i < vestingPlansLength; i++) {
64+
VestingPlan memory vestingPlan = vestingPlans[i];
65+
require(
66+
vestingPlan.cliffSeconds <= vestingPlan.durationSeconds,
67+
VestingWalletCliffConfidential.VestingWalletCliffConfidentialInvalidCliffDuration(
68+
vestingPlan.cliffSeconds,
69+
vestingPlan.durationSeconds
70+
)
71+
);
72+
73+
require(vestingPlan.beneficiary != address(0), OwnableUpgradeable.OwnableInvalidOwner(address(0)));
74+
address vestingWalletAddress = predictVestingWalletConfidential(
75+
vestingPlan.beneficiary,
76+
vestingPlan.startTimestamp,
77+
vestingPlan.durationSeconds,
78+
vestingPlan.cliffSeconds,
79+
executor
80+
);
81+
82+
euint64 transferredAmount;
83+
{
84+
// avoiding stack too deep with scope
85+
euint64 encryptedAmount = FHE.fromExternal(vestingPlan.encryptedAmount, inputProof);
86+
FHE.allowTransient(encryptedAmount, confidentialFungibleToken);
87+
transferredAmount = IConfidentialFungibleToken(confidentialFungibleToken).confidentialTransferFrom(
88+
msg.sender,
89+
vestingWalletAddress,
90+
encryptedAmount
91+
);
92+
}
93+
94+
emit VestingWalletConfidentialFunded(
95+
vestingWalletAddress,
96+
vestingPlan.beneficiary,
97+
confidentialFungibleToken,
98+
transferredAmount,
99+
vestingPlan.startTimestamp,
100+
vestingPlan.durationSeconds,
101+
vestingPlan.cliffSeconds,
102+
executor
103+
);
104+
}
105+
}
106+
107+
/**
108+
* @dev Creates a confidential vesting wallet.
109+
*
110+
* Emits a {VestingWalletConfidentialCreated}.
111+
*/
112+
function createVestingWalletConfidential(
113+
address beneficiary,
114+
uint48 startTimestamp,
115+
uint48 durationSeconds,
116+
uint48 cliffSeconds,
117+
address executor
118+
) public virtual returns (address) {
119+
// Will revert if clone already created
120+
address vestingWalletConfidentialAddress = Clones.cloneDeterministic(
121+
_vestingImplementation,
122+
_getCreate2VestingWalletConfidentialSalt(
123+
beneficiary,
124+
startTimestamp,
125+
durationSeconds,
126+
cliffSeconds,
127+
executor
128+
)
129+
);
130+
VestingWalletCliffExecutorConfidential(vestingWalletConfidentialAddress).initialize(
131+
beneficiary,
132+
startTimestamp,
133+
durationSeconds,
134+
cliffSeconds,
135+
executor
136+
);
137+
emit VestingWalletConfidentialCreated(
138+
beneficiary,
139+
vestingWalletConfidentialAddress,
140+
startTimestamp,
141+
durationSeconds,
142+
cliffSeconds,
143+
executor
144+
);
145+
return vestingWalletConfidentialAddress;
146+
}
147+
148+
/**
149+
* @dev Predicts deterministic address for a confidential vesting wallet.
150+
*/
151+
function predictVestingWalletConfidential(
152+
address beneficiary,
153+
uint48 startTimestamp,
154+
uint48 durationSeconds,
155+
uint48 cliffSeconds,
156+
address executor
157+
) public view virtual returns (address) {
158+
return
159+
Clones.predictDeterministicAddress(
160+
_vestingImplementation,
161+
_getCreate2VestingWalletConfidentialSalt(
162+
beneficiary,
163+
startTimestamp,
164+
durationSeconds,
165+
cliffSeconds,
166+
executor
167+
)
168+
);
169+
}
170+
171+
/**
172+
* @dev Gets create2 salt for a confidential vesting wallet.
173+
*/
174+
function _getCreate2VestingWalletConfidentialSalt(
175+
address beneficiary,
176+
uint48 startTimestamp,
177+
uint48 durationSeconds,
178+
uint48 cliffSeconds,
179+
address executor
180+
) internal pure virtual returns (bytes32) {
181+
return keccak256(abi.encodePacked(beneficiary, startTimestamp, durationSeconds, cliffSeconds, executor));
182+
}
183+
}
184+
185+
// slither-disable-next-line locked-ether
186+
contract VestingWalletCliffExecutorConfidential is VestingWalletCliffConfidential, ERC7821WithExecutor {
187+
constructor() {
188+
_disableInitializers();
189+
}
190+
191+
function initialize(
192+
address beneficiary,
193+
uint48 startTimestamp,
194+
uint48 durationSeconds,
195+
uint48 cliffSeconds,
196+
address executor
197+
) public initializer {
198+
__VestingWalletConfidential_init(beneficiary, startTimestamp, durationSeconds);
199+
__VestingWalletCliffConfidential_init(cliffSeconds);
200+
__ERC7821WithExecutor_init(executor);
201+
}
202+
}

0 commit comments

Comments
 (0)