Skip to content
Merged
Show file tree
Hide file tree
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 Jul 10, 2025
5d3a271
Deploy full vesting wallets from factory
james-toussaint Jul 10, 2025
d507607
Increase pragma in factory
james-toussaint Jul 10, 2025
0d211a1
fix types in `_vestingSchedule`
arr00 Jul 10, 2025
6418c8e
Update changeset
james-toussaint Jul 11, 2025
c12ab22
Add more context to events
james-toussaint Jul 11, 2025
c23abab
Update doc & comments
james-toussaint Jul 11, 2025
0e57d7a
Format doc
james-toussaint Jul 11, 2025
064d19b
Check cliff in batcher
james-toussaint Jul 11, 2025
b6c4134
Remove total transfered amount computation in batcher
james-toussaint Jul 11, 2025
ee6a464
Set factory as non abstract
james-toussaint Jul 11, 2025
966aef6
Lighten vesting struct & check beneficiary from batcher
james-toussaint Jul 11, 2025
5b4e8e1
up
arr00 Jul 11, 2025
70d754f
`ERC7821WithExecutor` instead of `VestingWalletExecutorConfidential` …
arr00 Jul 11, 2025
c92718a
clean
arr00 Jul 11, 2025
c8b5116
fix imports order
arr00 Jul 11, 2025
e8a91a8
Apply suggestions from code review
arr00 Jul 11, 2025
80e272a
up
arr00 Jul 11, 2025
83320ca
update comments
arr00 Jul 11, 2025
19629c2
remove constructor
arr00 Jul 11, 2025
d45da96
fix function ordering
arr00 Jul 11, 2025
3da80bf
add changeset
arr00 Jul 11, 2025
4f665d4
Update .changeset/tricky-boxes-train.md
arr00 Jul 11, 2025
d835bf0
`VestingWalletConfidentialFactory` -> `VestingWalletCliffExecutorConf…
arr00 Jul 11, 2025
41a7440
Merge branch 'james/feature/batch-fund-vesting-wallets-executor' of h…
arr00 Jul 11, 2025
353f1ed
Duration and Cliff per vesting plan (#105)
arr00 Jul 11, 2025
4d87ede
Update .changeset/poor-colts-glow.md
arr00 Jul 11, 2025
2ad4f1f
upgrade pragmas
arr00 Jul 11, 2025
0a6732a
fix docs
arr00 Jul 11, 2025
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
5 changes: 5 additions & 0 deletions .changeset/poor-colts-glow.md
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.
5 changes: 5 additions & 0 deletions .changeset/tricky-boxes-train.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-confidential-contracts': minor
---

`VestingWalletCliffExecutorConfidentialFactory`: Fund multiple `VestingWalletCliffExecutorConfidential` in batch.
45 changes: 45 additions & 0 deletions contracts/finance/ERC7821WithExecutor.sol
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
}
}
}
11 changes: 9 additions & 2 deletions contracts/finance/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,16 @@ This directory includes primitives for on-chain confidential financial systems:

- {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.
- {VestingWalletCliffConfidential}: Variant of {VestingWalletConfidential} which adds a cliff period to the vesting schedule.
- {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).

For convenience, this directory also includes:

- {VestingWalletCliffExecutorConfidential}: A Default implementation of `VestingWalletConfidential` which implements both `VestingWalletCliffConfidential` and execution via {ERC7821WithExecutor}.
- {VestingWalletCliffExecutorConfidentialFactory}: A factory which allows creating `VestingWalletCliffExecutorConfidential` in batch.


== Contracts
{{VestingWalletConfidential}}
{{VestingWalletCliffConfidential}}
{{VestingWalletExecutorConfidential}}
{{VestingWalletCliffExecutorConfidentialFactory}}
{{VestingWalletCliffExecutorConfidential}}
{{ERC7821WithExecutor}}
34 changes: 18 additions & 16 deletions contracts/finance/VestingWalletCliffConfidential.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
pragma solidity ^0.8.27;

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

function _getVestingWalletCliffStorage() private pure returns (VestingWalletCliffStorage storage $) {
assembly {
$.slot := VestingWalletCliffStorageLocation
}
}

/// @dev The specified cliff duration is larger than the vesting duration.
error InvalidCliffDuration(uint64 cliffSeconds, uint64 durationSeconds);
error VestingWalletCliffConfidentialInvalidCliffDuration(uint64 cliffSeconds, uint64 durationSeconds);

/// @dev The timestamp at which the cliff ends.
function cliff() public view virtual returns (uint64) {
return _getVestingWalletCliffStorage()._cliff;
}

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

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

/**
Expand All @@ -56,4 +52,10 @@ abstract contract VestingWalletCliffConfidential is VestingWalletConfidential {
function _vestingSchedule(euint128 totalAllocation, uint64 timestamp) internal virtual override returns (euint128) {
return timestamp < cliff() ? euint128.wrap(0) : super._vestingSchedule(totalAllocation, timestamp);
}

function _getVestingWalletCliffStorage() private pure returns (VestingWalletCliffStorage storage $) {
assembly {
$.slot := VestingWalletCliffStorageLocation
}
}
}
202 changes: 202 additions & 0 deletions contracts/finance/VestingWalletCliffExecutorConfidentialFactory.sol
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);
}
}
Loading