From 76b9398d2a5229831c6bea995b14e7e487d06bd3 Mon Sep 17 00:00:00 2001 From: James Toussaint <33313130+james-toussaint@users.noreply.github.com> Date: Thu, 10 Jul 2025 16:55:43 +0200 Subject: [PATCH 01/28] Fund multiple `VestingWalletConfidential` in batch --- .changeset/tricky-boxes-train.md | 5 + .../VestingWalletConfidentialFactory.sol | 143 +++++++++++++++ .../VestingWalletExecutorConfidentialImpl.sol | 17 ++ .../VestingWalletConfidentialFactoryMock.sol | 7 + .../VestingWalletConfidentialFactory.test.ts | 163 ++++++++++++++++++ 5 files changed, 335 insertions(+) create mode 100644 .changeset/tricky-boxes-train.md create mode 100644 contracts/finance/VestingWalletConfidentialFactory.sol create mode 100644 contracts/finance/VestingWalletExecutorConfidentialImpl.sol create mode 100644 contracts/mocks/finance/VestingWalletConfidentialFactoryMock.sol create mode 100644 test/finance/VestingWalletConfidentialFactory.test.ts diff --git a/.changeset/tricky-boxes-train.md b/.changeset/tricky-boxes-train.md new file mode 100644 index 00000000..d434d40e --- /dev/null +++ b/.changeset/tricky-boxes-train.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-confidential-contracts': minor +--- + +Fund multiple `VestingWalletConfidential` in batch. diff --git a/contracts/finance/VestingWalletConfidentialFactory.sol b/contracts/finance/VestingWalletConfidentialFactory.sol new file mode 100644 index 00000000..289f5627 --- /dev/null +++ b/contracts/finance/VestingWalletConfidentialFactory.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {FHE, euint64, externalEuint64, ebool} from "@fhevm/solidity/lib/FHE.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {IConfidentialFungibleToken} from "../interfaces/IConfidentialFungibleToken.sol"; +import {VestingWalletExecutorConfidentialImpl} from "./VestingWalletExecutorConfidentialImpl.sol"; + +abstract contract VestingWalletConfidentialFactory { + address private immutable _vestingWalletConfidentialImplementation; + + error VestingWalletConfidentialInvalidDuration(); + error VestingWalletConfidentialInvalidStartTimestamp(address beneficiary, uint64 startTimestamp); + + /** + * @dev + */ + event VestingWalletConfidentialBatchFunded(ebool success); + /** + * @dev + */ + event VestingWalletConfidentialCreated( + address indexed beneficiary, + address indexed vestingWalletConfidential, + uint48 startTimestamp + ); + + /** + * @dev + */ + struct VestingPlan { + address executor; + address beneficiary; + externalEuint64 encryptedAmount; + uint48 startTimestamp; + } + + /** + * @dev + */ + constructor() { + _vestingWalletConfidentialImplementation = address(new VestingWalletExecutorConfidentialImpl()); + } + + /** + * @dev Batches the funding of multiple confidential vesting wallets. + * + * Funds are sent to predeterministic wallet addresses. Wallets can be created later. + */ + function batchFundVestingWalletConfidential( + address confidentialFungibleToken, + externalEuint64 totalEncryptedAmount, + bytes calldata inputProof, + VestingPlan[] calldata vestingPlans, + uint48 durationSeconds + ) external returns (ebool) { + require(durationSeconds > 0, VestingWalletConfidentialInvalidDuration()); + euint64 totalTransferedAmount = euint64.wrap(0); + uint256 vestingPlansLength = vestingPlans.length; + for (uint256 i = 0; i < vestingPlansLength; i++) { + VestingPlan memory vestingPlan = vestingPlans[i]; + euint64 encryptedAmount = FHE.fromExternal(vestingPlan.encryptedAmount, inputProof); + require( + vestingPlan.startTimestamp >= block.timestamp, + VestingWalletConfidentialInvalidStartTimestamp(vestingPlan.beneficiary, vestingPlan.startTimestamp) + ); + address vestingWalletConfidential = predictVestingWalletConfidential( + vestingPlan.executor, + vestingPlan.beneficiary, + vestingPlan.startTimestamp, + durationSeconds + ); + FHE.allow(encryptedAmount, confidentialFungibleToken); + euint64 transferredAmount = IConfidentialFungibleToken(confidentialFungibleToken).confidentialTransferFrom( + msg.sender, + vestingWalletConfidential, + encryptedAmount + ); + totalTransferedAmount = FHE.select( + FHE.eq(encryptedAmount, transferredAmount), + FHE.add(totalTransferedAmount, transferredAmount), + FHE.asEuint64(0) + ); + } + // Revert batch if one failed? + ebool success = FHE.eq(totalTransferedAmount, FHE.fromExternal(totalEncryptedAmount, inputProof)); + emit VestingWalletConfidentialBatchFunded(success); + } + + /** + * @dev Creates a confidential vesting wallet. + */ + function createVestingWalletConfidential( + address executor, + address beneficiary, + uint48 startTimestamp, + uint48 durationSeconds + ) external returns (address) { + // TODO: Check params are authorized + // Will revert if clone already created + address vestingWalletConfidentialAddress = Clones.cloneDeterministicWithImmutableArgs( + _vestingWalletConfidentialImplementation, + abi.encodePacked(executor, beneficiary, startTimestamp, durationSeconds), + _getCreate2VestingWalletConfidentialSalt(beneficiary, startTimestamp) + ); + VestingWalletExecutorConfidentialImpl(vestingWalletConfidentialAddress).initialize( + executor, + beneficiary, + startTimestamp, + durationSeconds + ); + emit VestingWalletConfidentialCreated(beneficiary, vestingWalletConfidentialAddress, startTimestamp); + return vestingWalletConfidentialAddress; + } + + /** + * @dev + */ + function predictVestingWalletConfidential( + address executor, + address beneficiary, + uint48 startTimestamp, + uint48 durationSeconds + ) public view returns (address) { + return + Clones.predictDeterministicAddressWithImmutableArgs( + _vestingWalletConfidentialImplementation, + abi.encodePacked(executor, beneficiary, startTimestamp, durationSeconds), + _getCreate2VestingWalletConfidentialSalt(beneficiary, startTimestamp), + address(this) + ); + } + + /** + * @dev Gets create2 VestingWalletConfidential salt. + */ + function _getCreate2VestingWalletConfidentialSalt( + address beneficiary, + uint48 startTimestamp + ) internal pure virtual returns (bytes32) { + return keccak256(abi.encodePacked(beneficiary, startTimestamp)); + } +} diff --git a/contracts/finance/VestingWalletExecutorConfidentialImpl.sol b/contracts/finance/VestingWalletExecutorConfidentialImpl.sol new file mode 100644 index 00000000..1ae860a2 --- /dev/null +++ b/contracts/finance/VestingWalletExecutorConfidentialImpl.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {VestingWalletExecutorConfidential} from "./VestingWalletExecutorConfidential.sol"; + +contract VestingWalletExecutorConfidentialImpl is VestingWalletExecutorConfidential { + function initialize( + address executor, + address beneficiary, + uint48 startTimestamp, + uint48 durationSeconds + ) public virtual initializer { + __VestingWalletConfidential_init(beneficiary, startTimestamp, durationSeconds); + __VestingWalletExecutorConfidential_init(executor); + } +} diff --git a/contracts/mocks/finance/VestingWalletConfidentialFactoryMock.sol b/contracts/mocks/finance/VestingWalletConfidentialFactoryMock.sol new file mode 100644 index 00000000..f8b20092 --- /dev/null +++ b/contracts/mocks/finance/VestingWalletConfidentialFactoryMock.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {SepoliaConfig} from "@fhevm/solidity/config/ZamaConfig.sol"; +import {VestingWalletConfidentialFactory} from "../../finance/VestingWalletConfidentialFactory.sol"; + +abstract contract VestingWalletConfidentialFactoryMock is VestingWalletConfidentialFactory, SepoliaConfig {} diff --git a/test/finance/VestingWalletConfidentialFactory.test.ts b/test/finance/VestingWalletConfidentialFactory.test.ts new file mode 100644 index 00000000..75148b0f --- /dev/null +++ b/test/finance/VestingWalletConfidentialFactory.test.ts @@ -0,0 +1,163 @@ +import { VestingWalletExecutorConfidentialImpl__factory } from '../../types'; +import { $VestingWalletConfidentialFactory } from '../../types/contracts-exposed/finance/VestingWalletConfidentialFactory.sol/$VestingWalletConfidentialFactory'; +import { $ConfidentialFungibleTokenMock } from '../../types/contracts-exposed/mocks/token/ConfidentialFungibleTokenMock.sol/$ConfidentialFungibleTokenMock'; +import { anyValue } from '@nomicfoundation/hardhat-chai-matchers/withArgs'; +import { time } from '@nomicfoundation/hardhat-network-helpers'; +import { days } from '@nomicfoundation/hardhat-network-helpers/dist/src/helpers/time/duration'; +import { expect } from 'chai'; +import { ethers, fhevm } from 'hardhat'; + +const name = 'ConfidentialFungibleToken'; +const symbol = 'CFT'; +const uri = 'https://example.com/metadata'; +const startTimestamp = 9876543210; +const duration = 1234; +let factory: $VestingWalletConfidentialFactory; + +describe('VestingWalletConfidentialFactory', function () { + beforeEach(async function () { + const accounts = (await ethers.getSigners()).slice(5); + const [holder, recipient, recipient2, operator, executor] = accounts; + + const token = (await ethers.deployContract('$ConfidentialFungibleTokenMock', [ + name, + symbol, + uri, + ])) as any as $ConfidentialFungibleTokenMock; + + const encryptedInput = await fhevm + .createEncryptedInput(await token.getAddress(), holder.address) + .add64(1000) + .encrypt(); + + const currentTime = await time.latest(); + const schedule = [currentTime + 60, currentTime + 60 * 121]; + factory = (await ethers.deployContract( + '$VestingWalletConfidentialFactoryMock', + [], + )) as unknown as $VestingWalletConfidentialFactory; + + await token + .connect(holder) + ['$_mint(address,bytes32,bytes)'](holder.address, encryptedInput.handles[0], encryptedInput.inputProof) + .then(tx => tx.wait()); + const until = (await time.latest()) + days(1); + await expect( + await token + .connect(holder) + .setOperator(await factory.getAddress(), until) + .then(tx => tx.wait()), + ) + .to.emit(token, 'OperatorSet') + .withArgs(holder, await factory.getAddress(), until); + + Object.assign(this, { + accounts, + holder, + recipient, + recipient2, + operator, + executor, + token, + factory, + schedule, + vestingAmount: 1000, + }); + }); + + it('should create vesting wallet with predeterministic address', async function () { + const predictedVestingWalletAddress = await factory.predictVestingWalletConfidential( + this.executor, + this.recipient, + startTimestamp, + duration, + ); + const vestingWalletAddress = await factory.createVestingWalletConfidential.staticCall( + this.executor, + this.recipient, + startTimestamp, + duration, + ); + expect(vestingWalletAddress).to.be.equal(predictedVestingWalletAddress); + }); + + it('should create vesting wallet', async function () { + const vestingWalletAddress = await factory.predictVestingWalletConfidential( + this.executor, + this.recipient, + startTimestamp, + duration, + ); + + await expect(await factory.createVestingWalletConfidential(this.executor, this.recipient, startTimestamp, duration)) + .to.emit(factory, 'VestingWalletConfidentialCreated') + .withArgs(this.recipient, vestingWalletAddress, startTimestamp); + const vestingWallet = VestingWalletExecutorConfidentialImpl__factory.connect(vestingWalletAddress, ethers.provider); + expect(await vestingWallet.owner()).to.be.equal(this.recipient); + expect(await vestingWallet.start()).to.be.equal(startTimestamp); + expect(await vestingWallet.executor()).to.be.equal(this.executor); + expect(await vestingWallet.duration()).to.be.equal(duration); + }); + + it('should not create vesting wallet twice', async function () { + await expect( + await factory.createVestingWalletConfidential(this.executor, this.recipient, startTimestamp, duration), + ).to.emit(factory, 'VestingWalletConfidentialCreated'); + await expect( + factory.createVestingWalletConfidential(this.executor, this.recipient, startTimestamp, duration), + ).to.be.revertedWithCustomError(factory, 'FailedDeployment'); + }); + + it('should batch funding of vesting wallets', async function () { + const amount1 = 101; + const amount2 = 102; + const encryptedInput = await fhevm + .createEncryptedInput(await factory.getAddress(), this.holder.address) + .add64(amount1 + amount2) + .add64(amount1) + .add64(amount2) + .encrypt(); + const vestingWalletAddress1 = await factory.predictVestingWalletConfidential( + this.executor, + this.recipient, + startTimestamp, + duration, + ); + const vestingWalletAddress2 = await factory.predictVestingWalletConfidential( + this.executor, + this.recipient2, + startTimestamp, + duration, + ); + + await expect( + await factory.connect(this.holder).batchFundVestingWalletConfidential( + await this.token.getAddress(), + encryptedInput.handles[0], + encryptedInput.inputProof, + [ + { + executor: this.executor, + beneficiary: this.recipient, + encryptedAmount: encryptedInput.handles[1], + startTimestamp: startTimestamp, + }, + { + executor: this.executor, + beneficiary: this.recipient2, + encryptedAmount: encryptedInput.handles[2], + startTimestamp: startTimestamp, + }, + ], + duration, + ), + ) + .to.emit(factory, 'VestingWalletConfidentialBatchFunded') + //TODO: Check returned value from function & event params + .to.emit(this.token, 'ConfidentialTransfer') + .withArgs(this.holder, vestingWalletAddress1, anyValue) + .to.emit(this.token, 'ConfidentialTransfer') + .withArgs(this.holder, vestingWalletAddress2, anyValue); + // TODO: Check balances + }); +}); From 5d3a2711ddb4f3c257e125368dcd88e7ea793e4c Mon Sep 17 00:00:00 2001 From: James Toussaint <33313130+james-toussaint@users.noreply.github.com> Date: Thu, 10 Jul 2025 18:05:27 +0200 Subject: [PATCH 02/28] Deploy full vesting wallets from factory --- .../VestingWalletConfidentialFactory.sol | 60 ++++++++++++++----- .../VestingWalletExecutorConfidentialImpl.sol | 17 ------ .../VestingWalletConfidentialFactory.test.ts | 40 +++++++++---- 3 files changed, 73 insertions(+), 44 deletions(-) delete mode 100644 contracts/finance/VestingWalletExecutorConfidentialImpl.sol diff --git a/contracts/finance/VestingWalletConfidentialFactory.sol b/contracts/finance/VestingWalletConfidentialFactory.sol index 289f5627..2f1b62e2 100644 --- a/contracts/finance/VestingWalletConfidentialFactory.sol +++ b/contracts/finance/VestingWalletConfidentialFactory.sol @@ -4,7 +4,9 @@ pragma solidity ^0.8.20; import {FHE, euint64, externalEuint64, ebool} from "@fhevm/solidity/lib/FHE.sol"; import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; import {IConfidentialFungibleToken} from "../interfaces/IConfidentialFungibleToken.sol"; -import {VestingWalletExecutorConfidentialImpl} from "./VestingWalletExecutorConfidentialImpl.sol"; +import {VestingWalletCliffConfidential} from "./VestingWalletCliffConfidential.sol"; +import {VestingWalletConfidential} from "./VestingWalletConfidential.sol"; +import {VestingWalletExecutorConfidential} from "./VestingWalletExecutorConfidential.sol"; abstract contract VestingWalletConfidentialFactory { address private immutable _vestingWalletConfidentialImplementation; @@ -29,17 +31,18 @@ abstract contract VestingWalletConfidentialFactory { * @dev */ struct VestingPlan { - address executor; address beneficiary; externalEuint64 encryptedAmount; uint48 startTimestamp; + uint48 cliff; + address executor; } /** * @dev */ constructor() { - _vestingWalletConfidentialImplementation = address(new VestingWalletExecutorConfidentialImpl()); + _vestingWalletConfidentialImplementation = address(new VestingWalletCliffExecutorConfidential()); } /** @@ -65,10 +68,11 @@ abstract contract VestingWalletConfidentialFactory { VestingWalletConfidentialInvalidStartTimestamp(vestingPlan.beneficiary, vestingPlan.startTimestamp) ); address vestingWalletConfidential = predictVestingWalletConfidential( - vestingPlan.executor, vestingPlan.beneficiary, vestingPlan.startTimestamp, - durationSeconds + durationSeconds, + vestingPlan.cliff, + vestingPlan.executor ); FHE.allow(encryptedAmount, confidentialFungibleToken); euint64 transferredAmount = IConfidentialFungibleToken(confidentialFungibleToken).confidentialTransferFrom( @@ -91,23 +95,25 @@ abstract contract VestingWalletConfidentialFactory { * @dev Creates a confidential vesting wallet. */ function createVestingWalletConfidential( - address executor, address beneficiary, uint48 startTimestamp, - uint48 durationSeconds + uint48 durationSeconds, + uint48 cliffSeconds, + address executor ) external returns (address) { // TODO: Check params are authorized // Will revert if clone already created address vestingWalletConfidentialAddress = Clones.cloneDeterministicWithImmutableArgs( _vestingWalletConfidentialImplementation, - abi.encodePacked(executor, beneficiary, startTimestamp, durationSeconds), + abi.encodePacked(beneficiary, startTimestamp, durationSeconds, cliffSeconds, executor), _getCreate2VestingWalletConfidentialSalt(beneficiary, startTimestamp) ); - VestingWalletExecutorConfidentialImpl(vestingWalletConfidentialAddress).initialize( - executor, + VestingWalletCliffExecutorConfidential(vestingWalletConfidentialAddress).initialize( beneficiary, startTimestamp, - durationSeconds + durationSeconds, + cliffSeconds, + executor ); emit VestingWalletConfidentialCreated(beneficiary, vestingWalletConfidentialAddress, startTimestamp); return vestingWalletConfidentialAddress; @@ -117,15 +123,16 @@ abstract contract VestingWalletConfidentialFactory { * @dev */ function predictVestingWalletConfidential( - address executor, address beneficiary, uint48 startTimestamp, - uint48 durationSeconds + uint48 durationSeconds, + uint48 cliff, + address executor ) public view returns (address) { return Clones.predictDeterministicAddressWithImmutableArgs( _vestingWalletConfidentialImplementation, - abi.encodePacked(executor, beneficiary, startTimestamp, durationSeconds), + abi.encodePacked(beneficiary, startTimestamp, durationSeconds, cliff, executor), _getCreate2VestingWalletConfidentialSalt(beneficiary, startTimestamp), address(this) ); @@ -141,3 +148,28 @@ abstract contract VestingWalletConfidentialFactory { return keccak256(abi.encodePacked(beneficiary, startTimestamp)); } } + +contract VestingWalletCliffExecutorConfidential is VestingWalletCliffConfidential, VestingWalletExecutorConfidential { + constructor() { + _disableInitializers(); + } + + function initialize( + address beneficiary, + uint48 startTimestamp, + uint48 durationSeconds, + uint48 cliffSeconds, + address executor + ) public initializer { + __VestingWalletConfidential_init(beneficiary, startTimestamp, durationSeconds); + __VestingWalletCliffConfidential_init(cliffSeconds); + __VestingWalletExecutorConfidential_init(executor); + } + + function _vestingSchedule( + euint64 totalAllocation, + uint64 timestamp + ) internal override(VestingWalletCliffConfidential, VestingWalletConfidential) returns (euint64) { + return super._vestingSchedule(totalAllocation, timestamp); + } +} diff --git a/contracts/finance/VestingWalletExecutorConfidentialImpl.sol b/contracts/finance/VestingWalletExecutorConfidentialImpl.sol deleted file mode 100644 index 1ae860a2..00000000 --- a/contracts/finance/VestingWalletExecutorConfidentialImpl.sol +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; - -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -import {VestingWalletExecutorConfidential} from "./VestingWalletExecutorConfidential.sol"; - -contract VestingWalletExecutorConfidentialImpl is VestingWalletExecutorConfidential { - function initialize( - address executor, - address beneficiary, - uint48 startTimestamp, - uint48 durationSeconds - ) public virtual initializer { - __VestingWalletConfidential_init(beneficiary, startTimestamp, durationSeconds); - __VestingWalletExecutorConfidential_init(executor); - } -} diff --git a/test/finance/VestingWalletConfidentialFactory.test.ts b/test/finance/VestingWalletConfidentialFactory.test.ts index 75148b0f..c18a4c0e 100644 --- a/test/finance/VestingWalletConfidentialFactory.test.ts +++ b/test/finance/VestingWalletConfidentialFactory.test.ts @@ -1,4 +1,4 @@ -import { VestingWalletExecutorConfidentialImpl__factory } from '../../types'; +import { VestingWalletCliffExecutorConfidential__factory } from '../../types'; import { $VestingWalletConfidentialFactory } from '../../types/contracts-exposed/finance/VestingWalletConfidentialFactory.sol/$VestingWalletConfidentialFactory'; import { $ConfidentialFungibleTokenMock } from '../../types/contracts-exposed/mocks/token/ConfidentialFungibleTokenMock.sol/$ConfidentialFungibleTokenMock'; import { anyValue } from '@nomicfoundation/hardhat-chai-matchers/withArgs'; @@ -12,6 +12,7 @@ const symbol = 'CFT'; const uri = 'https://example.com/metadata'; const startTimestamp = 9876543210; const duration = 1234; +const cliff = 10; let factory: $VestingWalletConfidentialFactory; describe('VestingWalletConfidentialFactory', function () { @@ -67,44 +68,53 @@ describe('VestingWalletConfidentialFactory', function () { it('should create vesting wallet with predeterministic address', async function () { const predictedVestingWalletAddress = await factory.predictVestingWalletConfidential( - this.executor, this.recipient, startTimestamp, duration, + cliff, + this.executor, ); const vestingWalletAddress = await factory.createVestingWalletConfidential.staticCall( - this.executor, this.recipient, startTimestamp, duration, + cliff, + this.executor, ); expect(vestingWalletAddress).to.be.equal(predictedVestingWalletAddress); }); it('should create vesting wallet', async function () { const vestingWalletAddress = await factory.predictVestingWalletConfidential( - this.executor, this.recipient, startTimestamp, duration, + cliff, + this.executor, ); - await expect(await factory.createVestingWalletConfidential(this.executor, this.recipient, startTimestamp, duration)) + await expect( + await factory.createVestingWalletConfidential(this.recipient, startTimestamp, duration, cliff, this.executor), + ) .to.emit(factory, 'VestingWalletConfidentialCreated') .withArgs(this.recipient, vestingWalletAddress, startTimestamp); - const vestingWallet = VestingWalletExecutorConfidentialImpl__factory.connect(vestingWalletAddress, ethers.provider); + const vestingWallet = VestingWalletCliffExecutorConfidential__factory.connect( + vestingWalletAddress, + ethers.provider, + ); expect(await vestingWallet.owner()).to.be.equal(this.recipient); expect(await vestingWallet.start()).to.be.equal(startTimestamp); - expect(await vestingWallet.executor()).to.be.equal(this.executor); expect(await vestingWallet.duration()).to.be.equal(duration); + expect(await vestingWallet.cliff()).to.be.equal(startTimestamp + cliff); + expect(await vestingWallet.executor()).to.be.equal(this.executor); }); it('should not create vesting wallet twice', async function () { await expect( - await factory.createVestingWalletConfidential(this.executor, this.recipient, startTimestamp, duration), + await factory.createVestingWalletConfidential(this.recipient, startTimestamp, duration, cliff, this.executor), ).to.emit(factory, 'VestingWalletConfidentialCreated'); await expect( - factory.createVestingWalletConfidential(this.executor, this.recipient, startTimestamp, duration), + factory.createVestingWalletConfidential(this.recipient, startTimestamp, duration, cliff, this.executor), ).to.be.revertedWithCustomError(factory, 'FailedDeployment'); }); @@ -118,16 +128,18 @@ describe('VestingWalletConfidentialFactory', function () { .add64(amount2) .encrypt(); const vestingWalletAddress1 = await factory.predictVestingWalletConfidential( - this.executor, this.recipient, startTimestamp, duration, + cliff, + this.executor, ); const vestingWalletAddress2 = await factory.predictVestingWalletConfidential( - this.executor, this.recipient2, startTimestamp, duration, + cliff, + this.executor, ); await expect( @@ -137,16 +149,18 @@ describe('VestingWalletConfidentialFactory', function () { encryptedInput.inputProof, [ { - executor: this.executor, beneficiary: this.recipient, encryptedAmount: encryptedInput.handles[1], startTimestamp: startTimestamp, + cliff: cliff, + executor: this.executor, }, { - executor: this.executor, beneficiary: this.recipient2, encryptedAmount: encryptedInput.handles[2], startTimestamp: startTimestamp, + cliff: cliff, + executor: this.executor, }, ], duration, From d5076078c6e21194a48fbc7ba022f13222039845 Mon Sep 17 00:00:00 2001 From: James Toussaint <33313130+james-toussaint@users.noreply.github.com> Date: Thu, 10 Jul 2025 18:08:27 +0200 Subject: [PATCH 03/28] Increase pragma in factory --- contracts/finance/VestingWalletConfidentialFactory.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/finance/VestingWalletConfidentialFactory.sol b/contracts/finance/VestingWalletConfidentialFactory.sol index 2f1b62e2..8e3fc5a4 100644 --- a/contracts/finance/VestingWalletConfidentialFactory.sol +++ b/contracts/finance/VestingWalletConfidentialFactory.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.24; import {FHE, euint64, externalEuint64, ebool} from "@fhevm/solidity/lib/FHE.sol"; import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; From 0d211a18334bf77a006d7b2be2d248246313810b Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Thu, 10 Jul 2025 16:39:05 -0600 Subject: [PATCH 04/28] fix types in `_vestingSchedule` --- contracts/finance/VestingWalletConfidentialFactory.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/finance/VestingWalletConfidentialFactory.sol b/contracts/finance/VestingWalletConfidentialFactory.sol index 8e3fc5a4..76dbf737 100644 --- a/contracts/finance/VestingWalletConfidentialFactory.sol +++ b/contracts/finance/VestingWalletConfidentialFactory.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {FHE, euint64, externalEuint64, ebool} from "@fhevm/solidity/lib/FHE.sol"; +import {FHE, euint64, externalEuint64, euint128, ebool} from "@fhevm/solidity/lib/FHE.sol"; import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; import {IConfidentialFungibleToken} from "../interfaces/IConfidentialFungibleToken.sol"; import {VestingWalletCliffConfidential} from "./VestingWalletCliffConfidential.sol"; @@ -167,9 +167,9 @@ contract VestingWalletCliffExecutorConfidential is VestingWalletCliffConfidentia } function _vestingSchedule( - euint64 totalAllocation, + euint128 totalAllocation, uint64 timestamp - ) internal override(VestingWalletCliffConfidential, VestingWalletConfidential) returns (euint64) { + ) internal override(VestingWalletCliffConfidential, VestingWalletConfidential) returns (euint128) { return super._vestingSchedule(totalAllocation, timestamp); } } From 6418c8e07fa412cd0d0c880c3536767547099202 Mon Sep 17 00:00:00 2001 From: James Toussaint <33313130+james-toussaint@users.noreply.github.com> Date: Fri, 11 Jul 2025 09:18:46 +0200 Subject: [PATCH 05/28] Update changeset Co-authored-by: Arr00 <13561405+arr00@users.noreply.github.com> --- .changeset/tricky-boxes-train.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/tricky-boxes-train.md b/.changeset/tricky-boxes-train.md index d434d40e..94068755 100644 --- a/.changeset/tricky-boxes-train.md +++ b/.changeset/tricky-boxes-train.md @@ -2,4 +2,4 @@ 'openzeppelin-confidential-contracts': minor --- -Fund multiple `VestingWalletConfidential` in batch. +`VestingWalletConfidentialFactory`: Fund multiple `VestingWalletConfidential` in batch. From c12ab220420731b57c6c3800630b6b00c983d975 Mon Sep 17 00:00:00 2001 From: James Toussaint <33313130+james-toussaint@users.noreply.github.com> Date: Fri, 11 Jul 2025 11:33:20 +0200 Subject: [PATCH 06/28] Add more context to events --- .../VestingWalletConfidentialFactory.sol | 113 ++++++++++++------ .../VestingWalletConfidentialFactory.test.ts | 39 ++++-- 2 files changed, 106 insertions(+), 46 deletions(-) diff --git a/contracts/finance/VestingWalletConfidentialFactory.sol b/contracts/finance/VestingWalletConfidentialFactory.sol index 76dbf737..a31a0a6b 100644 --- a/contracts/finance/VestingWalletConfidentialFactory.sol +++ b/contracts/finance/VestingWalletConfidentialFactory.sol @@ -9,22 +9,35 @@ import {VestingWalletConfidential} from "./VestingWalletConfidential.sol"; import {VestingWalletExecutorConfidential} from "./VestingWalletExecutorConfidential.sol"; abstract contract VestingWalletConfidentialFactory { - address private immutable _vestingWalletConfidentialImplementation; + address private immutable _vestingImplementation; error VestingWalletConfidentialInvalidDuration(); error VestingWalletConfidentialInvalidStartTimestamp(address beneficiary, uint64 startTimestamp); + event VestingWalletConfidentialFunded( + address indexed vestingWalletConfidential, + address indexed beneficiary, + address confidentialFungibleToken, + euint64 encryptedAmount, + uint48 startTimestamp, + uint48 durationSeconds, + uint48 cliffSeconds, + address executor + ); /** * @dev */ - event VestingWalletConfidentialBatchFunded(ebool success); + event VestingWalletConfidentialBatchFunded(address indexed from, euint64 totalTransferedAmount); /** * @dev */ event VestingWalletConfidentialCreated( - address indexed beneficiary, address indexed vestingWalletConfidential, - uint48 startTimestamp + address indexed beneficiary, + uint48 startTimestamp, + uint48 durationSeconds, + uint48 cliffSeconds, + address executor ); /** @@ -33,7 +46,7 @@ abstract contract VestingWalletConfidentialFactory { struct VestingPlan { address beneficiary; externalEuint64 encryptedAmount; - uint48 startTimestamp; + uint48 start; uint48 cliff; address executor; } @@ -42,39 +55,38 @@ abstract contract VestingWalletConfidentialFactory { * @dev */ constructor() { - _vestingWalletConfidentialImplementation = address(new VestingWalletCliffExecutorConfidential()); + _vestingImplementation = address(new VestingWalletCliffExecutorConfidential()); } /** * @dev Batches the funding of multiple confidential vesting wallets. * - * Funds are sent to predeterministic wallet addresses. Wallets can be created later. + * Funds are sent to predeterministic wallet addresses. Wallets can be created either + * before or after this operation. */ function batchFundVestingWalletConfidential( address confidentialFungibleToken, - externalEuint64 totalEncryptedAmount, - bytes calldata inputProof, VestingPlan[] calldata vestingPlans, - uint48 durationSeconds - ) external returns (ebool) { + uint48 durationSeconds, + bytes calldata inputProof + ) public virtual returns (euint64 totalTransferedAmount) { require(durationSeconds > 0, VestingWalletConfidentialInvalidDuration()); - euint64 totalTransferedAmount = euint64.wrap(0); - uint256 vestingPlansLength = vestingPlans.length; - for (uint256 i = 0; i < vestingPlansLength; i++) { + totalTransferedAmount = euint64.wrap(0); + for (uint256 i = 0; i < vestingPlans.length; i++) { VestingPlan memory vestingPlan = vestingPlans[i]; euint64 encryptedAmount = FHE.fromExternal(vestingPlan.encryptedAmount, inputProof); require( - vestingPlan.startTimestamp >= block.timestamp, - VestingWalletConfidentialInvalidStartTimestamp(vestingPlan.beneficiary, vestingPlan.startTimestamp) + vestingPlan.start >= block.timestamp, + VestingWalletConfidentialInvalidStartTimestamp(vestingPlan.beneficiary, vestingPlan.start) ); address vestingWalletConfidential = predictVestingWalletConfidential( vestingPlan.beneficiary, - vestingPlan.startTimestamp, + vestingPlan.start, durationSeconds, vestingPlan.cliff, vestingPlan.executor ); - FHE.allow(encryptedAmount, confidentialFungibleToken); + FHE.allowTransient(encryptedAmount, confidentialFungibleToken); euint64 transferredAmount = IConfidentialFungibleToken(confidentialFungibleToken).confidentialTransferFrom( msg.sender, vestingWalletConfidential, @@ -85,10 +97,19 @@ abstract contract VestingWalletConfidentialFactory { FHE.add(totalTransferedAmount, transferredAmount), FHE.asEuint64(0) ); + emit VestingWalletConfidentialFunded( + vestingWalletConfidential, + vestingPlan.beneficiary, + confidentialFungibleToken, + transferredAmount, + vestingPlan.start, + durationSeconds, + vestingPlan.cliff, + vestingPlan.executor + ); } - // Revert batch if one failed? - ebool success = FHE.eq(totalTransferedAmount, FHE.fromExternal(totalEncryptedAmount, inputProof)); - emit VestingWalletConfidentialBatchFunded(success); + FHE.allow(totalTransferedAmount, msg.sender); + emit VestingWalletConfidentialBatchFunded(msg.sender, totalTransferedAmount); } /** @@ -100,13 +121,17 @@ abstract contract VestingWalletConfidentialFactory { uint48 durationSeconds, uint48 cliffSeconds, address executor - ) external returns (address) { - // TODO: Check params are authorized + ) public virtual returns (address) { // Will revert if clone already created - address vestingWalletConfidentialAddress = Clones.cloneDeterministicWithImmutableArgs( - _vestingWalletConfidentialImplementation, - abi.encodePacked(beneficiary, startTimestamp, durationSeconds, cliffSeconds, executor), - _getCreate2VestingWalletConfidentialSalt(beneficiary, startTimestamp) + address vestingWalletConfidentialAddress = Clones.cloneDeterministic( + _vestingImplementation, + _getCreate2VestingWalletConfidentialSalt( + beneficiary, + startTimestamp, + durationSeconds, + cliffSeconds, + executor + ) ); VestingWalletCliffExecutorConfidential(vestingWalletConfidentialAddress).initialize( beneficiary, @@ -115,7 +140,14 @@ abstract contract VestingWalletConfidentialFactory { cliffSeconds, executor ); - emit VestingWalletConfidentialCreated(beneficiary, vestingWalletConfidentialAddress, startTimestamp); + emit VestingWalletConfidentialCreated( + beneficiary, + vestingWalletConfidentialAddress, + startTimestamp, + durationSeconds, + cliffSeconds, + executor + ); return vestingWalletConfidentialAddress; } @@ -126,15 +158,19 @@ abstract contract VestingWalletConfidentialFactory { address beneficiary, uint48 startTimestamp, uint48 durationSeconds, - uint48 cliff, + uint48 cliffSeconds, address executor - ) public view returns (address) { + ) public view virtual returns (address) { return - Clones.predictDeterministicAddressWithImmutableArgs( - _vestingWalletConfidentialImplementation, - abi.encodePacked(beneficiary, startTimestamp, durationSeconds, cliff, executor), - _getCreate2VestingWalletConfidentialSalt(beneficiary, startTimestamp), - address(this) + Clones.predictDeterministicAddress( + _vestingImplementation, + _getCreate2VestingWalletConfidentialSalt( + beneficiary, + startTimestamp, + durationSeconds, + cliffSeconds, + executor + ) ); } @@ -143,9 +179,12 @@ abstract contract VestingWalletConfidentialFactory { */ function _getCreate2VestingWalletConfidentialSalt( address beneficiary, - uint48 startTimestamp + uint48 startTimestamp, + uint48 durationSeconds, + uint48 cliffSeconds, + address executor ) internal pure virtual returns (bytes32) { - return keccak256(abi.encodePacked(beneficiary, startTimestamp)); + return keccak256(abi.encodePacked(beneficiary, startTimestamp, durationSeconds, cliffSeconds, executor)); } } diff --git a/test/finance/VestingWalletConfidentialFactory.test.ts b/test/finance/VestingWalletConfidentialFactory.test.ts index c18a4c0e..6fc57450 100644 --- a/test/finance/VestingWalletConfidentialFactory.test.ts +++ b/test/finance/VestingWalletConfidentialFactory.test.ts @@ -97,7 +97,7 @@ describe('VestingWalletConfidentialFactory', function () { await factory.createVestingWalletConfidential(this.recipient, startTimestamp, duration, cliff, this.executor), ) .to.emit(factory, 'VestingWalletConfidentialCreated') - .withArgs(this.recipient, vestingWalletAddress, startTimestamp); + .withArgs(this.recipient, vestingWalletAddress, startTimestamp, duration, cliff, this.executor); const vestingWallet = VestingWalletCliffExecutorConfidential__factory.connect( vestingWalletAddress, ethers.provider, @@ -123,7 +123,6 @@ describe('VestingWalletConfidentialFactory', function () { const amount2 = 102; const encryptedInput = await fhevm .createEncryptedInput(await factory.getAddress(), this.holder.address) - .add64(amount1 + amount2) .add64(amount1) .add64(amount2) .encrypt(); @@ -145,31 +144,53 @@ describe('VestingWalletConfidentialFactory', function () { await expect( await factory.connect(this.holder).batchFundVestingWalletConfidential( await this.token.getAddress(), - encryptedInput.handles[0], - encryptedInput.inputProof, [ { beneficiary: this.recipient, - encryptedAmount: encryptedInput.handles[1], - startTimestamp: startTimestamp, + encryptedAmount: encryptedInput.handles[0], + start: startTimestamp, cliff: cliff, executor: this.executor, }, { beneficiary: this.recipient2, - encryptedAmount: encryptedInput.handles[2], - startTimestamp: startTimestamp, + encryptedAmount: encryptedInput.handles[1], + start: startTimestamp, cliff: cliff, executor: this.executor, }, ], duration, + encryptedInput.inputProof, ), ) .to.emit(factory, 'VestingWalletConfidentialBatchFunded') + .withArgs(this.holder, anyValue) //TODO: Check returned value from function & event params + .to.emit(factory, 'VestingWalletConfidentialFunded') + .withArgs( + vestingWalletAddress1, + this.recipient, + await this.token.getAddress(), + anyValue, + startTimestamp, + duration, + cliff, + this.executor, + ) .to.emit(this.token, 'ConfidentialTransfer') - .withArgs(this.holder, vestingWalletAddress1, anyValue) + .withArgs(this.holder, vestingWalletAddress2, anyValue) + .to.emit(factory, 'VestingWalletConfidentialFunded') + .withArgs( + vestingWalletAddress2, + this.recipient2, + await this.token.getAddress(), + anyValue, + startTimestamp, + duration, + cliff, + this.executor, + ) .to.emit(this.token, 'ConfidentialTransfer') .withArgs(this.holder, vestingWalletAddress2, anyValue); // TODO: Check balances From c23abab8613820b1a092c5e3d0735f18ead811d5 Mon Sep 17 00:00:00 2001 From: James Toussaint <33313130+james-toussaint@users.noreply.github.com> Date: Fri, 11 Jul 2025 12:22:35 +0200 Subject: [PATCH 07/28] Update doc & comments --- contracts/finance/README.adoc | 9 ++++++- .../VestingWalletConfidentialFactory.sol | 27 +++++++++---------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/contracts/finance/README.adoc b/contracts/finance/README.adoc index 40050dc6..c5c30ac3 100644 --- a/contracts/finance/README.adoc +++ b/contracts/finance/README.adoc @@ -10,7 +10,14 @@ This directory includes primitives for on-chain confidential financial systems: - {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 supports both "cliff" ({VestingWalletCliffConfidential}) and "executor" ({VestingWalletExecutorConfidential}) extensions. +- {VestingWalletConfidentialFactory}: A factory which allows creating {VestingWalletCliffExecutorConfidential} in batch. + + == Contracts {{VestingWalletConfidential}} {{VestingWalletCliffConfidential}} -{{VestingWalletExecutorConfidential}} \ No newline at end of file +{{VestingWalletExecutorConfidential}} +{{VestingWalletCliffExecutorConfidential}} +{{VestingWalletConfidentialFactory}} diff --git a/contracts/finance/VestingWalletConfidentialFactory.sol b/contracts/finance/VestingWalletConfidentialFactory.sol index a31a0a6b..dc7e1867 100644 --- a/contracts/finance/VestingWalletConfidentialFactory.sol +++ b/contracts/finance/VestingWalletConfidentialFactory.sol @@ -8,6 +8,12 @@ import {VestingWalletCliffConfidential} from "./VestingWalletCliffConfidential.s import {VestingWalletConfidential} from "./VestingWalletConfidential.sol"; import {VestingWalletExecutorConfidential} from "./VestingWalletExecutorConfidential.sol"; +/** + * @dev This factory enables creating {VestingWalletCliffExecutorConfidential} in batch. + * + * All confidential vesting wallets created support both "cliff" ({VestingWalletCliffConfidential}) + * and "executor" ({VestingWalletExecutorConfidential}) extensions. + */ abstract contract VestingWalletConfidentialFactory { address private immutable _vestingImplementation; @@ -24,13 +30,7 @@ abstract contract VestingWalletConfidentialFactory { uint48 cliffSeconds, address executor ); - /** - * @dev - */ event VestingWalletConfidentialBatchFunded(address indexed from, euint64 totalTransferedAmount); - /** - * @dev - */ event VestingWalletConfidentialCreated( address indexed vestingWalletConfidential, address indexed beneficiary, @@ -40,9 +40,6 @@ abstract contract VestingWalletConfidentialFactory { address executor ); - /** - * @dev - */ struct VestingPlan { address beneficiary; externalEuint64 encryptedAmount; @@ -51,9 +48,6 @@ abstract contract VestingWalletConfidentialFactory { address executor; } - /** - * @dev - */ constructor() { _vestingImplementation = address(new VestingWalletCliffExecutorConfidential()); } @@ -63,6 +57,9 @@ abstract contract VestingWalletConfidentialFactory { * * Funds are sent to predeterministic wallet addresses. Wallets can be created either * before or after this operation. + * + * Emits a single {VestingWalletConfidentialBatchFunded} event in addition to multiple + * {VestingWalletConfidentialFunded} events related to funded vesting plans. */ function batchFundVestingWalletConfidential( address confidentialFungibleToken, @@ -114,6 +111,8 @@ abstract contract VestingWalletConfidentialFactory { /** * @dev Creates a confidential vesting wallet. + * + * Emits a {VestingWalletConfidentialCreated}. */ function createVestingWalletConfidential( address beneficiary, @@ -152,7 +151,7 @@ abstract contract VestingWalletConfidentialFactory { } /** - * @dev + * @dev Predicts deterministic address for a confidential vesting wallet. */ function predictVestingWalletConfidential( address beneficiary, @@ -175,7 +174,7 @@ abstract contract VestingWalletConfidentialFactory { } /** - * @dev Gets create2 VestingWalletConfidential salt. + * @dev Gets create2 salt for a confidential vesting wallet. */ function _getCreate2VestingWalletConfidentialSalt( address beneficiary, From 0e57d7a5680a81fa727d4677d904c209e76b86ab Mon Sep 17 00:00:00 2001 From: James Toussaint <33313130+james-toussaint@users.noreply.github.com> Date: Fri, 11 Jul 2025 12:33:23 +0200 Subject: [PATCH 08/28] Format doc --- contracts/finance/README.adoc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/finance/README.adoc b/contracts/finance/README.adoc index c5c30ac3..486ac39c 100644 --- a/contracts/finance/README.adoc +++ b/contracts/finance/README.adoc @@ -11,6 +11,7 @@ This directory includes primitives for on-chain confidential financial systems: - {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 supports both "cliff" ({VestingWalletCliffConfidential}) and "executor" ({VestingWalletExecutorConfidential}) extensions. - {VestingWalletConfidentialFactory}: A factory which allows creating {VestingWalletCliffExecutorConfidential} in batch. @@ -19,5 +20,5 @@ For convenience, this directory also includes: {{VestingWalletConfidential}} {{VestingWalletCliffConfidential}} {{VestingWalletExecutorConfidential}} -{{VestingWalletCliffExecutorConfidential}} {{VestingWalletConfidentialFactory}} +{{VestingWalletCliffExecutorConfidential}} From 064d19b71e2a0eb88bc13210805dc06fe50828ed Mon Sep 17 00:00:00 2001 From: James Toussaint <33313130+james-toussaint@users.noreply.github.com> Date: Fri, 11 Jul 2025 17:21:37 +0200 Subject: [PATCH 09/28] Check cliff in batcher --- .../VestingWalletConfidentialFactory.sol | 9 +++--- .../VestingWalletConfidentialFactory.test.ts | 28 +++++++++++++++++-- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/contracts/finance/VestingWalletConfidentialFactory.sol b/contracts/finance/VestingWalletConfidentialFactory.sol index dc7e1867..2bc6d208 100644 --- a/contracts/finance/VestingWalletConfidentialFactory.sol +++ b/contracts/finance/VestingWalletConfidentialFactory.sol @@ -17,8 +17,8 @@ import {VestingWalletExecutorConfidential} from "./VestingWalletExecutorConfiden abstract contract VestingWalletConfidentialFactory { address private immutable _vestingImplementation; - error VestingWalletConfidentialInvalidDuration(); - error VestingWalletConfidentialInvalidStartTimestamp(address beneficiary, uint64 startTimestamp); + /// @dev The specified cliff duration is larger than the vesting duration. + error InvalidCliffDuration(address beneficiary, uint64 cliffSeconds, uint64 durationSeconds); event VestingWalletConfidentialFunded( address indexed vestingWalletConfidential, @@ -67,14 +67,13 @@ abstract contract VestingWalletConfidentialFactory { uint48 durationSeconds, bytes calldata inputProof ) public virtual returns (euint64 totalTransferedAmount) { - require(durationSeconds > 0, VestingWalletConfidentialInvalidDuration()); totalTransferedAmount = euint64.wrap(0); for (uint256 i = 0; i < vestingPlans.length; i++) { VestingPlan memory vestingPlan = vestingPlans[i]; euint64 encryptedAmount = FHE.fromExternal(vestingPlan.encryptedAmount, inputProof); require( - vestingPlan.start >= block.timestamp, - VestingWalletConfidentialInvalidStartTimestamp(vestingPlan.beneficiary, vestingPlan.start) + vestingPlan.cliff <= durationSeconds, + InvalidCliffDuration(vestingPlan.beneficiary, vestingPlan.cliff, durationSeconds) ); address vestingWalletConfidential = predictVestingWalletConfidential( vestingPlan.beneficiary, diff --git a/test/finance/VestingWalletConfidentialFactory.test.ts b/test/finance/VestingWalletConfidentialFactory.test.ts index 6fc57450..57062031 100644 --- a/test/finance/VestingWalletConfidentialFactory.test.ts +++ b/test/finance/VestingWalletConfidentialFactory.test.ts @@ -13,6 +13,8 @@ const uri = 'https://example.com/metadata'; const startTimestamp = 9876543210; const duration = 1234; const cliff = 10; +const amount1 = 101; +const amount2 = 102; let factory: $VestingWalletConfidentialFactory; describe('VestingWalletConfidentialFactory', function () { @@ -119,8 +121,6 @@ describe('VestingWalletConfidentialFactory', function () { }); it('should batch funding of vesting wallets', async function () { - const amount1 = 101; - const amount2 = 102; const encryptedInput = await fhevm .createEncryptedInput(await factory.getAddress(), this.holder.address) .add64(amount1) @@ -195,4 +195,28 @@ describe('VestingWalletConfidentialFactory', function () { .withArgs(this.holder, vestingWalletAddress2, anyValue); // TODO: Check balances }); + + it('should batch with invalid cliff', async function () { + const encryptedInput = await fhevm + .createEncryptedInput(await factory.getAddress(), this.holder.address) + .add64(amount1) + .encrypt(); + + await expect( + factory.connect(this.holder).batchFundVestingWalletConfidential( + await this.token.getAddress(), + [ + { + beneficiary: this.recipient, + encryptedAmount: encryptedInput.handles[0], + start: startTimestamp, + cliff: duration + 1, + executor: this.executor, + }, + ], + duration, + encryptedInput.inputProof, + ), + ).to.be.revertedWithCustomError(factory, 'InvalidCliffDuration'); + }); }); From b6c41348458076580b22ac119bb52eee59aa6191 Mon Sep 17 00:00:00 2001 From: James Toussaint <33313130+james-toussaint@users.noreply.github.com> Date: Fri, 11 Jul 2025 17:25:10 +0200 Subject: [PATCH 10/28] Remove total transfered amount computation in batcher --- .../finance/VestingWalletConfidentialFactory.sol | 14 ++++---------- .../VestingWalletConfidentialFactory.test.ts | 2 +- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/contracts/finance/VestingWalletConfidentialFactory.sol b/contracts/finance/VestingWalletConfidentialFactory.sol index 2bc6d208..99ab3dcd 100644 --- a/contracts/finance/VestingWalletConfidentialFactory.sol +++ b/contracts/finance/VestingWalletConfidentialFactory.sol @@ -30,7 +30,7 @@ abstract contract VestingWalletConfidentialFactory { uint48 cliffSeconds, address executor ); - event VestingWalletConfidentialBatchFunded(address indexed from, euint64 totalTransferedAmount); + event VestingWalletConfidentialBatchFunded(address indexed from); event VestingWalletConfidentialCreated( address indexed vestingWalletConfidential, address indexed beneficiary, @@ -66,8 +66,7 @@ abstract contract VestingWalletConfidentialFactory { VestingPlan[] calldata vestingPlans, uint48 durationSeconds, bytes calldata inputProof - ) public virtual returns (euint64 totalTransferedAmount) { - totalTransferedAmount = euint64.wrap(0); + ) public virtual returns (bool) { for (uint256 i = 0; i < vestingPlans.length; i++) { VestingPlan memory vestingPlan = vestingPlans[i]; euint64 encryptedAmount = FHE.fromExternal(vestingPlan.encryptedAmount, inputProof); @@ -88,11 +87,6 @@ abstract contract VestingWalletConfidentialFactory { vestingWalletConfidential, encryptedAmount ); - totalTransferedAmount = FHE.select( - FHE.eq(encryptedAmount, transferredAmount), - FHE.add(totalTransferedAmount, transferredAmount), - FHE.asEuint64(0) - ); emit VestingWalletConfidentialFunded( vestingWalletConfidential, vestingPlan.beneficiary, @@ -104,8 +98,8 @@ abstract contract VestingWalletConfidentialFactory { vestingPlan.executor ); } - FHE.allow(totalTransferedAmount, msg.sender); - emit VestingWalletConfidentialBatchFunded(msg.sender, totalTransferedAmount); + emit VestingWalletConfidentialBatchFunded(msg.sender); + return true; } /** diff --git a/test/finance/VestingWalletConfidentialFactory.test.ts b/test/finance/VestingWalletConfidentialFactory.test.ts index 57062031..141ef680 100644 --- a/test/finance/VestingWalletConfidentialFactory.test.ts +++ b/test/finance/VestingWalletConfidentialFactory.test.ts @@ -165,7 +165,7 @@ describe('VestingWalletConfidentialFactory', function () { ), ) .to.emit(factory, 'VestingWalletConfidentialBatchFunded') - .withArgs(this.holder, anyValue) + .withArgs(this.holder) //TODO: Check returned value from function & event params .to.emit(factory, 'VestingWalletConfidentialFunded') .withArgs( From ee6a464082714a83eb42fcc8c474e82be18b7a44 Mon Sep 17 00:00:00 2001 From: James Toussaint <33313130+james-toussaint@users.noreply.github.com> Date: Fri, 11 Jul 2025 17:31:51 +0200 Subject: [PATCH 11/28] Set factory as non abstract --- contracts/finance/VestingWalletConfidentialFactory.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/finance/VestingWalletConfidentialFactory.sol b/contracts/finance/VestingWalletConfidentialFactory.sol index 99ab3dcd..458895b6 100644 --- a/contracts/finance/VestingWalletConfidentialFactory.sol +++ b/contracts/finance/VestingWalletConfidentialFactory.sol @@ -14,7 +14,7 @@ import {VestingWalletExecutorConfidential} from "./VestingWalletExecutorConfiden * All confidential vesting wallets created support both "cliff" ({VestingWalletCliffConfidential}) * and "executor" ({VestingWalletExecutorConfidential}) extensions. */ -abstract contract VestingWalletConfidentialFactory { +contract VestingWalletConfidentialFactory { address private immutable _vestingImplementation; /// @dev The specified cliff duration is larger than the vesting duration. From 966aef6c4d0ab8f80310ea56f1af453ce0cfffd0 Mon Sep 17 00:00:00 2001 From: James Toussaint <33313130+james-toussaint@users.noreply.github.com> Date: Fri, 11 Jul 2025 18:29:44 +0200 Subject: [PATCH 12/28] Lighten vesting struct & check beneficiary from batcher --- .../VestingWalletConfidentialFactory.sol | 40 ++++++++++--------- .../VestingWalletConfidentialFactory.test.ts | 39 +++++++++++++----- 2 files changed, 50 insertions(+), 29 deletions(-) diff --git a/contracts/finance/VestingWalletConfidentialFactory.sol b/contracts/finance/VestingWalletConfidentialFactory.sol index 458895b6..aeeeeec3 100644 --- a/contracts/finance/VestingWalletConfidentialFactory.sol +++ b/contracts/finance/VestingWalletConfidentialFactory.sol @@ -18,7 +18,8 @@ contract VestingWalletConfidentialFactory { address private immutable _vestingImplementation; /// @dev The specified cliff duration is larger than the vesting duration. - error InvalidCliffDuration(address beneficiary, uint64 cliffSeconds, uint64 durationSeconds); + error InvalidCliffDuration(uint64 cliffSeconds, uint64 durationSeconds); + error InvalidVestingBeneficiary(uint256 i); event VestingWalletConfidentialFunded( address indexed vestingWalletConfidential, @@ -44,8 +45,6 @@ contract VestingWalletConfidentialFactory { address beneficiary; externalEuint64 encryptedAmount; uint48 start; - uint48 cliff; - address executor; } constructor() { @@ -65,28 +64,32 @@ contract VestingWalletConfidentialFactory { address confidentialFungibleToken, VestingPlan[] calldata vestingPlans, uint48 durationSeconds, + uint48 cliffSeconds, + address executor, bytes calldata inputProof ) public virtual returns (bool) { + require(cliffSeconds <= durationSeconds, InvalidCliffDuration(cliffSeconds, durationSeconds)); for (uint256 i = 0; i < vestingPlans.length; i++) { VestingPlan memory vestingPlan = vestingPlans[i]; - euint64 encryptedAmount = FHE.fromExternal(vestingPlan.encryptedAmount, inputProof); - require( - vestingPlan.cliff <= durationSeconds, - InvalidCliffDuration(vestingPlan.beneficiary, vestingPlan.cliff, durationSeconds) - ); + require(vestingPlan.beneficiary != address(0), InvalidVestingBeneficiary(i)); address vestingWalletConfidential = predictVestingWalletConfidential( vestingPlan.beneficiary, vestingPlan.start, durationSeconds, - vestingPlan.cliff, - vestingPlan.executor - ); - FHE.allowTransient(encryptedAmount, confidentialFungibleToken); - euint64 transferredAmount = IConfidentialFungibleToken(confidentialFungibleToken).confidentialTransferFrom( - msg.sender, - vestingWalletConfidential, - encryptedAmount + 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, + vestingWalletConfidential, + encryptedAmount + ); + } emit VestingWalletConfidentialFunded( vestingWalletConfidential, vestingPlan.beneficiary, @@ -94,11 +97,10 @@ contract VestingWalletConfidentialFactory { transferredAmount, vestingPlan.start, durationSeconds, - vestingPlan.cliff, - vestingPlan.executor + cliffSeconds, + executor ); } - emit VestingWalletConfidentialBatchFunded(msg.sender); return true; } diff --git a/test/finance/VestingWalletConfidentialFactory.test.ts b/test/finance/VestingWalletConfidentialFactory.test.ts index 141ef680..845af198 100644 --- a/test/finance/VestingWalletConfidentialFactory.test.ts +++ b/test/finance/VestingWalletConfidentialFactory.test.ts @@ -149,24 +149,19 @@ describe('VestingWalletConfidentialFactory', function () { beneficiary: this.recipient, encryptedAmount: encryptedInput.handles[0], start: startTimestamp, - cliff: cliff, - executor: this.executor, }, { beneficiary: this.recipient2, encryptedAmount: encryptedInput.handles[1], start: startTimestamp, - cliff: cliff, - executor: this.executor, }, ], duration, + cliff, + this.executor, encryptedInput.inputProof, ), ) - .to.emit(factory, 'VestingWalletConfidentialBatchFunded') - .withArgs(this.holder) - //TODO: Check returned value from function & event params .to.emit(factory, 'VestingWalletConfidentialFunded') .withArgs( vestingWalletAddress1, @@ -196,7 +191,7 @@ describe('VestingWalletConfidentialFactory', function () { // TODO: Check balances }); - it('should batch with invalid cliff', async function () { + it('should not batch with invalid cliff', async function () { const encryptedInput = await fhevm .createEncryptedInput(await factory.getAddress(), this.holder.address) .add64(amount1) @@ -210,13 +205,37 @@ describe('VestingWalletConfidentialFactory', function () { beneficiary: this.recipient, encryptedAmount: encryptedInput.handles[0], start: startTimestamp, - cliff: duration + 1, - executor: this.executor, }, ], duration, + duration + 1, // cliff + this.executor, encryptedInput.inputProof, ), ).to.be.revertedWithCustomError(factory, 'InvalidCliffDuration'); }); + + it('should not batch with invalid beneficiary', async function () { + const encryptedInput = await fhevm + .createEncryptedInput(await factory.getAddress(), this.holder.address) + .add64(amount1) + .encrypt(); + + await expect( + factory.connect(this.holder).batchFundVestingWalletConfidential( + await this.token.getAddress(), + [ + { + beneficiary: ethers.ZeroAddress, + encryptedAmount: encryptedInput.handles[0], + start: startTimestamp, + }, + ], + duration, + cliff, + this.executor, + encryptedInput.inputProof, + ), + ).to.be.revertedWithCustomError(factory, 'InvalidVestingBeneficiary'); + }); }); From 5b4e8e1ed0e3f53680753ceb4b28ba3af7766953 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Fri, 11 Jul 2025 11:33:45 -0600 Subject: [PATCH 13/28] up --- .../VestingWalletConfidentialFactory.sol | 14 +- .../VestingWalletConfidentialFactory.test.ts | 148 +++++++++--------- 2 files changed, 84 insertions(+), 78 deletions(-) diff --git a/contracts/finance/VestingWalletConfidentialFactory.sol b/contracts/finance/VestingWalletConfidentialFactory.sol index aeeeeec3..28a51611 100644 --- a/contracts/finance/VestingWalletConfidentialFactory.sol +++ b/contracts/finance/VestingWalletConfidentialFactory.sol @@ -17,10 +17,6 @@ import {VestingWalletExecutorConfidential} from "./VestingWalletExecutorConfiden contract VestingWalletConfidentialFactory { address private immutable _vestingImplementation; - /// @dev The specified cliff duration is larger than the vesting duration. - error InvalidCliffDuration(uint64 cliffSeconds, uint64 durationSeconds); - error InvalidVestingBeneficiary(uint256 i); - event VestingWalletConfidentialFunded( address indexed vestingWalletConfidential, address indexed beneficiary, @@ -31,7 +27,6 @@ contract VestingWalletConfidentialFactory { uint48 cliffSeconds, address executor ); - event VestingWalletConfidentialBatchFunded(address indexed from); event VestingWalletConfidentialCreated( address indexed vestingWalletConfidential, address indexed beneficiary, @@ -41,6 +36,10 @@ contract VestingWalletConfidentialFactory { address executor ); + /// @dev The specified cliff duration is larger than the vesting duration. + error InvalidCliffDuration(uint64 cliffSeconds, uint64 durationSeconds); + error InvalidVestingBeneficiary(address account); + struct VestingPlan { address beneficiary; externalEuint64 encryptedAmount; @@ -67,11 +66,11 @@ contract VestingWalletConfidentialFactory { uint48 cliffSeconds, address executor, bytes calldata inputProof - ) public virtual returns (bool) { + ) public virtual { require(cliffSeconds <= durationSeconds, InvalidCliffDuration(cliffSeconds, durationSeconds)); for (uint256 i = 0; i < vestingPlans.length; i++) { VestingPlan memory vestingPlan = vestingPlans[i]; - require(vestingPlan.beneficiary != address(0), InvalidVestingBeneficiary(i)); + require(vestingPlan.beneficiary != address(0), InvalidVestingBeneficiary(address(0))); address vestingWalletConfidential = predictVestingWalletConfidential( vestingPlan.beneficiary, vestingPlan.start, @@ -101,7 +100,6 @@ contract VestingWalletConfidentialFactory { executor ); } - return true; } /** diff --git a/test/finance/VestingWalletConfidentialFactory.test.ts b/test/finance/VestingWalletConfidentialFactory.test.ts index 845af198..a50b3117 100644 --- a/test/finance/VestingWalletConfidentialFactory.test.ts +++ b/test/finance/VestingWalletConfidentialFactory.test.ts @@ -1,6 +1,6 @@ -import { VestingWalletCliffExecutorConfidential__factory } from '../../types'; import { $VestingWalletConfidentialFactory } from '../../types/contracts-exposed/finance/VestingWalletConfidentialFactory.sol/$VestingWalletConfidentialFactory'; import { $ConfidentialFungibleTokenMock } from '../../types/contracts-exposed/mocks/token/ConfidentialFungibleTokenMock.sol/$ConfidentialFungibleTokenMock'; +import { FhevmType } from '@fhevm/hardhat-plugin'; import { anyValue } from '@nomicfoundation/hardhat-chai-matchers/withArgs'; import { time } from '@nomicfoundation/hardhat-network-helpers'; import { days } from '@nomicfoundation/hardhat-network-helpers/dist/src/helpers/time/duration'; @@ -15,12 +15,10 @@ const duration = 1234; const cliff = 10; const amount1 = 101; const amount2 = 102; -let factory: $VestingWalletConfidentialFactory; describe('VestingWalletConfidentialFactory', function () { beforeEach(async function () { - const accounts = (await ethers.getSigners()).slice(5); - const [holder, recipient, recipient2, operator, executor] = accounts; + const [holder, recipient, recipient2, operator, executor, ...accounts] = await ethers.getSigners(); const token = (await ethers.deployContract('$ConfidentialFungibleTokenMock', [ name, @@ -33,24 +31,15 @@ describe('VestingWalletConfidentialFactory', function () { .add64(1000) .encrypt(); - const currentTime = await time.latest(); - const schedule = [currentTime + 60, currentTime + 60 * 121]; - factory = (await ethers.deployContract( + const factory = (await ethers.deployContract( '$VestingWalletConfidentialFactoryMock', - [], )) as unknown as $VestingWalletConfidentialFactory; await token .connect(holder) - ['$_mint(address,bytes32,bytes)'](holder.address, encryptedInput.handles[0], encryptedInput.inputProof) - .then(tx => tx.wait()); + ['$_mint(address,bytes32,bytes)'](holder.address, encryptedInput.handles[0], encryptedInput.inputProof); const until = (await time.latest()) + days(1); - await expect( - await token - .connect(holder) - .setOperator(await factory.getAddress(), until) - .then(tx => tx.wait()), - ) + await expect(await token.connect(holder).setOperator(await factory.getAddress(), until)) .to.emit(token, 'OperatorSet') .withArgs(holder, await factory.getAddress(), until); @@ -63,20 +52,18 @@ describe('VestingWalletConfidentialFactory', function () { executor, token, factory, - schedule, - vestingAmount: 1000, }); }); - it('should create vesting wallet with predeterministic address', async function () { - const predictedVestingWalletAddress = await factory.predictVestingWalletConfidential( + it('should create vesting wallet with deterministic address', async function () { + const predictedVestingWalletAddress = await this.factory.predictVestingWalletConfidential( this.recipient, startTimestamp, duration, cliff, this.executor, ); - const vestingWalletAddress = await factory.createVestingWalletConfidential.staticCall( + const vestingWalletAddress = await this.factory.createVestingWalletConfidential.staticCall( this.recipient, startTimestamp, duration, @@ -87,7 +74,7 @@ describe('VestingWalletConfidentialFactory', function () { }); it('should create vesting wallet', async function () { - const vestingWalletAddress = await factory.predictVestingWalletConfidential( + const vestingWalletAddress = await this.factory.predictVestingWalletConfidential( this.recipient, startTimestamp, duration, @@ -96,14 +83,17 @@ describe('VestingWalletConfidentialFactory', function () { ); await expect( - await factory.createVestingWalletConfidential(this.recipient, startTimestamp, duration, cliff, this.executor), + await this.factory.createVestingWalletConfidential( + this.recipient, + startTimestamp, + duration, + cliff, + this.executor, + ), ) - .to.emit(factory, 'VestingWalletConfidentialCreated') + .to.emit(this.factory, 'VestingWalletConfidentialCreated') .withArgs(this.recipient, vestingWalletAddress, startTimestamp, duration, cliff, this.executor); - const vestingWallet = VestingWalletCliffExecutorConfidential__factory.connect( - vestingWalletAddress, - ethers.provider, - ); + const vestingWallet = await ethers.getContractAt('VestingWalletCliffExecutorConfidential', vestingWalletAddress); expect(await vestingWallet.owner()).to.be.equal(this.recipient); expect(await vestingWallet.start()).to.be.equal(startTimestamp); expect(await vestingWallet.duration()).to.be.equal(duration); @@ -113,27 +103,33 @@ describe('VestingWalletConfidentialFactory', function () { it('should not create vesting wallet twice', async function () { await expect( - await factory.createVestingWalletConfidential(this.recipient, startTimestamp, duration, cliff, this.executor), - ).to.emit(factory, 'VestingWalletConfidentialCreated'); + await this.factory.createVestingWalletConfidential( + this.recipient, + startTimestamp, + duration, + cliff, + this.executor, + ), + ).to.emit(this.factory, 'VestingWalletConfidentialCreated'); await expect( - factory.createVestingWalletConfidential(this.recipient, startTimestamp, duration, cliff, this.executor), - ).to.be.revertedWithCustomError(factory, 'FailedDeployment'); + this.factory.createVestingWalletConfidential(this.recipient, startTimestamp, duration, cliff, this.executor), + ).to.be.revertedWithCustomError(this.factory, 'FailedDeployment'); }); - it('should batch funding of vesting wallets', async function () { + it('should batch fund vesting wallets', async function () { const encryptedInput = await fhevm - .createEncryptedInput(await factory.getAddress(), this.holder.address) + .createEncryptedInput(await this.factory.getAddress(), this.holder.address) .add64(amount1) .add64(amount2) .encrypt(); - const vestingWalletAddress1 = await factory.predictVestingWalletConfidential( + const vestingWalletAddress1 = await this.factory.predictVestingWalletConfidential( this.recipient, startTimestamp, duration, cliff, this.executor, ); - const vestingWalletAddress2 = await factory.predictVestingWalletConfidential( + const vestingWalletAddress2 = await this.factory.predictVestingWalletConfidential( this.recipient2, startTimestamp, duration, @@ -141,32 +137,32 @@ describe('VestingWalletConfidentialFactory', function () { this.executor, ); - await expect( - await factory.connect(this.holder).batchFundVestingWalletConfidential( - await this.token.getAddress(), - [ - { - beneficiary: this.recipient, - encryptedAmount: encryptedInput.handles[0], - start: startTimestamp, - }, - { - beneficiary: this.recipient2, - encryptedAmount: encryptedInput.handles[1], - start: startTimestamp, - }, - ], - duration, - cliff, - this.executor, - encryptedInput.inputProof, - ), - ) - .to.emit(factory, 'VestingWalletConfidentialFunded') + const vestingCreationTx = await this.factory.connect(this.holder).batchFundVestingWalletConfidential( + this.token.target, + [ + { + beneficiary: this.recipient, + encryptedAmount: encryptedInput.handles[0], + start: startTimestamp, + }, + { + beneficiary: this.recipient2, + encryptedAmount: encryptedInput.handles[1], + start: startTimestamp, + }, + ], + duration, + cliff, + this.executor, + encryptedInput.inputProof, + ); + + expect(vestingCreationTx) + .to.emit(this.factory, 'VestingWalletConfidentialFunded') .withArgs( vestingWalletAddress1, this.recipient, - await this.token.getAddress(), + this.token.target, anyValue, startTimestamp, duration, @@ -175,11 +171,11 @@ describe('VestingWalletConfidentialFactory', function () { ) .to.emit(this.token, 'ConfidentialTransfer') .withArgs(this.holder, vestingWalletAddress2, anyValue) - .to.emit(factory, 'VestingWalletConfidentialFunded') + .to.emit(this.factory, 'VestingWalletConfidentialFunded') .withArgs( vestingWalletAddress2, this.recipient2, - await this.token.getAddress(), + this.token.target, anyValue, startTimestamp, duration, @@ -188,18 +184,30 @@ describe('VestingWalletConfidentialFactory', function () { ) .to.emit(this.token, 'ConfidentialTransfer') .withArgs(this.holder, vestingWalletAddress2, anyValue); - // TODO: Check balances + + const transferEvents = await vestingCreationTx + .wait() + .then(tx => tx!.logs.filter(log => log.address === this.token.target)); + + const vestingWallet1TransferAmount = transferEvents[0].topics[3]; + const vestingWallet2TransferAmount = transferEvents[1].topics[3]; + expect( + await fhevm.userDecryptEuint(FhevmType.euint64, vestingWallet1TransferAmount, this.token.target, this.holder), + ).to.equal(amount1); + expect( + await fhevm.userDecryptEuint(FhevmType.euint64, vestingWallet2TransferAmount, this.token.target, this.holder), + ).to.equal(amount2); }); it('should not batch with invalid cliff', async function () { const encryptedInput = await fhevm - .createEncryptedInput(await factory.getAddress(), this.holder.address) + .createEncryptedInput(await this.factory.getAddress(), this.holder.address) .add64(amount1) .encrypt(); await expect( - factory.connect(this.holder).batchFundVestingWalletConfidential( - await this.token.getAddress(), + this.factory.connect(this.holder).batchFundVestingWalletConfidential( + this.token.target, [ { beneficiary: this.recipient, @@ -212,18 +220,18 @@ describe('VestingWalletConfidentialFactory', function () { this.executor, encryptedInput.inputProof, ), - ).to.be.revertedWithCustomError(factory, 'InvalidCliffDuration'); + ).to.be.revertedWithCustomError(this.factory, 'InvalidCliffDuration'); }); it('should not batch with invalid beneficiary', async function () { const encryptedInput = await fhevm - .createEncryptedInput(await factory.getAddress(), this.holder.address) + .createEncryptedInput(this.factory.target, this.holder.address) .add64(amount1) .encrypt(); await expect( - factory.connect(this.holder).batchFundVestingWalletConfidential( - await this.token.getAddress(), + this.factory.connect(this.holder).batchFundVestingWalletConfidential( + this.token.target, [ { beneficiary: ethers.ZeroAddress, @@ -236,6 +244,6 @@ describe('VestingWalletConfidentialFactory', function () { this.executor, encryptedInput.inputProof, ), - ).to.be.revertedWithCustomError(factory, 'InvalidVestingBeneficiary'); + ).to.be.revertedWithCustomError(this.factory, 'InvalidVestingBeneficiary'); }); }); From 70d754f2b7099876046cc40324eeb4e33865df3a Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Fri, 11 Jul 2025 11:47:16 -0600 Subject: [PATCH 14/28] `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 --- contracts/finance/ERC7821WithExecutor.sol | 45 +++ .../VestingWalletConfidentialFactory.sol | 16 +- .../VestingWalletExecutorConfidential.sol | 60 ---- .../VestingWalletExecutorConfidentialMock.sol | 7 - lib/openzeppelin-contracts | 2 +- package-lock.json | 289 +++++++++++++++++- test/finance/ERC7821WithExecutor.test.ts | 60 ++++ .../VestingWalletExecutorConfidential.test.ts | 68 ----- 8 files changed, 391 insertions(+), 156 deletions(-) create mode 100644 contracts/finance/ERC7821WithExecutor.sol delete mode 100644 contracts/finance/VestingWalletExecutorConfidential.sol delete mode 100644 contracts/mocks/finance/VestingWalletExecutorConfidentialMock.sol create mode 100644 test/finance/ERC7821WithExecutor.test.ts delete mode 100644 test/finance/VestingWalletExecutorConfidential.test.ts diff --git a/contracts/finance/ERC7821WithExecutor.sol b/contracts/finance/ERC7821WithExecutor.sol new file mode 100644 index 00000000..a9918164 --- /dev/null +++ b/contracts/finance/ERC7821WithExecutor.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {ERC7821} from "@openzeppelin/contracts/account/extensions/draft-ERC7821.sol"; + +/** + * @dev 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). + */ +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; + + function _getERC7821WithExecutorStorage() private pure returns (ERC7821WithExecutorStorage storage $) { + assembly { + $.slot := ERC7821WithExecutorStorageLocation + } + } + + // solhint-disable-next-line func-name-mixedcase + function __ERC7821WithExecutor_init(address executor_) internal onlyInitializing { + _getERC7821WithExecutorStorage()._executor = executor_; + } + + /// @dev Trusted address that is able to execute arbitrary calls from the vesting wallet via `ERC7821`. + function executor() public view virtual returns (address) { + return _getERC7821WithExecutorStorage()._executor; + } + + function _erc7821AuthorizedExecutor( + address caller, + bytes32 mode, + bytes calldata executionData + ) internal view virtual override returns (bool) { + return caller == executor() || super._erc7821AuthorizedExecutor(caller, mode, executionData); + } +} diff --git a/contracts/finance/VestingWalletConfidentialFactory.sol b/contracts/finance/VestingWalletConfidentialFactory.sol index 28a51611..530ab02e 100644 --- a/contracts/finance/VestingWalletConfidentialFactory.sol +++ b/contracts/finance/VestingWalletConfidentialFactory.sol @@ -3,10 +3,10 @@ pragma solidity ^0.8.24; import {FHE, euint64, externalEuint64, euint128, ebool} from "@fhevm/solidity/lib/FHE.sol"; import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; -import {IConfidentialFungibleToken} from "../interfaces/IConfidentialFungibleToken.sol"; +import {IConfidentialFungibleToken} from "./../interfaces/IConfidentialFungibleToken.sol"; +import {ERC7821WithExecutor} from "./ERC7821WithExecutor.sol"; import {VestingWalletCliffConfidential} from "./VestingWalletCliffConfidential.sol"; import {VestingWalletConfidential} from "./VestingWalletConfidential.sol"; -import {VestingWalletExecutorConfidential} from "./VestingWalletExecutorConfidential.sol"; /** * @dev This factory enables creating {VestingWalletCliffExecutorConfidential} in batch. @@ -180,7 +180,8 @@ contract VestingWalletConfidentialFactory { } } -contract VestingWalletCliffExecutorConfidential is VestingWalletCliffConfidential, VestingWalletExecutorConfidential { +// slither-disable-next-line locked-ether +contract VestingWalletCliffExecutorConfidential is VestingWalletCliffConfidential, ERC7821WithExecutor { constructor() { _disableInitializers(); } @@ -194,13 +195,6 @@ contract VestingWalletCliffExecutorConfidential is VestingWalletCliffConfidentia ) public initializer { __VestingWalletConfidential_init(beneficiary, startTimestamp, durationSeconds); __VestingWalletCliffConfidential_init(cliffSeconds); - __VestingWalletExecutorConfidential_init(executor); - } - - function _vestingSchedule( - euint128 totalAllocation, - uint64 timestamp - ) internal override(VestingWalletCliffConfidential, VestingWalletConfidential) returns (euint128) { - return super._vestingSchedule(totalAllocation, timestamp); + __ERC7821WithExecutor_init(executor); } } diff --git a/contracts/finance/VestingWalletExecutorConfidential.sol b/contracts/finance/VestingWalletExecutorConfidential.sol deleted file mode 100644 index 742a8197..00000000 --- a/contracts/finance/VestingWalletExecutorConfidential.sol +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; - -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -import {VestingWalletConfidential} from "./VestingWalletConfidential.sol"; - -/** - * @dev 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). - */ -abstract contract VestingWalletExecutorConfidential is VestingWalletConfidential { - /// @custom:storage-location erc7201:openzeppelin.storage.VestingWalletExecutorConfidential - struct VestingWalletExecutorStorage { - address _executor; - } - - // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.VestingWalletExecutorConfidential")) - 1)) & ~bytes32(uint256(0xff)) - // solhint-disable-next-line const-name-snakecase - bytes32 private constant VestingWalletExecutorStorageLocation = - 0x165c39f99e134d4ac22afe0db4de9fbb73791548e71f117f46b120e313690700; - - function _getVestingWalletExecutorStorage() private pure returns (VestingWalletExecutorStorage storage $) { - assembly { - $.slot := VestingWalletExecutorStorageLocation - } - } - - event VestingWalletExecutorConfidentialCallExecuted(address indexed target, uint256 value, bytes data); - - /// @dev Thrown when a non-executor attempts to call {call}. - error VestingWalletExecutorConfidentialOnlyExecutor(); - - // solhint-disable-next-line func-name-mixedcase - function __VestingWalletExecutorConfidential_init(address executor_) internal onlyInitializing { - _getVestingWalletExecutorStorage()._executor = executor_; - } - - /// @dev Trusted address that is able to execute arbitrary calls from the vesting wallet via {call}. - function executor() public view virtual returns (address) { - return _getVestingWalletExecutorStorage()._executor; - } - - /** - * @dev Execute an arbitrary call from the vesting wallet. Only callable by the {executor}. - * - * Emits a {VestingWalletExecutorConfidentialCallExecuted} event. - */ - function call(address target, uint256 value, bytes memory data) public virtual { - require(msg.sender == executor(), VestingWalletExecutorConfidentialOnlyExecutor()); - _call(target, value, data); - } - - /// @dev Internal function for executing an arbitrary call from the vesting wallet. - function _call(address target, uint256 value, bytes memory data) internal virtual { - (bool success, bytes memory res) = target.call{value: value}(data); - Address.verifyCallResult(success, res); - - emit VestingWalletExecutorConfidentialCallExecuted(target, value, data); - } -} diff --git a/contracts/mocks/finance/VestingWalletExecutorConfidentialMock.sol b/contracts/mocks/finance/VestingWalletExecutorConfidentialMock.sol deleted file mode 100644 index 22474af9..00000000 --- a/contracts/mocks/finance/VestingWalletExecutorConfidentialMock.sol +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; - -import {SepoliaConfig} from "@fhevm/solidity/config/ZamaConfig.sol"; -import {VestingWalletExecutorConfidential} from "../../finance/VestingWalletExecutorConfidential.sol"; - -abstract contract VestingWalletExecutorConfidentialMock is VestingWalletExecutorConfidential, SepoliaConfig {} diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts index e4f70216..f12605ad 160000 --- a/lib/openzeppelin-contracts +++ b/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit e4f70216d759d8e6a64144a9e1f7bbeed78e7079 +Subproject commit f12605ad4d74abd05ae238f5bbe6afee19fead32 diff --git a/package-lock.json b/package-lock.json index cc17ee1b..141de26a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -76,7 +76,7 @@ }, "lib/openzeppelin-contracts": { "name": "openzeppelin-solidity", - "version": "5.3.0", + "version": "5.4.0-rc.1", "dev": true, "license": "MIT", "devDependencies": { @@ -94,17 +94,17 @@ "@openzeppelin/upgrades-core": "^1.20.6", "chai": "^4.2.0", "eslint": "^9.0.0", - "eslint-config-prettier": "^9.0.0", - "ethers": "^6.13.4", + "eslint-config-prettier": "^10.0.0", + "ethers": "^6.14.0", "glob": "^11.0.0", - "globals": "^15.3.0", + "globals": "^16.0.0", "graphlib": "^2.1.8", - "hardhat": "^2.22.7", + "hardhat": "^2.24.0", "hardhat-exposed": "^0.3.15", "hardhat-gas-reporter": "^2.1.0", "hardhat-ignore-warnings": "^0.2.11", "husky": "^9.1.7", - "lint-staged": "^15.2.10", + "lint-staged": "^16.0.0", "lodash.startcase": "^4.4.0", "micromatch": "^4.0.2", "p-limit": "^6.0.0", @@ -115,9 +115,9 @@ "solhint": "^5.0.0", "solhint-plugin-openzeppelin": "file:scripts/solhint-custom", "solidity-ast": "^0.4.50", - "solidity-coverage": "^0.8.5", + "solidity-coverage": "^0.8.14", "solidity-docgen": "^0.6.0-beta.29", - "undici": "^7.0.0", + "undici": "^7.4.0", "yargs": "^17.0.0" } }, @@ -130,17 +130,203 @@ "@openzeppelin/contracts": "5.3.0" } }, + "lib/openzeppelin-contracts/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "lib/openzeppelin-contracts/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "lib/openzeppelin-contracts/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "lib/openzeppelin-contracts/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "lib/openzeppelin-contracts/node_modules/commander": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", + "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "lib/openzeppelin-contracts/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, "lib/openzeppelin-contracts/node_modules/eslint-config-prettier": { - "version": "9.1.0", + "version": "10.1.5", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz", + "integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==", "dev": true, "license": "MIT", "bin": { "eslint-config-prettier": "bin/cli.js" }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, "peerDependencies": { "eslint": ">=7.0.0" } }, + "lib/openzeppelin-contracts/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "lib/openzeppelin-contracts/node_modules/globals": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", + "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "lib/openzeppelin-contracts/node_modules/globby": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.2.tgz", + "integrity": "sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/glob": "^7.1.1", + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.0.3", + "glob": "^7.1.3", + "ignore": "^5.1.1", + "merge2": "^1.2.3", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "lib/openzeppelin-contracts/node_modules/globby/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "lib/openzeppelin-contracts/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "lib/openzeppelin-contracts/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "lib/openzeppelin-contracts/node_modules/lint-staged": { + "version": "16.1.2", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.1.2.tgz", + "integrity": "sha512-sQKw2Si2g9KUZNY3XNvRuDq4UJqpHwF0/FQzZR2M7I5MvtpWvibikCjUVJzZdGE0ByurEl3KQNvsGetd1ty1/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1", + "commander": "^14.0.0", + "debug": "^4.4.1", + "lilconfig": "^3.1.3", + "listr2": "^8.3.3", + "micromatch": "^4.0.8", + "nano-spawn": "^1.0.2", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.8.0" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, "lib/openzeppelin-contracts/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -186,6 +372,78 @@ "url": "https://github.com/sponsors/isaacs" } }, + "lib/openzeppelin-contracts/node_modules/solidity-coverage": { + "version": "0.8.16", + "resolved": "https://registry.npmjs.org/solidity-coverage/-/solidity-coverage-0.8.16.tgz", + "integrity": "sha512-qKqgm8TPpcnCK0HCDLJrjbOA2tQNEJY4dHX/LSSQ9iwYFS973MwjtgYn2Iv3vfCEQJTj5xtm4cuUMzlJsJSMbg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@ethersproject/abi": "^5.0.9", + "@solidity-parser/parser": "^0.20.1", + "chalk": "^2.4.2", + "death": "^1.1.0", + "difflib": "^0.2.4", + "fs-extra": "^8.1.0", + "ghost-testrpc": "^0.0.2", + "global-modules": "^2.0.0", + "globby": "^10.0.1", + "jsonschema": "^1.2.4", + "lodash": "^4.17.21", + "mocha": "^10.2.0", + "node-emoji": "^1.10.0", + "pify": "^4.0.1", + "recursive-readdir": "^2.2.2", + "sc-istanbul": "^0.4.5", + "semver": "^7.3.4", + "shelljs": "^0.8.3", + "web3-utils": "^1.3.6" + }, + "bin": { + "solidity-coverage": "plugins/bin.js" + }, + "peerDependencies": { + "hardhat": "^2.11.0" + } + }, + "lib/openzeppelin-contracts/node_modules/solidity-coverage/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "lib/openzeppelin-contracts/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "lib/openzeppelin-contracts/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "lib/openzeppelin-contracts/scripts/solhint-custom": { "name": "solhint-plugin-openzeppelin", "version": "0.0.0", @@ -8732,6 +8990,19 @@ "dev": true, "license": "MIT" }, + "node_modules/nano-spawn": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-1.0.2.tgz", + "integrity": "sha512-21t+ozMQDAL/UGgQVBbZ/xXvNO10++ZPuTmKRO8k9V3AClVRht49ahtDjfY8l1q6nSHOrE5ASfthzH3ol6R/hg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" + } + }, "node_modules/napi-build-utils": { "version": "2.0.0", "dev": true, diff --git a/test/finance/ERC7821WithExecutor.test.ts b/test/finance/ERC7821WithExecutor.test.ts new file mode 100644 index 00000000..8a33ae24 --- /dev/null +++ b/test/finance/ERC7821WithExecutor.test.ts @@ -0,0 +1,60 @@ +import { ERC7821WithExecutor } from '../../types'; +import { anyValue } from '@nomicfoundation/hardhat-chai-matchers/withArgs'; +import { encodeMode, encodeBatch, CALL_TYPE_BATCH } from '@openzeppelin/contracts/test/helpers/erc7579'; +import { expect } from 'chai'; +import { ethers, fhevm } from 'hardhat'; + +const name = 'ConfidentialFungibleToken'; +const symbol = 'CFT'; +const uri = 'https://example.com/metadata'; + +describe('ERC7821WithExecutor', function () { + beforeEach(async function () { + const [recipient, executor, ...accounts] = await ethers.getSigners(); + + const token = await ethers.deployContract('$ConfidentialFungibleTokenMock', [name, symbol, uri]); + + const encryptedInput = await fhevm + .createEncryptedInput(await token.getAddress(), recipient.address) + .add64(1000) + .encrypt(); + + const executorWallet = (await ethers.deployContract('$ERC7821WithExecutor', [ + executor, + ])) as unknown as ERC7821WithExecutor; + + await (token as any) + .connect(recipient) + ['$_mint(address,bytes32,bytes)'](executorWallet.target, encryptedInput.handles[0], encryptedInput.inputProof); + + Object.assign(this, { accounts, recipient, executor, executorWallet, token }); + }); + + describe('call', async function () { + it('should fail if not called by executor', async function () { + await expect(this.executorWallet.connect(this.recipient).execute(ethers.ZeroHash, '0x')) + .to.be.revertedWithCustomError(this.executorWallet, 'AccountUnauthorized') + .withArgs(this.recipient); + }); + + it('should call if called by executor', async function () { + const balance = await this.token.confidentialBalanceOf(this.executorWallet); + + await expect( + this.executorWallet.connect(this.executor).execute( + encodeMode({ callType: CALL_TYPE_BATCH }), + encodeBatch({ + target: this.token, + value: 0n, + data: this.token.interface.encodeFunctionData('confidentialTransfer(address,bytes32)', [ + this.recipient.address, + balance, + ]), + }), + ), + ) + .to.emit(this.token, 'ConfidentialTransfer') + .withArgs(this.executorWallet, this.recipient, anyValue); + }); + }); +}); diff --git a/test/finance/VestingWalletExecutorConfidential.test.ts b/test/finance/VestingWalletExecutorConfidential.test.ts deleted file mode 100644 index 03ea0456..00000000 --- a/test/finance/VestingWalletExecutorConfidential.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { shouldBehaveLikeVestingConfidential } from './VestingWalletConfidential.behavior'; -import { anyValue } from '@nomicfoundation/hardhat-chai-matchers/withArgs'; -import { time } from '@nomicfoundation/hardhat-network-helpers'; -import { expect } from 'chai'; -import { ethers, fhevm } from 'hardhat'; - -const name = 'ConfidentialFungibleToken'; -const symbol = 'CFT'; -const uri = 'https://example.com/metadata'; - -describe('VestingWalletExecutorConfidential', function () { - beforeEach(async function () { - const accounts = (await ethers.getSigners()).slice(3); - const [holder, recipient, executor] = accounts; - - const token = await ethers.deployContract('$ConfidentialFungibleTokenMock', [name, symbol, uri]); - - const encryptedInput = await fhevm - .createEncryptedInput(await token.getAddress(), holder.address) - .add64(1000) - .encrypt(); - - const currentTime = await time.latest(); - const schedule = [currentTime + 60, currentTime + 60 * 61]; - const vesting = await ethers.deployContract('$VestingWalletExecutorConfidentialMock', [ - recipient, - currentTime + 60, - 60 * 60 /* 1 hour */, - executor, - ]); - - await (token as any) - .connect(holder) - ['$_mint(address,bytes32,bytes)'](vesting.target, encryptedInput.handles[0], encryptedInput.inputProof); - - Object.assign(this, { accounts, holder, recipient, executor, token, vesting, schedule, vestingAmount: 1000 }); - }); - - describe('call', async function () { - it('should fail if not called by executor', async function () { - await expect(this.vesting.call(this.token, 0, '0x')).to.be.revertedWithCustomError( - this.vesting, - 'VestingWalletExecutorConfidentialOnlyExecutor', - ); - }); - - it('should call if called by executor', async function () { - await expect( - this.vesting - .connect(this.executor) - .call( - this.token, - 0, - ( - await this.token.confidentialTransfer.populateTransaction( - this.recipient, - await this.token.confidentialBalanceOf(this.vesting), - ) - ).data, - ), - ) - .to.emit(this.token, 'ConfidentialTransfer') - .withArgs(this.vesting, this.recipient, anyValue); - }); - }); - - shouldBehaveLikeVestingConfidential(); -}); From c92718afcba465f42c2b294173d64f08ff10d423 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Fri, 11 Jul 2025 12:37:57 -0600 Subject: [PATCH 15/28] clean --- contracts/finance/ERC7821WithExecutor.sol | 4 ++-- contracts/finance/README.adoc | 7 +++---- .../finance/VestingWalletCliffConfidential.sol | 4 ++-- contracts/finance/VestingWalletConfidential.sol | 4 ---- .../VestingWalletConfidentialFactory.sol | 17 ++++++++++------- .../VestingWalletCliffConfidential.test.ts | 3 +-- .../VestingWalletConfidentialFactory.test.ts | 4 ++-- 7 files changed, 20 insertions(+), 23 deletions(-) diff --git a/contracts/finance/ERC7821WithExecutor.sol b/contracts/finance/ERC7821WithExecutor.sol index a9918164..664557e0 100644 --- a/contracts/finance/ERC7821WithExecutor.sol +++ b/contracts/finance/ERC7821WithExecutor.sol @@ -5,8 +5,7 @@ import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Ini import {ERC7821} from "@openzeppelin/contracts/account/extensions/draft-ERC7821.sol"; /** - * @dev 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). + * @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 @@ -35,6 +34,7 @@ abstract contract ERC7821WithExecutor is Initializable, ERC7821 { return _getERC7821WithExecutorStorage()._executor; } + /// @inheritdoc ERC7821 function _erc7821AuthorizedExecutor( address caller, bytes32 mode, diff --git a/contracts/finance/README.adoc b/contracts/finance/README.adoc index 486ac39c..8e3a4608 100644 --- a/contracts/finance/README.adoc +++ b/contracts/finance/README.adoc @@ -8,17 +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 supports both "cliff" ({VestingWalletCliffConfidential}) and "executor" ({VestingWalletExecutorConfidential}) extensions. -- {VestingWalletConfidentialFactory}: A factory which allows creating {VestingWalletCliffExecutorConfidential} in batch. +- {VestingWalletCliffExecutorConfidential}: A Default implementation of `VestingWalletConfidential` which implements both `VestingWalletCliffConfidential` and execution via {ERC7821WithExecutor}. +- {VestingWalletConfidentialFactory}: A factory which allows creating `VestingWalletCliffExecutorConfidential` in batch. == Contracts {{VestingWalletConfidential}} {{VestingWalletCliffConfidential}} -{{VestingWalletExecutorConfidential}} {{VestingWalletConfidentialFactory}} {{VestingWalletCliffExecutorConfidential}} +{{ERC7821WithExecutor}} diff --git a/contracts/finance/VestingWalletCliffConfidential.sol b/contracts/finance/VestingWalletCliffConfidential.sol index bf55b9ed..c202e976 100644 --- a/contracts/finance/VestingWalletCliffConfidential.sol +++ b/contracts/finance/VestingWalletCliffConfidential.sol @@ -26,7 +26,7 @@ abstract contract VestingWalletCliffConfidential is VestingWalletConfidential { } /// @dev The specified cliff duration is larger than the vesting duration. - error InvalidCliffDuration(uint64 cliffSeconds, uint64 durationSeconds); + error VestingWalletCliffConfidentialInvalidCliffDuration(uint64 cliffSeconds, uint64 durationSeconds); /** * @dev Set the duration of the cliff, in seconds. The cliff starts at the vesting @@ -35,7 +35,7 @@ abstract contract VestingWalletCliffConfidential is VestingWalletConfidential { // solhint-disable-next-line func-name-mixedcase function __VestingWalletCliffConfidential_init(uint48 cliffSeconds) internal onlyInitializing { if (cliffSeconds > duration()) { - revert InvalidCliffDuration(cliffSeconds, duration()); + revert VestingWalletCliffConfidentialInvalidCliffDuration(cliffSeconds, duration()); } _getVestingWalletCliffStorage()._cliff = start() + cliffSeconds; } diff --git a/contracts/finance/VestingWalletConfidential.sol b/contracts/finance/VestingWalletConfidential.sol index 6d354bb6..25ae6828 100644 --- a/contracts/finance/VestingWalletConfidential.sol +++ b/contracts/finance/VestingWalletConfidential.sol @@ -3,10 +3,8 @@ pragma solidity ^0.8.24; import {FHE, ebool, euint64, euint128} from "@fhevm/solidity/lib/FHE.sol"; import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {ReentrancyGuardTransient} from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol"; import {IConfidentialFungibleToken} from "./../interfaces/IConfidentialFungibleToken.sol"; -import {TFHESafeMath} from "./../utils/TFHESafeMath.sol"; /** * @dev A vesting wallet is an ownable contract that can receive ConfidentialFungibleTokens, and release these @@ -45,8 +43,6 @@ abstract contract VestingWalletConfidential is OwnableUpgradeable, ReentrancyGua event VestingWalletConfidentialTokenReleased(address indexed token, euint64 amount); - error VestingWalletConfidentialInvalidDuration(); - /** * @dev Initializes the vesting wallet for a given `beneficiary` with a start time of `startTimestamp` * and an end time of `startTimestamp + durationSeconds`. diff --git a/contracts/finance/VestingWalletConfidentialFactory.sol b/contracts/finance/VestingWalletConfidentialFactory.sol index 530ab02e..ec207740 100644 --- a/contracts/finance/VestingWalletConfidentialFactory.sol +++ b/contracts/finance/VestingWalletConfidentialFactory.sol @@ -1,12 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {FHE, euint64, externalEuint64, euint128, ebool} from "@fhevm/solidity/lib/FHE.sol"; +import {FHE, euint64, externalEuint64, euint128} from "@fhevm/solidity/lib/FHE.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"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; /** * @dev This factory enables creating {VestingWalletCliffExecutorConfidential} in batch. @@ -36,10 +37,6 @@ contract VestingWalletConfidentialFactory { address executor ); - /// @dev The specified cliff duration is larger than the vesting duration. - error InvalidCliffDuration(uint64 cliffSeconds, uint64 durationSeconds); - error InvalidVestingBeneficiary(address account); - struct VestingPlan { address beneficiary; externalEuint64 encryptedAmount; @@ -67,10 +64,16 @@ contract VestingWalletConfidentialFactory { address executor, bytes calldata inputProof ) public virtual { - require(cliffSeconds <= durationSeconds, InvalidCliffDuration(cliffSeconds, durationSeconds)); + require( + cliffSeconds <= durationSeconds, + VestingWalletCliffConfidential.VestingWalletCliffConfidentialInvalidCliffDuration( + cliffSeconds, + durationSeconds + ) + ); for (uint256 i = 0; i < vestingPlans.length; i++) { VestingPlan memory vestingPlan = vestingPlans[i]; - require(vestingPlan.beneficiary != address(0), InvalidVestingBeneficiary(address(0))); + require(vestingPlan.beneficiary != address(0), OwnableUpgradeable.OwnableInvalidOwner(address(0))); address vestingWalletConfidential = predictVestingWalletConfidential( vestingPlan.beneficiary, vestingPlan.start, diff --git a/test/finance/VestingWalletCliffConfidential.test.ts b/test/finance/VestingWalletCliffConfidential.test.ts index 2308fb29..94609802 100644 --- a/test/finance/VestingWalletCliffConfidential.test.ts +++ b/test/finance/VestingWalletCliffConfidential.test.ts @@ -2,7 +2,6 @@ import { shouldBehaveLikeVestingConfidential } from './VestingWalletConfidential import { FhevmType } from '@fhevm/hardhat-plugin'; import { time } from '@nomicfoundation/hardhat-network-helpers'; import { expect } from 'chai'; -import { EventLog } from 'ethers'; import { ethers, fhevm } from 'hardhat'; const name = 'ConfidentialFungibleToken'; @@ -64,7 +63,7 @@ describe(`VestingWalletCliffConfidential`, function () { 60 * 10, 60 * 60, ]), - ).to.be.revertedWithCustomError(this.vesting, 'InvalidCliffDuration'); + ).to.be.revertedWithCustomError(this.vesting, 'VestingWalletCliffConfidentialInvalidCliffDuration'); }); shouldBehaveLikeVestingConfidential(); diff --git a/test/finance/VestingWalletConfidentialFactory.test.ts b/test/finance/VestingWalletConfidentialFactory.test.ts index a50b3117..6d63fc35 100644 --- a/test/finance/VestingWalletConfidentialFactory.test.ts +++ b/test/finance/VestingWalletConfidentialFactory.test.ts @@ -220,7 +220,7 @@ describe('VestingWalletConfidentialFactory', function () { this.executor, encryptedInput.inputProof, ), - ).to.be.revertedWithCustomError(this.factory, 'InvalidCliffDuration'); + ).to.be.revertedWithCustomError(this.factory, 'VestingWalletCliffConfidentialInvalidCliffDuration'); }); it('should not batch with invalid beneficiary', async function () { @@ -244,6 +244,6 @@ describe('VestingWalletConfidentialFactory', function () { this.executor, encryptedInput.inputProof, ), - ).to.be.revertedWithCustomError(this.factory, 'InvalidVestingBeneficiary'); + ).to.be.revertedWithCustomError(this.factory, 'OwnableInvalidOwner'); }); }); From c8b511639cc7e86f0445f878eb7da3a3d8e3715e Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Fri, 11 Jul 2025 12:42:57 -0600 Subject: [PATCH 16/28] fix imports order --- contracts/finance/VestingWalletConfidentialFactory.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/finance/VestingWalletConfidentialFactory.sol b/contracts/finance/VestingWalletConfidentialFactory.sol index ec207740..2c20fc31 100644 --- a/contracts/finance/VestingWalletConfidentialFactory.sol +++ b/contracts/finance/VestingWalletConfidentialFactory.sol @@ -2,12 +2,12 @@ pragma solidity ^0.8.24; 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"; -import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; /** * @dev This factory enables creating {VestingWalletCliffExecutorConfidential} in batch. From e8a91a800f87a5cd7c0782f195c18a3563c43994 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Fri, 11 Jul 2025 12:50:56 -0600 Subject: [PATCH 17/28] Apply suggestions from code review --- contracts/finance/VestingWalletConfidentialFactory.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/finance/VestingWalletConfidentialFactory.sol b/contracts/finance/VestingWalletConfidentialFactory.sol index 2c20fc31..d0b04a29 100644 --- a/contracts/finance/VestingWalletConfidentialFactory.sol +++ b/contracts/finance/VestingWalletConfidentialFactory.sol @@ -21,12 +21,12 @@ contract VestingWalletConfidentialFactory { event VestingWalletConfidentialFunded( address indexed vestingWalletConfidential, address indexed beneficiary, - address confidentialFungibleToken, + address indexed confidentialFungibleToken, euint64 encryptedAmount, uint48 startTimestamp, uint48 durationSeconds, uint48 cliffSeconds, - address executor + address indexed executor ); event VestingWalletConfidentialCreated( address indexed vestingWalletConfidential, @@ -34,7 +34,7 @@ contract VestingWalletConfidentialFactory { uint48 startTimestamp, uint48 durationSeconds, uint48 cliffSeconds, - address executor + address indexed executor ); struct VestingPlan { From 80e272a70fb42e74843f4d6e554c65ae2cff202c Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Fri, 11 Jul 2025 12:54:00 -0600 Subject: [PATCH 18/28] up --- contracts/finance/VestingWalletConfidentialFactory.sol | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/contracts/finance/VestingWalletConfidentialFactory.sol b/contracts/finance/VestingWalletConfidentialFactory.sol index d0b04a29..4e7ab3bd 100644 --- a/contracts/finance/VestingWalletConfidentialFactory.sol +++ b/contracts/finance/VestingWalletConfidentialFactory.sol @@ -26,7 +26,7 @@ contract VestingWalletConfidentialFactory { uint48 startTimestamp, uint48 durationSeconds, uint48 cliffSeconds, - address indexed executor + address executor ); event VestingWalletConfidentialCreated( address indexed vestingWalletConfidential, @@ -50,7 +50,7 @@ contract VestingWalletConfidentialFactory { /** * @dev Batches the funding of multiple confidential vesting wallets. * - * Funds are sent to predeterministic wallet addresses. Wallets can be created either + * Funds are sent to deterministic wallet addresses. Wallets can be created either * before or after this operation. * * Emits a single {VestingWalletConfidentialBatchFunded} event in addition to multiple @@ -71,7 +71,8 @@ contract VestingWalletConfidentialFactory { durationSeconds ) ); - for (uint256 i = 0; i < vestingPlans.length; i++) { + uint256 vestingPlansLength = vestingPlans.length; + for (uint256 i = 0; i < vestingPlansLength; i++) { VestingPlan memory vestingPlan = vestingPlans[i]; require(vestingPlan.beneficiary != address(0), OwnableUpgradeable.OwnableInvalidOwner(address(0))); address vestingWalletConfidential = predictVestingWalletConfidential( From 83320ca4565cb3a8297be5a140b4c07362dc883c Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Fri, 11 Jul 2025 13:16:55 -0600 Subject: [PATCH 19/28] update comments --- contracts/finance/ERC7821WithExecutor.sol | 2 +- contracts/finance/VestingWalletConfidentialFactory.sol | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/finance/ERC7821WithExecutor.sol b/contracts/finance/ERC7821WithExecutor.sol index 664557e0..e9eca914 100644 --- a/contracts/finance/ERC7821WithExecutor.sol +++ b/contracts/finance/ERC7821WithExecutor.sol @@ -29,7 +29,7 @@ abstract contract ERC7821WithExecutor is Initializable, ERC7821 { _getERC7821WithExecutorStorage()._executor = executor_; } - /// @dev Trusted address that is able to execute arbitrary calls from the vesting wallet via `ERC7821`. + /// @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; } diff --git a/contracts/finance/VestingWalletConfidentialFactory.sol b/contracts/finance/VestingWalletConfidentialFactory.sol index 4e7ab3bd..c2a39e74 100644 --- a/contracts/finance/VestingWalletConfidentialFactory.sol +++ b/contracts/finance/VestingWalletConfidentialFactory.sol @@ -12,8 +12,8 @@ import {VestingWalletConfidential} from "./VestingWalletConfidential.sol"; /** * @dev This factory enables creating {VestingWalletCliffExecutorConfidential} in batch. * - * All confidential vesting wallets created support both "cliff" ({VestingWalletCliffConfidential}) - * and "executor" ({VestingWalletExecutorConfidential}) extensions. + * 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 VestingWalletConfidentialFactory { address private immutable _vestingImplementation; From 19629c214904e5d703644773b480331e044970cb Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Fri, 11 Jul 2025 14:15:25 -0600 Subject: [PATCH 20/28] remove constructor --- contracts/finance/VestingWalletConfidentialFactory.sol | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/contracts/finance/VestingWalletConfidentialFactory.sol b/contracts/finance/VestingWalletConfidentialFactory.sol index c2a39e74..5d3cff92 100644 --- a/contracts/finance/VestingWalletConfidentialFactory.sol +++ b/contracts/finance/VestingWalletConfidentialFactory.sol @@ -16,7 +16,7 @@ import {VestingWalletConfidential} from "./VestingWalletConfidential.sol"; * and {ERC7821WithExecutor} to allow for arbitrary calls to be executed from the vesting wallet. */ contract VestingWalletConfidentialFactory { - address private immutable _vestingImplementation; + address private immutable _vestingImplementation = address(new VestingWalletCliffExecutorConfidential()); event VestingWalletConfidentialFunded( address indexed vestingWalletConfidential, @@ -43,10 +43,6 @@ contract VestingWalletConfidentialFactory { uint48 start; } - constructor() { - _vestingImplementation = address(new VestingWalletCliffExecutorConfidential()); - } - /** * @dev Batches the funding of multiple confidential vesting wallets. * From d45da96a780c530f94ac879224b1a6d1887cb71d Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Fri, 11 Jul 2025 14:25:47 -0600 Subject: [PATCH 21/28] fix function ordering --- contracts/finance/ERC7821WithExecutor.sol | 18 ++++---- .../VestingWalletCliffConfidential.sol | 22 +++++----- .../finance/VestingWalletConfidential.sol | 44 +++++++++---------- .../VestingWalletConfidentialFactory.sol | 12 ++--- 4 files changed, 48 insertions(+), 48 deletions(-) diff --git a/contracts/finance/ERC7821WithExecutor.sol b/contracts/finance/ERC7821WithExecutor.sol index e9eca914..46815928 100644 --- a/contracts/finance/ERC7821WithExecutor.sol +++ b/contracts/finance/ERC7821WithExecutor.sol @@ -18,10 +18,9 @@ abstract contract ERC7821WithExecutor is Initializable, ERC7821 { bytes32 private constant ERC7821WithExecutorStorageLocation = 0x246106ffca67a7d3806ba14f6748826b9c39c9fa594b14f83fe454e8e9d0dc00; - function _getERC7821WithExecutorStorage() private pure returns (ERC7821WithExecutorStorage storage $) { - assembly { - $.slot := ERC7821WithExecutorStorageLocation - } + /// @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 @@ -29,11 +28,6 @@ abstract contract ERC7821WithExecutor is Initializable, ERC7821 { _getERC7821WithExecutorStorage()._executor = executor_; } - /// @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; - } - /// @inheritdoc ERC7821 function _erc7821AuthorizedExecutor( address caller, @@ -42,4 +36,10 @@ abstract contract ERC7821WithExecutor is Initializable, ERC7821 { ) internal view virtual override returns (bool) { return caller == executor() || super._erc7821AuthorizedExecutor(caller, mode, executionData); } + + function _getERC7821WithExecutorStorage() private pure returns (ERC7821WithExecutorStorage storage $) { + assembly { + $.slot := ERC7821WithExecutorStorageLocation + } + } } diff --git a/contracts/finance/VestingWalletCliffConfidential.sol b/contracts/finance/VestingWalletCliffConfidential.sol index c202e976..fc2a0609 100644 --- a/contracts/finance/VestingWalletCliffConfidential.sol +++ b/contracts/finance/VestingWalletCliffConfidential.sol @@ -19,15 +19,14 @@ 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 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. @@ -40,11 +39,6 @@ abstract contract VestingWalletCliffConfidential is VestingWalletConfidential { _getVestingWalletCliffStorage()._cliff = start() + cliffSeconds; } - /// @dev The timestamp at which the cliff ends. - function cliff() public view virtual returns (uint64) { - return _getVestingWalletCliffStorage()._cliff; - } - /** * @dev This function returns the amount vested, as a function of time, for * an asset given its total historical allocation. Returns 0 if the {cliff} timestamp is not met. @@ -56,4 +50,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 + } + } } diff --git a/contracts/finance/VestingWalletConfidential.sol b/contracts/finance/VestingWalletConfidential.sol index 25ae6828..c01c7906 100644 --- a/contracts/finance/VestingWalletConfidential.sol +++ b/contracts/finance/VestingWalletConfidential.sol @@ -35,30 +35,8 @@ abstract contract VestingWalletConfidential is OwnableUpgradeable, ReentrancyGua bytes32 private constant VestingWalletStorageLocation = 0x78ce9ee9eb65fa0cf5bf10e861c3a95cb7c3c713c96ab1e5323a21e846796800; - function _getVestingWalletStorage() private pure returns (VestingWalletStorage storage $) { - assembly { - $.slot := VestingWalletStorageLocation - } - } - event VestingWalletConfidentialTokenReleased(address indexed token, euint64 amount); - /** - * @dev Initializes the vesting wallet for a given `beneficiary` with a start time of `startTimestamp` - * and an end time of `startTimestamp + durationSeconds`. - */ - // solhint-disable-next-line func-name-mixedcase - function __VestingWalletConfidential_init( - address beneficiary, - uint48 startTimestamp, - uint48 durationSeconds - ) internal onlyInitializing { - __Ownable_init(beneficiary); - VestingWalletStorage storage $ = _getVestingWalletStorage(); - $._start = startTimestamp; - $._duration = durationSeconds; - } - /// @dev Timestamp at which the vesting starts. function start() public view virtual returns (uint64) { return _getVestingWalletStorage()._start; @@ -116,6 +94,22 @@ abstract contract VestingWalletConfidential is OwnableUpgradeable, ReentrancyGua ); } + /** + * @dev Initializes the vesting wallet for a given `beneficiary` with a start time of `startTimestamp` + * and an end time of `startTimestamp + durationSeconds`. + */ + // solhint-disable-next-line func-name-mixedcase + function __VestingWalletConfidential_init( + address beneficiary, + uint48 startTimestamp, + uint48 durationSeconds + ) internal onlyInitializing { + __Ownable_init(beneficiary); + VestingWalletStorage storage $ = _getVestingWalletStorage(); + $._start = startTimestamp; + $._duration = durationSeconds; + } + /// @dev This returns the amount vested, as a function of time, for an asset given its total historical allocation. function _vestingSchedule(euint128 totalAllocation, uint64 timestamp) internal virtual returns (euint128) { if (timestamp < start()) { @@ -126,4 +120,10 @@ abstract contract VestingWalletConfidential is OwnableUpgradeable, ReentrancyGua return FHE.div(FHE.mul(totalAllocation, (timestamp - start())), duration()); } } + + function _getVestingWalletStorage() private pure returns (VestingWalletStorage storage $) { + assembly { + $.slot := VestingWalletStorageLocation + } + } } diff --git a/contracts/finance/VestingWalletConfidentialFactory.sol b/contracts/finance/VestingWalletConfidentialFactory.sol index 5d3cff92..ec508923 100644 --- a/contracts/finance/VestingWalletConfidentialFactory.sol +++ b/contracts/finance/VestingWalletConfidentialFactory.sol @@ -16,6 +16,12 @@ import {VestingWalletConfidential} from "./VestingWalletConfidential.sol"; * and {ERC7821WithExecutor} to allow for arbitrary calls to be executed from the vesting wallet. */ contract VestingWalletConfidentialFactory { + struct VestingPlan { + address beneficiary; + externalEuint64 encryptedAmount; + uint48 start; + } + address private immutable _vestingImplementation = address(new VestingWalletCliffExecutorConfidential()); event VestingWalletConfidentialFunded( @@ -37,12 +43,6 @@ contract VestingWalletConfidentialFactory { address indexed executor ); - struct VestingPlan { - address beneficiary; - externalEuint64 encryptedAmount; - uint48 start; - } - /** * @dev Batches the funding of multiple confidential vesting wallets. * From 3da80bf1510f05e1c4a9122f24cbb86198203034 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Fri, 11 Jul 2025 14:28:13 -0600 Subject: [PATCH 22/28] add changeset --- .changeset/poor-colts-glow.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/poor-colts-glow.md diff --git a/.changeset/poor-colts-glow.md b/.changeset/poor-colts-glow.md new file mode 100644 index 00000000..1a042cc7 --- /dev/null +++ b/.changeset/poor-colts-glow.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-confidential-contracts': minor +--- + +`ERC7821WithExecutor`: Add an abstract contract that inherits from `ERC7821` and add's an `executor` role. From 4f665d4db0e312f7481d3937c0842f748d321771 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Fri, 11 Jul 2025 14:34:31 -0600 Subject: [PATCH 23/28] Update .changeset/tricky-boxes-train.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ernesto GarcĂ­a --- .changeset/tricky-boxes-train.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/tricky-boxes-train.md b/.changeset/tricky-boxes-train.md index 94068755..7641b8f1 100644 --- a/.changeset/tricky-boxes-train.md +++ b/.changeset/tricky-boxes-train.md @@ -2,4 +2,4 @@ 'openzeppelin-confidential-contracts': minor --- -`VestingWalletConfidentialFactory`: Fund multiple `VestingWalletConfidential` in batch. +`VestingWalletConfidentialFactory`: Fund multiple `VestingWalletCliffExecutorConfidential` in batch. From d835bf0791eb8fa98c3d23f465e79ec0afdc90b8 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Fri, 11 Jul 2025 14:40:11 -0600 Subject: [PATCH 24/28] `VestingWalletConfidentialFactory` -> `VestingWalletCliffExecutorConfidentialFactory` --- .changeset/tricky-boxes-train.md | 2 +- contracts/finance/README.adoc | 4 ++-- ... => VestingWalletCliffExecutorConfidentialFactory.sol} | 2 +- .../finance/VestingWalletConfidentialFactoryMock.sol | 7 +++++-- test/finance/VestingWalletConfidentialFactory.test.ts | 8 ++++---- 5 files changed, 13 insertions(+), 10 deletions(-) rename contracts/finance/{VestingWalletConfidentialFactory.sol => VestingWalletCliffExecutorConfidentialFactory.sol} (99%) diff --git a/.changeset/tricky-boxes-train.md b/.changeset/tricky-boxes-train.md index 94068755..9eea7f59 100644 --- a/.changeset/tricky-boxes-train.md +++ b/.changeset/tricky-boxes-train.md @@ -2,4 +2,4 @@ 'openzeppelin-confidential-contracts': minor --- -`VestingWalletConfidentialFactory`: Fund multiple `VestingWalletConfidential` in batch. +`VestingWalletCliffExecutorConfidentialFactory`: Fund multiple `VestingWalletConfidential` in batch. diff --git a/contracts/finance/README.adoc b/contracts/finance/README.adoc index 8e3a4608..7149dcf2 100644 --- a/contracts/finance/README.adoc +++ b/contracts/finance/README.adoc @@ -12,12 +12,12 @@ This directory includes primitives for on-chain confidential financial systems: For convenience, this directory also includes: - {VestingWalletCliffExecutorConfidential}: A Default implementation of `VestingWalletConfidential` which implements both `VestingWalletCliffConfidential` and execution via {ERC7821WithExecutor}. -- {VestingWalletConfidentialFactory}: A factory which allows creating `VestingWalletCliffExecutorConfidential` in batch. +- {VestingWalletCliffExecutorConfidentialFactory}: A factory which allows creating `VestingWalletCliffExecutorConfidential` in batch. == Contracts {{VestingWalletConfidential}} {{VestingWalletCliffConfidential}} -{{VestingWalletConfidentialFactory}} +{{VestingWalletCliffExecutorConfidentialFactory}} {{VestingWalletCliffExecutorConfidential}} {{ERC7821WithExecutor}} diff --git a/contracts/finance/VestingWalletConfidentialFactory.sol b/contracts/finance/VestingWalletCliffExecutorConfidentialFactory.sol similarity index 99% rename from contracts/finance/VestingWalletConfidentialFactory.sol rename to contracts/finance/VestingWalletCliffExecutorConfidentialFactory.sol index ec508923..a237cf7e 100644 --- a/contracts/finance/VestingWalletConfidentialFactory.sol +++ b/contracts/finance/VestingWalletCliffExecutorConfidentialFactory.sol @@ -15,7 +15,7 @@ import {VestingWalletConfidential} from "./VestingWalletConfidential.sol"; * 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 VestingWalletConfidentialFactory { +contract VestingWalletCliffExecutorConfidentialFactory { struct VestingPlan { address beneficiary; externalEuint64 encryptedAmount; diff --git a/contracts/mocks/finance/VestingWalletConfidentialFactoryMock.sol b/contracts/mocks/finance/VestingWalletConfidentialFactoryMock.sol index f8b20092..9d31063c 100644 --- a/contracts/mocks/finance/VestingWalletConfidentialFactoryMock.sol +++ b/contracts/mocks/finance/VestingWalletConfidentialFactoryMock.sol @@ -2,6 +2,9 @@ pragma solidity ^0.8.24; import {SepoliaConfig} from "@fhevm/solidity/config/ZamaConfig.sol"; -import {VestingWalletConfidentialFactory} from "../../finance/VestingWalletConfidentialFactory.sol"; +import {VestingWalletCliffExecutorConfidentialFactory} from "../../finance/VestingWalletCliffExecutorConfidentialFactory.sol"; -abstract contract VestingWalletConfidentialFactoryMock is VestingWalletConfidentialFactory, SepoliaConfig {} +abstract contract VestingWalletCliffExecutorConfidentialFactoryMock is + VestingWalletCliffExecutorConfidentialFactory, + SepoliaConfig +{} diff --git a/test/finance/VestingWalletConfidentialFactory.test.ts b/test/finance/VestingWalletConfidentialFactory.test.ts index 6d63fc35..22165901 100644 --- a/test/finance/VestingWalletConfidentialFactory.test.ts +++ b/test/finance/VestingWalletConfidentialFactory.test.ts @@ -1,4 +1,4 @@ -import { $VestingWalletConfidentialFactory } from '../../types/contracts-exposed/finance/VestingWalletConfidentialFactory.sol/$VestingWalletConfidentialFactory'; +import { $VestingWalletCliffExecutorConfidentialFactory } from '../../types/contracts-exposed/finance/VestingWalletCliffExecutorConfidentialFactory.sol/$VestingWalletCliffExecutorConfidentialFactory'; import { $ConfidentialFungibleTokenMock } from '../../types/contracts-exposed/mocks/token/ConfidentialFungibleTokenMock.sol/$ConfidentialFungibleTokenMock'; import { FhevmType } from '@fhevm/hardhat-plugin'; import { anyValue } from '@nomicfoundation/hardhat-chai-matchers/withArgs'; @@ -16,7 +16,7 @@ const cliff = 10; const amount1 = 101; const amount2 = 102; -describe('VestingWalletConfidentialFactory', function () { +describe('VestingWalletCliffExecutorConfidentialFactory', function () { beforeEach(async function () { const [holder, recipient, recipient2, operator, executor, ...accounts] = await ethers.getSigners(); @@ -32,8 +32,8 @@ describe('VestingWalletConfidentialFactory', function () { .encrypt(); const factory = (await ethers.deployContract( - '$VestingWalletConfidentialFactoryMock', - )) as unknown as $VestingWalletConfidentialFactory; + '$VestingWalletCliffExecutorConfidentialFactoryMock', + )) as unknown as $VestingWalletCliffExecutorConfidentialFactory; await token .connect(holder) From 353f1edd4e20267ad35665168636e68292403333 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Fri, 11 Jul 2025 16:58:52 -0600 Subject: [PATCH 25/28] Duration and Cliff per vesting plan (#105) --- ...WalletCliffExecutorConfidentialFactory.sol | 41 ++++++++++--------- .../VestingWalletConfidentialFactory.test.ts | 32 ++++++++------- 2 files changed, 39 insertions(+), 34 deletions(-) diff --git a/contracts/finance/VestingWalletCliffExecutorConfidentialFactory.sol b/contracts/finance/VestingWalletCliffExecutorConfidentialFactory.sol index a237cf7e..96f34438 100644 --- a/contracts/finance/VestingWalletCliffExecutorConfidentialFactory.sol +++ b/contracts/finance/VestingWalletCliffExecutorConfidentialFactory.sol @@ -19,7 +19,9 @@ contract VestingWalletCliffExecutorConfidentialFactory { struct VestingPlan { address beneficiary; externalEuint64 encryptedAmount; - uint48 start; + uint48 startTimestamp; + uint48 durationSeconds; + uint48 cliffSeconds; } address private immutable _vestingImplementation = address(new VestingWalletCliffExecutorConfidential()); @@ -55,29 +57,29 @@ contract VestingWalletCliffExecutorConfidentialFactory { function batchFundVestingWalletConfidential( address confidentialFungibleToken, VestingPlan[] calldata vestingPlans, - uint48 durationSeconds, - uint48 cliffSeconds, address executor, bytes calldata inputProof ) public virtual { - require( - cliffSeconds <= durationSeconds, - VestingWalletCliffConfidential.VestingWalletCliffConfidentialInvalidCliffDuration( - cliffSeconds, - durationSeconds - ) - ); 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 vestingWalletConfidential = predictVestingWalletConfidential( + address vestingWalletAddress = predictVestingWalletConfidential( vestingPlan.beneficiary, - vestingPlan.start, - durationSeconds, - cliffSeconds, + vestingPlan.startTimestamp, + vestingPlan.durationSeconds, + vestingPlan.cliffSeconds, executor ); + euint64 transferredAmount; { // avoiding stack too deep with scope @@ -85,18 +87,19 @@ contract VestingWalletCliffExecutorConfidentialFactory { FHE.allowTransient(encryptedAmount, confidentialFungibleToken); transferredAmount = IConfidentialFungibleToken(confidentialFungibleToken).confidentialTransferFrom( msg.sender, - vestingWalletConfidential, + vestingWalletAddress, encryptedAmount ); } + emit VestingWalletConfidentialFunded( - vestingWalletConfidential, + vestingWalletAddress, vestingPlan.beneficiary, confidentialFungibleToken, transferredAmount, - vestingPlan.start, - durationSeconds, - cliffSeconds, + vestingPlan.startTimestamp, + vestingPlan.durationSeconds, + vestingPlan.cliffSeconds, executor ); } diff --git a/test/finance/VestingWalletConfidentialFactory.test.ts b/test/finance/VestingWalletConfidentialFactory.test.ts index 22165901..52e37944 100644 --- a/test/finance/VestingWalletConfidentialFactory.test.ts +++ b/test/finance/VestingWalletConfidentialFactory.test.ts @@ -94,11 +94,11 @@ describe('VestingWalletCliffExecutorConfidentialFactory', function () { .to.emit(this.factory, 'VestingWalletConfidentialCreated') .withArgs(this.recipient, vestingWalletAddress, startTimestamp, duration, cliff, this.executor); const vestingWallet = await ethers.getContractAt('VestingWalletCliffExecutorConfidential', vestingWalletAddress); - expect(await vestingWallet.owner()).to.be.equal(this.recipient); - expect(await vestingWallet.start()).to.be.equal(startTimestamp); - expect(await vestingWallet.duration()).to.be.equal(duration); - expect(await vestingWallet.cliff()).to.be.equal(startTimestamp + cliff); - expect(await vestingWallet.executor()).to.be.equal(this.executor); + await expect(vestingWallet.owner()).to.eventually.equal(this.recipient); + await expect(vestingWallet.start()).to.eventually.equal(startTimestamp); + await expect(vestingWallet.duration()).to.eventually.equal(duration); + await expect(vestingWallet.cliff()).to.eventually.equal(startTimestamp + cliff); + await expect(vestingWallet.executor()).to.eventually.equal(this.executor); }); it('should not create vesting wallet twice', async function () { @@ -143,16 +143,18 @@ describe('VestingWalletCliffExecutorConfidentialFactory', function () { { beneficiary: this.recipient, encryptedAmount: encryptedInput.handles[0], - start: startTimestamp, + startTimestamp, + durationSeconds: duration, + cliffSeconds: cliff, }, { beneficiary: this.recipient2, encryptedAmount: encryptedInput.handles[1], - start: startTimestamp, + startTimestamp, + durationSeconds: duration, + cliffSeconds: cliff, }, ], - duration, - cliff, this.executor, encryptedInput.inputProof, ); @@ -212,11 +214,11 @@ describe('VestingWalletCliffExecutorConfidentialFactory', function () { { beneficiary: this.recipient, encryptedAmount: encryptedInput.handles[0], - start: startTimestamp, + startTimestamp, + durationSeconds: duration, + cliffSeconds: duration + 1, }, ], - duration, - duration + 1, // cliff this.executor, encryptedInput.inputProof, ), @@ -236,11 +238,11 @@ describe('VestingWalletCliffExecutorConfidentialFactory', function () { { beneficiary: ethers.ZeroAddress, encryptedAmount: encryptedInput.handles[0], - start: startTimestamp, + startTimestamp, + durationSeconds: duration, + cliffSeconds: cliff, }, ], - duration, - cliff, this.executor, encryptedInput.inputProof, ), From 4d87ede0a8990a7bf567ef093a3c7ce7965db214 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Fri, 11 Jul 2025 16:59:52 -0600 Subject: [PATCH 26/28] Update .changeset/poor-colts-glow.md --- .changeset/poor-colts-glow.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/poor-colts-glow.md b/.changeset/poor-colts-glow.md index 1a042cc7..46cb05cb 100644 --- a/.changeset/poor-colts-glow.md +++ b/.changeset/poor-colts-glow.md @@ -2,4 +2,4 @@ 'openzeppelin-confidential-contracts': minor --- -`ERC7821WithExecutor`: Add an abstract contract that inherits from `ERC7821` and add's an `executor` role. +`ERC7821WithExecutor`: Add an abstract contract that inherits from `ERC7821` and adds an `executor` role. From 2ad4f1fd4192e82cc86f2c5b0ba79b49a57f48ae Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Fri, 11 Jul 2025 17:03:54 -0600 Subject: [PATCH 27/28] upgrade pragmas --- contracts/finance/ERC7821WithExecutor.sol | 2 +- contracts/finance/VestingWalletCliffConfidential.sol | 10 ++++++---- .../VestingWalletCliffExecutorConfidentialFactory.sol | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/contracts/finance/ERC7821WithExecutor.sol b/contracts/finance/ERC7821WithExecutor.sol index 46815928..839bc960 100644 --- a/contracts/finance/ERC7821WithExecutor.sol +++ b/contracts/finance/ERC7821WithExecutor.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; +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"; diff --git a/contracts/finance/VestingWalletCliffConfidential.sol b/contracts/finance/VestingWalletCliffConfidential.sol index fc2a0609..50c32348 100644 --- a/contracts/finance/VestingWalletCliffConfidential.sol +++ b/contracts/finance/VestingWalletCliffConfidential.sol @@ -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"; @@ -33,9 +33,11 @@ abstract contract VestingWalletCliffConfidential is VestingWalletConfidential { */ // solhint-disable-next-line func-name-mixedcase function __VestingWalletCliffConfidential_init(uint48 cliffSeconds) internal onlyInitializing { - if (cliffSeconds > duration()) { - revert VestingWalletCliffConfidentialInvalidCliffDuration(cliffSeconds, duration()); - } + require( + cliffSeconds <= duration(), + VestingWalletCliffConfidentialInvalidCliffDuration(cliffSeconds, duration()) + ); + _getVestingWalletCliffStorage()._cliff = start() + cliffSeconds; } diff --git a/contracts/finance/VestingWalletCliffExecutorConfidentialFactory.sol b/contracts/finance/VestingWalletCliffExecutorConfidentialFactory.sol index 96f34438..3dd828f0 100644 --- a/contracts/finance/VestingWalletCliffExecutorConfidentialFactory.sol +++ b/contracts/finance/VestingWalletCliffExecutorConfidentialFactory.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; +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"; From 0a6732ab754c39f16c1227cced1afb17f23100ef Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Fri, 11 Jul 2025 17:13:53 -0600 Subject: [PATCH 28/28] fix docs --- .../finance/VestingWalletCliffExecutorConfidentialFactory.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/finance/VestingWalletCliffExecutorConfidentialFactory.sol b/contracts/finance/VestingWalletCliffExecutorConfidentialFactory.sol index 3dd828f0..0fc599c0 100644 --- a/contracts/finance/VestingWalletCliffExecutorConfidentialFactory.sol +++ b/contracts/finance/VestingWalletCliffExecutorConfidentialFactory.sol @@ -51,8 +51,7 @@ contract VestingWalletCliffExecutorConfidentialFactory { * Funds are sent to deterministic wallet addresses. Wallets can be created either * before or after this operation. * - * Emits a single {VestingWalletConfidentialBatchFunded} event in addition to multiple - * {VestingWalletConfidentialFunded} events related to funded vesting plans. + * Emits a {VestingWalletConfidentialFunded} event for each funded vesting plan. */ function batchFundVestingWalletConfidential( address confidentialFungibleToken,