瀏覽代碼

Add getter for number of releasable tokens in VestingWallet (#3580)

Co-authored-by: Francisco <frangio.1@gmail.com>
Hadrien Croubois 3 年之前
父節點
當前提交
c797195f1d
共有 5 個文件被更改,包括 63 次插入22 次删除
  1. 1 0
      CHANGELOG.md
  2. 23 8
      contracts/finance/VestingWallet.sol
  3. 22 0
      package-lock.json
  4. 1 0
      package.json
  5. 16 14
      test/finance/VestingWallet.behavior.js

+ 1 - 0
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
 

+ 23 - 8
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);
     }
 
     /**

+ 22 - 0
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",

+ 1 - 0
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",

+ 16 - 14
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,