-
Notifications
You must be signed in to change notification settings - Fork 30
Fund multiple VestingWalletConfidential in batch
#102
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
Merged
arr00
merged 29 commits into
master
from
james/feature/batch-fund-vesting-wallets-executor
Jul 11, 2025
Merged
Changes from all commits
Commits
Show all changes
29 commits
Select commit
Hold shift + click to select a range
76b9398
Fund multiple `VestingWalletConfidential` in batch
james-toussaint 5d3a271
Deploy full vesting wallets from factory
james-toussaint d507607
Increase pragma in factory
james-toussaint 0d211a1
fix types in `_vestingSchedule`
arr00 6418c8e
Update changeset
james-toussaint c12ab22
Add more context to events
james-toussaint c23abab
Update doc & comments
james-toussaint 0e57d7a
Format doc
james-toussaint 064d19b
Check cliff in batcher
james-toussaint b6c4134
Remove total transfered amount computation in batcher
james-toussaint ee6a464
Set factory as non abstract
james-toussaint 966aef6
Lighten vesting struct & check beneficiary from batcher
james-toussaint 5b4e8e1
up
arr00 70d754f
`ERC7821WithExecutor` instead of `VestingWalletExecutorConfidential` …
arr00 c92718a
clean
arr00 c8b5116
fix imports order
arr00 e8a91a8
Apply suggestions from code review
arr00 80e272a
up
arr00 83320ca
update comments
arr00 19629c2
remove constructor
arr00 d45da96
fix function ordering
arr00 3da80bf
add changeset
arr00 4f665d4
Update .changeset/tricky-boxes-train.md
arr00 d835bf0
`VestingWalletConfidentialFactory` -> `VestingWalletCliffExecutorConf…
arr00 41a7440
Merge branch 'james/feature/batch-fund-vesting-wallets-executor' of h…
arr00 353f1ed
Duration and Cliff per vesting plan (#105)
arr00 4d87ede
Update .changeset/poor-colts-glow.md
arr00 2ad4f1f
upgrade pragmas
arr00 0a6732a
fix docs
arr00 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| 'openzeppelin-confidential-contracts': minor | ||
| --- | ||
|
|
||
| `ERC7821WithExecutor`: Add an abstract contract that inherits from `ERC7821` and adds an `executor` role. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| 'openzeppelin-confidential-contracts': minor | ||
| --- | ||
|
|
||
| `VestingWalletCliffExecutorConfidentialFactory`: Fund multiple `VestingWalletCliffExecutorConfidential` in batch. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.20; | ||
|
|
||
| import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; | ||
| import {ERC7821} from "@openzeppelin/contracts/account/extensions/draft-ERC7821.sol"; | ||
|
|
||
| /** | ||
| * @dev Extension of `ERC7821` that adds an {executor} address that is able to execute arbitrary calls via `ERC7821.execute`. | ||
| */ | ||
| abstract contract ERC7821WithExecutor is Initializable, ERC7821 { | ||
| /// @custom:storage-location erc7201:openzeppelin.storage.ERC7821WithExecutor | ||
| struct ERC7821WithExecutorStorage { | ||
| address _executor; | ||
| } | ||
|
|
||
| // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.ERC7821WithExecutor")) - 1)) & ~bytes32(uint256(0xff)) | ||
| // solhint-disable-next-line const-name-snakecase | ||
| bytes32 private constant ERC7821WithExecutorStorageLocation = | ||
| 0x246106ffca67a7d3806ba14f6748826b9c39c9fa594b14f83fe454e8e9d0dc00; | ||
|
|
||
| /// @dev Trusted address that is able to execute arbitrary calls from the vesting wallet via `ERC7821.execute`. | ||
| function executor() public view virtual returns (address) { | ||
| return _getERC7821WithExecutorStorage()._executor; | ||
| } | ||
|
|
||
| // solhint-disable-next-line func-name-mixedcase | ||
| function __ERC7821WithExecutor_init(address executor_) internal onlyInitializing { | ||
| _getERC7821WithExecutorStorage()._executor = executor_; | ||
| } | ||
|
|
||
| /// @inheritdoc ERC7821 | ||
| function _erc7821AuthorizedExecutor( | ||
| address caller, | ||
| bytes32 mode, | ||
| bytes calldata executionData | ||
| ) internal view virtual override returns (bool) { | ||
| return caller == executor() || super._erc7821AuthorizedExecutor(caller, mode, executionData); | ||
| } | ||
|
|
||
| function _getERC7821WithExecutorStorage() private pure returns (ERC7821WithExecutorStorage storage $) { | ||
| assembly { | ||
| $.slot := ERC7821WithExecutorStorageLocation | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
202 changes: 202 additions & 0 deletions
202
contracts/finance/VestingWalletCliffExecutorConfidentialFactory.sol
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,202 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.27; | ||
|
|
||
| import {FHE, euint64, externalEuint64, euint128} from "@fhevm/solidity/lib/FHE.sol"; | ||
| import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; | ||
| import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; | ||
| import {IConfidentialFungibleToken} from "./../interfaces/IConfidentialFungibleToken.sol"; | ||
| import {ERC7821WithExecutor} from "./ERC7821WithExecutor.sol"; | ||
| import {VestingWalletCliffConfidential} from "./VestingWalletCliffConfidential.sol"; | ||
| import {VestingWalletConfidential} from "./VestingWalletConfidential.sol"; | ||
|
|
||
| /** | ||
| * @dev This factory enables creating {VestingWalletCliffExecutorConfidential} in batch. | ||
| * | ||
| * Confidential vesting wallets created inherit both {VestingWalletCliffConfidential} for vesting cliffs | ||
| * and {ERC7821WithExecutor} to allow for arbitrary calls to be executed from the vesting wallet. | ||
| */ | ||
| contract VestingWalletCliffExecutorConfidentialFactory { | ||
| struct VestingPlan { | ||
| address beneficiary; | ||
| externalEuint64 encryptedAmount; | ||
| uint48 startTimestamp; | ||
| uint48 durationSeconds; | ||
| uint48 cliffSeconds; | ||
| } | ||
|
|
||
| address private immutable _vestingImplementation = address(new VestingWalletCliffExecutorConfidential()); | ||
|
|
||
| event VestingWalletConfidentialFunded( | ||
| address indexed vestingWalletConfidential, | ||
| address indexed beneficiary, | ||
| address indexed confidentialFungibleToken, | ||
| euint64 encryptedAmount, | ||
| uint48 startTimestamp, | ||
| uint48 durationSeconds, | ||
| uint48 cliffSeconds, | ||
| address executor | ||
| ); | ||
| event VestingWalletConfidentialCreated( | ||
| address indexed vestingWalletConfidential, | ||
| address indexed beneficiary, | ||
| uint48 startTimestamp, | ||
| uint48 durationSeconds, | ||
| uint48 cliffSeconds, | ||
| address indexed executor | ||
| ); | ||
|
|
||
| /** | ||
| * @dev Batches the funding of multiple confidential vesting wallets. | ||
| * | ||
| * Funds are sent to deterministic wallet addresses. Wallets can be created either | ||
| * before or after this operation. | ||
| * | ||
| * Emits a {VestingWalletConfidentialFunded} event for each funded vesting plan. | ||
| */ | ||
| function batchFundVestingWalletConfidential( | ||
| address confidentialFungibleToken, | ||
| VestingPlan[] calldata vestingPlans, | ||
| address executor, | ||
| bytes calldata inputProof | ||
| ) public virtual { | ||
| uint256 vestingPlansLength = vestingPlans.length; | ||
| for (uint256 i = 0; i < vestingPlansLength; i++) { | ||
| VestingPlan memory vestingPlan = vestingPlans[i]; | ||
| require( | ||
| vestingPlan.cliffSeconds <= vestingPlan.durationSeconds, | ||
| VestingWalletCliffConfidential.VestingWalletCliffConfidentialInvalidCliffDuration( | ||
| vestingPlan.cliffSeconds, | ||
| vestingPlan.durationSeconds | ||
| ) | ||
| ); | ||
|
|
||
| require(vestingPlan.beneficiary != address(0), OwnableUpgradeable.OwnableInvalidOwner(address(0))); | ||
| address vestingWalletAddress = predictVestingWalletConfidential( | ||
| vestingPlan.beneficiary, | ||
| vestingPlan.startTimestamp, | ||
| vestingPlan.durationSeconds, | ||
| vestingPlan.cliffSeconds, | ||
| executor | ||
| ); | ||
|
|
||
| euint64 transferredAmount; | ||
| { | ||
| // avoiding stack too deep with scope | ||
| euint64 encryptedAmount = FHE.fromExternal(vestingPlan.encryptedAmount, inputProof); | ||
| FHE.allowTransient(encryptedAmount, confidentialFungibleToken); | ||
| transferredAmount = IConfidentialFungibleToken(confidentialFungibleToken).confidentialTransferFrom( | ||
| msg.sender, | ||
| vestingWalletAddress, | ||
| encryptedAmount | ||
| ); | ||
| } | ||
|
|
||
| emit VestingWalletConfidentialFunded( | ||
| vestingWalletAddress, | ||
| vestingPlan.beneficiary, | ||
| confidentialFungibleToken, | ||
| transferredAmount, | ||
| vestingPlan.startTimestamp, | ||
| vestingPlan.durationSeconds, | ||
| vestingPlan.cliffSeconds, | ||
| executor | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * @dev Creates a confidential vesting wallet. | ||
| * | ||
| * Emits a {VestingWalletConfidentialCreated}. | ||
| */ | ||
| function createVestingWalletConfidential( | ||
| address beneficiary, | ||
| uint48 startTimestamp, | ||
| uint48 durationSeconds, | ||
| uint48 cliffSeconds, | ||
| address executor | ||
| ) public virtual returns (address) { | ||
| // Will revert if clone already created | ||
| address vestingWalletConfidentialAddress = Clones.cloneDeterministic( | ||
| _vestingImplementation, | ||
| _getCreate2VestingWalletConfidentialSalt( | ||
| beneficiary, | ||
| startTimestamp, | ||
| durationSeconds, | ||
| cliffSeconds, | ||
| executor | ||
| ) | ||
| ); | ||
| VestingWalletCliffExecutorConfidential(vestingWalletConfidentialAddress).initialize( | ||
| beneficiary, | ||
| startTimestamp, | ||
| durationSeconds, | ||
| cliffSeconds, | ||
| executor | ||
| ); | ||
| emit VestingWalletConfidentialCreated( | ||
| beneficiary, | ||
| vestingWalletConfidentialAddress, | ||
| startTimestamp, | ||
| durationSeconds, | ||
| cliffSeconds, | ||
| executor | ||
| ); | ||
| return vestingWalletConfidentialAddress; | ||
| } | ||
|
|
||
| /** | ||
| * @dev Predicts deterministic address for a confidential vesting wallet. | ||
| */ | ||
| function predictVestingWalletConfidential( | ||
| address beneficiary, | ||
| uint48 startTimestamp, | ||
| uint48 durationSeconds, | ||
| uint48 cliffSeconds, | ||
| address executor | ||
| ) public view virtual returns (address) { | ||
| return | ||
| Clones.predictDeterministicAddress( | ||
| _vestingImplementation, | ||
| _getCreate2VestingWalletConfidentialSalt( | ||
| beneficiary, | ||
| startTimestamp, | ||
| durationSeconds, | ||
| cliffSeconds, | ||
| executor | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Gets create2 salt for a confidential vesting wallet. | ||
| */ | ||
| function _getCreate2VestingWalletConfidentialSalt( | ||
| address beneficiary, | ||
| uint48 startTimestamp, | ||
| uint48 durationSeconds, | ||
| uint48 cliffSeconds, | ||
| address executor | ||
| ) internal pure virtual returns (bytes32) { | ||
| return keccak256(abi.encodePacked(beneficiary, startTimestamp, durationSeconds, cliffSeconds, executor)); | ||
| } | ||
| } | ||
|
|
||
| // slither-disable-next-line locked-ether | ||
| contract VestingWalletCliffExecutorConfidential is VestingWalletCliffConfidential, ERC7821WithExecutor { | ||
| constructor() { | ||
| _disableInitializers(); | ||
| } | ||
|
|
||
| function initialize( | ||
| address beneficiary, | ||
| uint48 startTimestamp, | ||
| uint48 durationSeconds, | ||
| uint48 cliffSeconds, | ||
| address executor | ||
| ) public initializer { | ||
| __VestingWalletConfidential_init(beneficiary, startTimestamp, durationSeconds); | ||
| __VestingWalletCliffConfidential_init(cliffSeconds); | ||
| __ERC7821WithExecutor_init(executor); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.