From 356ede7a43a0b0d37b3c6b7bf617a38c5f85de21 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 25 Jul 2022 21:48:22 +0200 Subject: [PATCH 1/3] Add releaseable function to VestingWallet --- contracts/finance/VestingWallet.sol | 31 +++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/contracts/finance/VestingWallet.sol b/contracts/finance/VestingWallet.sol index ebb5ee50fc5..78c16e18ea8 100644 --- a/contracts/finance/VestingWallet.sol +++ b/contracts/finance/VestingWallet.sol @@ -81,16 +81,31 @@ contract VestingWallet is Context { return _erc20Released[token]; } + /** + * @dev Getter for the amount of releasable eth. + */ + function releasable() public view virtual returns (uint256) { + return vestedAmount(uint64(block.timestamp)) - released(); + } + + /** + * @dev Getter for the amount of releasable `token` tokens. `token` should be the address of an + * IERC20 contract. + */ + function releasable(address token) public view virtual returns (uint256) { + return vestedAmount(token, uint64(block.timestamp)) - released(token); + } + /** * @dev Release the native token (ether) that have already vested. * * Emits a {EtherReleased} event. */ function release() public virtual { - uint256 releasable = vestedAmount(uint64(block.timestamp)) - released(); - _released += releasable; - emit EtherReleased(releasable); - Address.sendValue(payable(beneficiary()), releasable); + uint256 amount = releasable(); + _released += amount; + emit EtherReleased(amount); + Address.sendValue(payable(beneficiary()), amount); } /** @@ -99,10 +114,10 @@ contract VestingWallet is Context { * Emits a {ERC20Released} event. */ function release(address token) public virtual { - uint256 releasable = vestedAmount(token, uint64(block.timestamp)) - released(token); - _erc20Released[token] += releasable; - emit ERC20Released(token, releasable); - SafeERC20.safeTransfer(IERC20(token), beneficiary(), releasable); + uint256 amount = releasable(token); + _erc20Released[token] += amount; + emit ERC20Released(token, amount); + SafeERC20.safeTransfer(IERC20(token), beneficiary(), amount); } /** From e5d5d0f5b152e42206217959caa2b0210f56b3ab Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 25 Jul 2022 21:50:30 +0200 Subject: [PATCH 2/3] Add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1200341c93..277ca74b8b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ * `ReentrancyGuard`: Reduce code size impact of the modifier by using internal functions. ([#3515](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3515)) * `SafeCast`: optimize downcasting of signed integers. ([#3565](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3565)) * `ERC20FlashMint`: add an internal `_flashFee` function for overriding. ([#3551](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3551)) + * `VestingWallet`: add `releasable` getters. ([#3580](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3580)) ### Compatibility Note From 1c10cfc39a1f2444ca7ee39102f8b88a07dcc1d4 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 26 Jul 2022 16:36:46 +0200 Subject: [PATCH 3/3] testing --- package-lock.json | 22 +++++++++++++++++++ package.json | 1 + test/finance/VestingWallet.behavior.js | 30 ++++++++++++++------------ 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 49792f95323..d8c84d91060 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "openzeppelin-contracts-migrate-imports": "scripts/migrate-imports.js" }, "devDependencies": { + "@nomicfoundation/hardhat-network-helpers": "^1.0.3", "@nomiclabs/hardhat-truffle5": "^2.0.5", "@nomiclabs/hardhat-web3": "^2.0.0", "@openzeppelin/docs-utils": "^0.1.0", @@ -1229,6 +1230,18 @@ "node": ">= 8" } }, + "node_modules/@nomicfoundation/hardhat-network-helpers": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-1.0.3.tgz", + "integrity": "sha512-sBeUCzgrcdS8x7Nnr4t6wNpC7GIMMR3gQs8lVaxff5VWVKf7SjdkJ2rvSJsS2ckD8/8KGxeDTLb7XCOqVAjFjA==", + "dev": true, + "dependencies": { + "ethereumjs-util": "^7.1.4" + }, + "peerDependencies": { + "hardhat": "^2.0.0" + } + }, "node_modules/@nomiclabs/hardhat-truffle5": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@nomiclabs/hardhat-truffle5/-/hardhat-truffle5-2.0.6.tgz", @@ -19119,6 +19132,15 @@ "fastq": "^1.6.0" } }, + "@nomicfoundation/hardhat-network-helpers": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-1.0.3.tgz", + "integrity": "sha512-sBeUCzgrcdS8x7Nnr4t6wNpC7GIMMR3gQs8lVaxff5VWVKf7SjdkJ2rvSJsS2ckD8/8KGxeDTLb7XCOqVAjFjA==", + "dev": true, + "requires": { + "ethereumjs-util": "^7.1.4" + } + }, "@nomiclabs/hardhat-truffle5": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@nomiclabs/hardhat-truffle5/-/hardhat-truffle5-2.0.6.tgz", diff --git a/package.json b/package.json index b85308eaf27..7e0d14c40da 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ }, "homepage": "https://openzeppelin.com/contracts/", "devDependencies": { + "@nomicfoundation/hardhat-network-helpers": "^1.0.3", "@nomiclabs/hardhat-truffle5": "^2.0.5", "@nomiclabs/hardhat-web3": "^2.0.0", "@openzeppelin/docs-utils": "^0.1.0", diff --git a/test/finance/VestingWallet.behavior.js b/test/finance/VestingWallet.behavior.js index 0f07e5f459d..d1d2fbf4a20 100644 --- a/test/finance/VestingWallet.behavior.js +++ b/test/finance/VestingWallet.behavior.js @@ -1,3 +1,4 @@ +const { time } = require('@nomicfoundation/hardhat-network-helpers'); const { expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); @@ -9,18 +10,24 @@ function releasedEvent (token, amount) { function shouldBehaveLikeVesting (beneficiary) { it('check vesting schedule', async function () { - const [ method, ...args ] = this.token - ? [ 'vestedAmount(address,uint64)', this.token.address ] - : [ 'vestedAmount(uint64)' ]; + const [ fnVestedAmount, fnReleasable, ...args ] = this.token + ? [ 'vestedAmount(address,uint64)', 'releasable(address)', this.token.address ] + : [ 'vestedAmount(uint64)', 'releasable()' ]; for (const timestamp of this.schedule) { - expect(await this.mock.methods[method](...args, timestamp)) - .to.be.bignumber.equal(this.vestingFn(timestamp)); + await time.increaseTo(timestamp); + const vesting = this.vestingFn(timestamp); + + expect(await this.mock.methods[fnVestedAmount](...args, timestamp)) + .to.be.bignumber.equal(vesting); + + expect(await this.mock.methods[fnReleasable](...args)) + .to.be.bignumber.equal(vesting); } }); it('execute vesting schedule', async function () { - const [ method, ...args ] = this.token + const [ fnRelease, ...args ] = this.token ? [ 'release(address)', this.token.address ] : [ 'release()' ]; @@ -28,7 +35,7 @@ function shouldBehaveLikeVesting (beneficiary) { const before = await this.getBalance(beneficiary); { - const receipt = await this.mock.methods[method](...args); + const receipt = await this.mock.methods[fnRelease](...args); await expectEvent.inTransaction( receipt.tx, @@ -42,15 +49,10 @@ function shouldBehaveLikeVesting (beneficiary) { } for (const timestamp of this.schedule) { + await time.setNextBlockTimestamp(timestamp); const vested = this.vestingFn(timestamp); - await new Promise(resolve => web3.currentProvider.send({ - method: 'evm_setNextBlockTimestamp', - params: [ timestamp.toNumber() ], - }, resolve)); - - const receipt = await this.mock.methods[method](...args); - + const receipt = await this.mock.methods[fnRelease](...args); await expectEvent.inTransaction( receipt.tx, this.mock,