diff --git a/CHANGELOG.md b/CHANGELOG.md index f74d9103f78..e727a49e3dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ * `SafeCast`: optimize downcasting of signed integers. ([#3565](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3565)) * `VestingWallet`: remove unused library `Math.sol`. ([#3605](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3605)) * `ECDSA`: Remove redundant check on the `v` value. ([#3591](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3591)) + * `VestingWallet`: add `releasable` getters. ([#3580](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3580)) ### Deprecations diff --git a/contracts/finance/VestingWallet.sol b/contracts/finance/VestingWallet.sol index 60e1eb82030..d855436dc6f 100644 --- a/contracts/finance/VestingWallet.sol +++ b/contracts/finance/VestingWallet.sol @@ -80,16 +80,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); } /** @@ -98,10 +113,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); } /** diff --git a/package-lock.json b/package-lock.json index 07a7d3afc38..d14e4c81ca4 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", @@ -17863,6 +17876,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,