Skip to content

Commit ad85c34

Browse files
authored
add initializable version of vesting (#95)
* add initializable version of vesting * update tests
1 parent 27ca14b commit ad85c34

File tree

6 files changed

+224
-77
lines changed

6 files changed

+224
-77
lines changed

contracts/finance/VestingWalletCliffConfidential.sol

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

44
import {VestingWalletConfidential} from "./VestingWalletConfidential.sol";
55
import {euint64} from "@fhevm/solidity/lib/FHE.sol";
66

7-
contract VestingWalletCliffConfidential is VestingWalletConfidential {
8-
uint64 private _cliff;
7+
abstract contract VestingWalletCliffConfidential is VestingWalletConfidential {
8+
uint64 private immutable _cliff;
99

1010
/// @dev The specified cliff duration is larger than the vesting duration.
1111
error InvalidCliffDuration(uint64 cliffSeconds, uint64 durationSeconds);
@@ -14,18 +14,7 @@ contract VestingWalletCliffConfidential is VestingWalletConfidential {
1414
* @dev Set the duration of the cliff, in seconds. The cliff starts at the vesting
1515
* start timestamp (see {VestingWalletConfidential}) and ends `cliffSeconds` later.
1616
*/
17-
constructor() {
18-
_disableInitializers();
19-
}
20-
21-
function initialize(
22-
address executor_,
23-
address beneficiary,
24-
uint64 startTimestamp,
25-
uint64 durationSeconds,
26-
uint64 cliffSeconds
27-
) public virtual initializer {
28-
VestingWalletConfidential.initialize(executor_, beneficiary, startTimestamp, durationSeconds);
17+
constructor(uint64 cliffSeconds) {
2918
if (cliffSeconds > duration()) {
3019
revert InvalidCliffDuration(cliffSeconds, duration());
3120
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.24;
3+
4+
import {VestingWalletConfidential} from "./VestingWalletConfidential.sol";
5+
import {VestingWalletCliffConfidential} from "./VestingWalletCliffConfidential.sol";
6+
import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
7+
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
8+
9+
contract VestingWalletCliffConfidentialInitializable is Initializable, VestingWalletCliffConfidential {
10+
address private _executor;
11+
uint64 private _start;
12+
uint64 private _duration;
13+
uint64 private _cliff;
14+
15+
constructor() VestingWalletCliffConfidential(0) VestingWalletConfidential(address(0), address(1), 0, 0) {}
16+
17+
function initialize(
18+
address executor_,
19+
address beneficiary,
20+
uint64 startTimestamp,
21+
uint64 durationSeconds,
22+
uint64 cliffSeconds
23+
) public virtual initializer {
24+
require(beneficiary != address(0), Ownable.OwnableInvalidOwner(address(0)));
25+
_transferOwnership(beneficiary);
26+
27+
_executor = executor_;
28+
_start = startTimestamp;
29+
_duration = durationSeconds;
30+
31+
if (cliffSeconds > duration()) {
32+
revert InvalidCliffDuration(cliffSeconds, duration());
33+
}
34+
_cliff = start() + cliffSeconds;
35+
}
36+
37+
/// @inheritdoc VestingWalletConfidential
38+
function executor() public view virtual override returns (address) {
39+
return _executor;
40+
}
41+
42+
/// @inheritdoc VestingWalletConfidential
43+
function start() public view virtual override returns (uint64) {
44+
return _start;
45+
}
46+
47+
/// @inheritdoc VestingWalletConfidential
48+
function duration() public view virtual override returns (uint64) {
49+
return _duration;
50+
}
51+
52+
/// @inheritdoc VestingWalletCliffConfidential
53+
function cliff() public view virtual override returns (uint64) {
54+
return _cliff;
55+
}
56+
}

contracts/finance/VestingWalletConfidential.sol

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

44
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
5-
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
5+
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
66
import {FHE, ebool, euint64} from "@fhevm/solidity/lib/FHE.sol";
77

88
import {IConfidentialFungibleToken} from "../interfaces/IConfidentialFungibleToken.sol";
@@ -24,29 +24,24 @@ import {TFHESafeMath} from "../utils/TFHESafeMath.sol";
2424
* NOTE: When using this contract with any token whose balance is adjusted automatically (i.e. a rebase token), make
2525
* sure to account the supply/balance adjustment in the vesting schedule to ensure the vested amount is as intended.
2626
*/
27-
contract VestingWalletConfidential is OwnableUpgradeable {
27+
abstract contract VestingWalletConfidential is Ownable {
2828
mapping(address token => euint64) private _tokenReleased;
29-
uint64 private _start;
30-
uint64 private _duration;
31-
address private _executor;
29+
uint64 private immutable _start;
30+
uint64 private immutable _duration;
31+
address private immutable _executor;
3232

3333
event VestingWalletConfidentialTokenReleased(address indexed token, euint64 amount);
3434
event VestingWalletCallExecuted(address indexed target, uint256 value, bytes data);
3535

3636
error VestingWalletConfidentialInvalidDuration();
3737
error VestingWalletConfidentialOnlyExecutor();
3838

39-
constructor() {
40-
_disableInitializers();
41-
}
42-
43-
function initialize(
39+
constructor(
4440
address executor_,
4541
address beneficiary,
4642
uint64 startTimestamp,
4743
uint64 durationSeconds
48-
) public virtual initializer {
49-
__Ownable_init(beneficiary);
44+
) Ownable(beneficiary) {
5045
_start = startTimestamp;
5146
_duration = durationSeconds;
5247
_executor = executor_;

contracts/mocks/Create2Factory.sol

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.26;
3+
4+
import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
5+
6+
contract Create2Factory {
7+
event Deployed(address clone);
8+
9+
function create2(address impl, bytes memory data) public returns (address) {
10+
address deployedTo = Clones.clone(impl);
11+
(bool success, ) = deployedTo.call(data);
12+
if (!success) {
13+
assembly {
14+
returndatacopy(0, 0, returndatasize())
15+
revert(0, returndatasize())
16+
}
17+
}
18+
19+
emit Deployed(deployedTo);
20+
return deployedTo;
21+
}
22+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.24;
3+
4+
import {VestingWalletCliffConfidentialInitializable} from "../finance/VestingWalletCliffConfidentialInitializable.sol";
5+
import {FHE} from "@fhevm/solidity/lib/FHE.sol";
6+
import {ZamaConfig} from "@fhevm/solidity/config/ZamaConfig.sol";
7+
8+
abstract contract VestingWalletCliffConfidentialInitializableMock is VestingWalletCliffConfidentialInitializable {
9+
function initialize(
10+
address executor_,
11+
address beneficiary,
12+
uint64 startTimestamp,
13+
uint64 durationSeconds,
14+
uint64 cliffSeconds
15+
) public override {
16+
super.initialize(executor_, beneficiary, startTimestamp, durationSeconds, cliffSeconds);
17+
18+
FHE.setCoprocessor(ZamaConfig.getSepoliaConfig());
19+
FHE.setDecryptionOracle(ZamaConfig.getSepoliaOracleAddress());
20+
}
21+
}

test/finance/VestingWalletCliffConfidential.test.ts

Lines changed: 113 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,62 +2,126 @@ import { shouldBehaveLikeVestingConfidential } from './VestingWalletConfidential
22
import { FhevmType } from '@fhevm/hardhat-plugin';
33
import { time } from '@nomicfoundation/hardhat-network-helpers';
44
import { expect } from 'chai';
5+
import { EventLog } from 'ethers';
56
import { ethers, fhevm } from 'hardhat';
67

78
const name = 'ConfidentialFungibleToken';
89
const symbol = 'CFT';
910
const uri = 'https://example.com/metadata';
1011

11-
describe('VestingWalletCliffConfidential', function () {
12-
beforeEach(async function () {
13-
const accounts = (await ethers.getSigners()).slice(3);
14-
const [holder, recipient, operator] = accounts;
15-
16-
const token = await ethers.deployContract('$ConfidentialFungibleTokenMock', [name, symbol, uri]);
17-
18-
const encryptedInput = await fhevm
19-
.createEncryptedInput(await token.getAddress(), holder.address)
20-
.add64(1000)
21-
.encrypt();
22-
23-
const currentTime = await time.latest();
24-
const schedule = [currentTime + 60, currentTime + 60 * 121];
25-
const vesting = await ethers.deployContract('$VestingWalletCliffConfidentialMock', [
26-
operator,
27-
recipient,
28-
currentTime + 60,
29-
60 * 60 * 2 /* 2 hours */,
30-
60 * 60 /* 1 hour */,
31-
]);
32-
33-
await (token as any)
34-
.connect(holder)
35-
['$_mint(address,bytes32,bytes)'](vesting.target, encryptedInput.handles[0], encryptedInput.inputProof);
36-
37-
Object.assign(this, { accounts, holder, recipient, operator, token, vesting, schedule, vestingAmount: 1000 });
38-
});
12+
for (const useInitializable of [false, true]) {
13+
describe(`VestingWalletCliffConfidential${useInitializable ? 'Initializable' : ''}`, function () {
14+
beforeEach(async function () {
15+
const accounts = (await ethers.getSigners()).slice(3);
16+
const [holder, recipient, operator] = accounts;
3917

40-
it('should release nothing before cliff', async function () {
41-
await time.increaseTo(this.schedule[0] + 60);
42-
await this.vesting.release(this.token);
18+
const token = await ethers.deployContract('$ConfidentialFungibleTokenMock', [name, symbol, uri]);
4319

44-
const balanceOfHandle = await this.token.balanceOf(this.recipient);
45-
await expect(
46-
fhevm.userDecryptEuint(FhevmType.euint64, balanceOfHandle, this.token.target, this.recipient),
47-
).to.eventually.equal(0);
48-
});
20+
const encryptedInput = await fhevm
21+
.createEncryptedInput(await token.getAddress(), holder.address)
22+
.add64(1000)
23+
.encrypt();
4924

50-
it('should fail construction if cliff is longer than duration', async function () {
51-
await expect(
52-
ethers.deployContract('$VestingWalletCliffConfidentialMock', [
53-
this.operator,
54-
this.recipient,
55-
(await time.latest()) + 60,
56-
60 * 10,
57-
60 * 60,
58-
]),
59-
).to.be.revertedWithCustomError(this.vesting, 'InvalidCliffDuration');
60-
});
25+
const currentTime = await time.latest();
26+
const schedule = [currentTime + 60, currentTime + 60 * 121];
27+
28+
let vesting;
29+
let factory;
30+
let impl;
31+
32+
if (!useInitializable) {
33+
vesting = await ethers.deployContract('$VestingWalletCliffConfidentialMock', [
34+
operator,
35+
recipient,
36+
currentTime + 60,
37+
60 * 60 * 2 /* 2 hours */,
38+
60 * 60 /* 1 hour */,
39+
]);
40+
} else {
41+
impl = await ethers.deployContract('$VestingWalletCliffConfidentialInitializableMock');
42+
factory = await ethers.deployContract('Create2Factory');
43+
44+
const callData = await impl.initialize.populateTransaction(
45+
operator,
46+
recipient,
47+
currentTime + 60,
48+
60 * 60 * 2 /* 2 hours */,
49+
60 * 60 /* 1 hour */,
50+
);
51+
const cloneTx = (await (await factory.create2(impl.target, callData.data)).wait())!;
52+
const cloneAddress = (cloneTx.logs[2] as EventLog).args[0];
53+
54+
vesting = await ethers.getContractAt('$VestingWalletCliffConfidentialInitializableMock', cloneAddress);
55+
}
6156

62-
shouldBehaveLikeVestingConfidential();
63-
});
57+
await (token as any)
58+
.connect(holder)
59+
['$_mint(address,bytes32,bytes)'](vesting.target, encryptedInput.handles[0], encryptedInput.inputProof);
60+
61+
Object.assign(this, {
62+
accounts,
63+
holder,
64+
recipient,
65+
operator,
66+
token,
67+
vesting,
68+
schedule,
69+
vestingAmount: 1000,
70+
factory,
71+
impl,
72+
});
73+
});
74+
75+
it('should release nothing before cliff', async function () {
76+
await time.increaseTo(this.schedule[0] + 60);
77+
await this.vesting.release(this.token);
78+
79+
const balanceOfHandle = await this.token.balanceOf(this.recipient);
80+
await expect(
81+
fhevm.userDecryptEuint(FhevmType.euint64, balanceOfHandle, this.token.target, this.recipient),
82+
).to.eventually.equal(0);
83+
});
84+
85+
it('should fail construction if cliff is longer than duration', async function () {
86+
if (!useInitializable) {
87+
await expect(
88+
ethers.deployContract('$VestingWalletCliffConfidentialMock', [
89+
this.operator,
90+
this.recipient,
91+
(await time.latest()) + 60,
92+
60 * 10,
93+
60 * 60,
94+
]),
95+
).to.be.revertedWithCustomError(this.vesting, 'InvalidCliffDuration');
96+
} else {
97+
const callData = await this.impl.initialize.populateTransaction(
98+
this.operator,
99+
this.recipient,
100+
(await time.latest()) + 60,
101+
60 * 10,
102+
60 * 60,
103+
);
104+
await expect(this.factory.create2(this.impl.target, callData.data)).to.be.revertedWithCustomError(
105+
this.vesting,
106+
'InvalidCliffDuration',
107+
);
108+
}
109+
});
110+
111+
if (useInitializable) {
112+
it('cannot reinitialize', async function () {
113+
await expect(
114+
this.vesting.initialize(
115+
this.operator,
116+
this.recipient,
117+
(await time.latest()) + 60,
118+
60 * 60 * 2 /* 2 hours */,
119+
60 * 60 /* 1 hour */,
120+
),
121+
).to.be.revertedWithCustomError(this.vesting, 'InvalidInitialization');
122+
});
123+
}
124+
125+
shouldBehaveLikeVestingConfidential();
126+
});
127+
}

0 commit comments

Comments
 (0)