diff --git a/CHANGELOG.md b/CHANGELOG.md index 79b86c06763..985bebf8887 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ * `MerkleProof`: add `multiProofVerify` to prove multiple values are part of a Merkle tree. ([#3276](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3276)) * `ERC721`, `ERC1155`: simplified revert reasons. ([#3254](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3254)) * `ERC721`: removed redundant require statement. ([#3434](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3434)) + * `PaymentSplitter`: add `releasable` getters. ([#3350](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3350)) ## 4.6.0 (2022-04-26) diff --git a/contracts/finance/PaymentSplitter.sol b/contracts/finance/PaymentSplitter.sol index 94d2ab827c5..69786e3979c 100644 --- a/contracts/finance/PaymentSplitter.sol +++ b/contracts/finance/PaymentSplitter.sol @@ -120,6 +120,23 @@ contract PaymentSplitter is Context { return _payees[index]; } + /** + * @dev Getter for the amount of payee's releasable Ether. + */ + function releasable(address account) public view returns (uint256) { + uint256 totalReceived = address(this).balance + totalReleased(); + return _pendingPayment(account, totalReceived, released(account)); + } + + /** + * @dev Getter for the amount of payee's releasable `token` tokens. `token` should be the address of an + * IERC20 contract. + */ + function releasable(IERC20 token, address account) public view returns (uint256) { + uint256 totalReceived = token.balanceOf(address(this)) + totalReleased(token); + return _pendingPayment(account, totalReceived, released(token, account)); + } + /** * @dev Triggers a transfer to `account` of the amount of Ether they are owed, according to their percentage of the * total shares and their previous withdrawals. @@ -127,8 +144,7 @@ contract PaymentSplitter is Context { function release(address payable account) public virtual { require(_shares[account] > 0, "PaymentSplitter: account has no shares"); - uint256 totalReceived = address(this).balance + totalReleased(); - uint256 payment = _pendingPayment(account, totalReceived, released(account)); + uint256 payment = releasable(account); require(payment != 0, "PaymentSplitter: account is not due payment"); @@ -147,8 +163,7 @@ contract PaymentSplitter is Context { function release(IERC20 token, address account) public virtual { require(_shares[account] > 0, "PaymentSplitter: account has no shares"); - uint256 totalReceived = token.balanceOf(address(this)) + totalReleased(token); - uint256 payment = _pendingPayment(account, totalReceived, released(token, account)); + uint256 payment = releasable(token, account); require(payment != 0, "PaymentSplitter: account is not due payment"); diff --git a/test/finance/PaymentSplitter.test.js b/test/finance/PaymentSplitter.test.js index fdb81f6104e..2fa7a26fa82 100644 --- a/test/finance/PaymentSplitter.test.js +++ b/test/finance/PaymentSplitter.test.js @@ -62,10 +62,11 @@ contract('PaymentSplitter', function (accounts) { await Promise.all(this.payees.map(async (payee, index) => { expect(await this.contract.payee(index)).to.equal(payee); expect(await this.contract.released(payee)).to.be.bignumber.equal('0'); + expect(await this.contract.releasable(payee)).to.be.bignumber.equal('0'); })); }); - describe('accepts payments', async function () { + describe('accepts payments', function () { it('Ether', async function () { await send.ether(owner, this.contract.address, amount); @@ -79,7 +80,7 @@ contract('PaymentSplitter', function (accounts) { }); }); - describe('shares', async function () { + describe('shares', function () { it('stores shares if address is payee', async function () { expect(await this.contract.shares(payee1)).to.be.bignumber.not.equal('0'); }); @@ -89,8 +90,8 @@ contract('PaymentSplitter', function (accounts) { }); }); - describe('release', async function () { - describe('Ether', async function () { + describe('release', function () { + describe('Ether', function () { it('reverts if no funds to claim', async function () { await expectRevert(this.contract.release(payee1), 'PaymentSplitter: account is not due payment', @@ -104,7 +105,7 @@ contract('PaymentSplitter', function (accounts) { }); }); - describe('Token', async function () { + describe('Token', function () { it('reverts if no funds to claim', async function () { await expectRevert(this.contract.release(this.token.address, payee1), 'PaymentSplitter: account is not due payment', @@ -119,7 +120,27 @@ contract('PaymentSplitter', function (accounts) { }); }); - describe('distributes funds to payees', async function () { + describe('tracks releasable and released', function () { + it('Ether', async function () { + await send.ether(payer1, this.contract.address, amount); + const payment = amount.divn(10); + expect(await this.contract.releasable(payee2)).to.be.bignumber.equal(payment); + await this.contract.release(payee2); + expect(await this.contract.releasable(payee2)).to.be.bignumber.equal('0'); + expect(await this.contract.released(payee2)).to.be.bignumber.equal(payment); + }); + + it('Token', async function () { + await this.token.transfer(this.contract.address, amount, { from: owner }); + const payment = amount.divn(10); + expect(await this.contract.releasable(this.token.address, payee2, {})).to.be.bignumber.equal(payment); + await this.contract.release(this.token.address, payee2); + expect(await this.contract.releasable(this.token.address, payee2, {})).to.be.bignumber.equal('0'); + expect(await this.contract.released(this.token.address, payee2)).to.be.bignumber.equal(payment); + }); + }); + + describe('distributes funds to payees', function () { it('Ether', async function () { await send.ether(payer1, this.contract.address, amount);