-
Notifications
You must be signed in to change notification settings - Fork 12.4k
Add VestingWalletWithCliff #4870
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
a01c23e
5018549
a5d1a08
3a78490
0d72a4d
ef2717d
0cc39d8
378d26e
d4367bb
7d1b418
adf3716
91e4a12
89d52e7
fa112ed
11fb45b
47ddb0d
188d86e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| 'openzeppelin-solidity': minor | ||
| --- | ||
|
|
||
| `VestinWalletWithCliff`: Add a extension of the `VestingWallet` contract with an added cliff. | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,46 @@ | ||||||
| // SPDX-License-Identifier: MIT | ||||||
|
|
||||||
| pragma solidity ^0.8.20; | ||||||
|
|
||||||
| import {SafeCast} from "../utils/math/SafeCast.sol"; | ||||||
| import {VestingWallet} from "./VestingWallet.sol"; | ||||||
|
|
||||||
| /** | ||||||
| * @dev Extension of {VestingWallet} that adds a cliff to the vesting schedule. | ||||||
| */ | ||||||
| abstract contract VestingWalletWithCliff is VestingWallet { | ||||||
Amxx marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||
| using SafeCast for *; | ||||||
|
|
||||||
| uint64 private immutable _cliff; | ||||||
|
|
||||||
| error InvalidCliffDuration(uint64 cliffSeconds, uint64 durationSeconds); | ||||||
Amxx marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
|
||||||
| /** | ||||||
| * @dev Sets the sender as the initial owner, the beneficiary as the pending owner, the start timestamp, the | ||||||
| * vesting duration and the duration of the cliff of the vesting wallet. | ||||||
| */ | ||||||
| constructor(uint64 cliffSeconds) { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see We can:
Leaning towards number 1:
Suggested change
|
||||||
| if (cliffSeconds > duration()) { | ||||||
ernestognw marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| revert InvalidCliffDuration(cliffSeconds, duration().toUint64()); | ||||||
| } | ||||||
| _cliff = start().toUint64() + cliffSeconds; | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * @dev Getter for the cliff timestamp. | ||||||
| */ | ||||||
| function cliff() public view virtual returns (uint256) { | ||||||
| return _cliff; | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * @dev Virtual implementation of the vesting formula. This returns the amount vested, as a function of time, for | ||||||
| * an asset given its total historical allocation. | ||||||
| */ | ||||||
| function _vestingSchedule( | ||||||
| uint256 totalAllocation, | ||||||
| uint64 timestamp | ||||||
| ) internal view virtual override returns (uint256) { | ||||||
| return timestamp < cliff() ? 0 : super._vestingSchedule(totalAllocation, timestamp); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not always calling In this case it harmless, so it doesn't make sense to call super every time since the ternary operator is extending the original branching in function _vestingSchedule(uint256 totalAllocation, uint64 timestamp) internal view virtual returns (uint256) {
if (timestamp < cliff() || timestamp < start()) {
return 0;
} else if (timestamp >= end()) {
return totalAllocation;
} else {
return (totalAllocation * (timestamp - start())) / duration();
}
}I think it doesn't make sense to call I don't have a strong opinion. But I'm leaning towards not calling
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think not caller super at all is dangerous. Lets discuss that next week. |
||||||
| } | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| const { ethers } = require('hardhat'); | ||
| const { expect } = require('chai'); | ||
| const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); | ||
|
|
||
| const { min } = require('../helpers/math'); | ||
| const time = require('../helpers/time'); | ||
|
|
||
| const { shouldBehaveLikeVesting } = require('./VestingWallet.behavior'); | ||
|
|
||
| async function fixture() { | ||
| const amount = ethers.parseEther('100'); | ||
| const duration = time.duration.years(4); | ||
| const start = (await time.clock.timestamp()) + time.duration.hours(1); | ||
| const cliffDuration = time.duration.years(1); | ||
| const cliff = start + cliffDuration; | ||
|
|
||
| const [sender, beneficiary] = await ethers.getSigners(); | ||
| const mock = await ethers.deployContract('$VestingWalletWithCliff', [beneficiary, start, duration, cliffDuration]); | ||
|
|
||
| const token = await ethers.deployContract('$ERC20', ['Name', 'Symbol']); | ||
| await token.$_mint(mock, amount); | ||
| await sender.sendTransaction({ to: mock, value: amount }); | ||
|
|
||
| const pausableToken = await ethers.deployContract('$ERC20Pausable', ['Name', 'Symbol']); | ||
| const beneficiaryMock = await ethers.deployContract('EtherReceiverMock'); | ||
|
|
||
| const env = { | ||
| eth: { | ||
| checkRelease: async (tx, amount) => { | ||
| await expect(tx).to.emit(mock, 'EtherReleased').withArgs(amount); | ||
| await expect(tx).to.changeEtherBalances([mock, beneficiary], [-amount, amount]); | ||
| }, | ||
| setupFailure: async () => { | ||
| await beneficiaryMock.setAcceptEther(false); | ||
| await mock.connect(beneficiary).transferOwnership(beneficiaryMock); | ||
| return { args: [], error: [mock, 'FailedInnerCall'] }; | ||
| }, | ||
| releasedEvent: 'EtherReleased', | ||
| argsVerify: [], | ||
| args: [], | ||
| }, | ||
| token: { | ||
| checkRelease: async (tx, amount) => { | ||
| await expect(tx).to.emit(token, 'Transfer').withArgs(mock, beneficiary, amount); | ||
| await expect(tx).to.changeTokenBalances(token, [mock, beneficiary], [-amount, amount]); | ||
| }, | ||
| setupFailure: async () => { | ||
| await pausableToken.$_pause(); | ||
| return { | ||
| args: [ethers.Typed.address(pausableToken)], | ||
| error: [pausableToken, 'EnforcedPause'], | ||
| }; | ||
| }, | ||
| releasedEvent: 'ERC20Released', | ||
| argsVerify: [token], | ||
| args: [ethers.Typed.address(token)], | ||
| }, | ||
| }; | ||
|
|
||
| const schedule = Array(64) | ||
| .fill() | ||
| .map((_, i) => (BigInt(i) * duration) / 60n + start); | ||
|
|
||
| const vestingFn = timestamp => min(amount, timestamp < cliff ? 0n : (amount * (timestamp - start)) / duration); | ||
|
|
||
| return { mock, duration, start, beneficiary, cliff, schedule, vestingFn, env }; | ||
| } | ||
|
|
||
| describe('VestingWallet', function () { | ||
| beforeEach(async function () { | ||
| Object.assign(this, await loadFixture(fixture)); | ||
| }); | ||
|
|
||
| it('rejects zero address for beneficiary', async function () { | ||
Amxx marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| await expect( | ||
| ethers.deployContract('$VestingWalletWithCliff', [ | ||
| this.beneficiary, | ||
| this.start, | ||
| this.duration, | ||
| this.duration + 1n, | ||
| ]), | ||
| ) | ||
| .revertedWithCustomError(this.mock, 'InvalidCliffDuration') | ||
| .withArgs(this.duration + 1n, this.duration); | ||
| }); | ||
|
|
||
| it('check vesting contract', async function () { | ||
| expect(await this.mock.owner()).to.equal(this.beneficiary); | ||
| expect(await this.mock.start()).to.equal(this.start); | ||
| expect(await this.mock.duration()).to.equal(this.duration); | ||
| expect(await this.mock.end()).to.equal(this.start + this.duration); | ||
| expect(await this.mock.cliff()).to.equal(this.cliff); | ||
| }); | ||
|
|
||
| describe('vesting schedule', function () { | ||
| describe('Eth vesting', function () { | ||
| beforeEach(async function () { | ||
| Object.assign(this, this.env.eth); | ||
| }); | ||
|
|
||
| shouldBehaveLikeVesting(); | ||
| }); | ||
|
|
||
| describe('ERC20 vesting', function () { | ||
| beforeEach(async function () { | ||
| Object.assign(this, this.env.token); | ||
| }); | ||
|
|
||
| shouldBehaveLikeVesting(); | ||
| }); | ||
| }); | ||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.