forked from OpenZeppelin/openzeppelin-contracts
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a SplitPayment contract to distribute funds among multiple benefi…
…ciaries
- Loading branch information
Showing
3 changed files
with
259 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
pragma solidity ^0.4.15; | ||
|
||
import '../ReentrancyGuard.sol'; | ||
import '../math/SafeMath.sol'; | ||
|
||
/** | ||
* @title SplitPayment | ||
* @dev Base contract supporting the distribution of funds send to this contract to multiple payees. | ||
*/ | ||
contract SplitPayment is ReentrancyGuard { | ||
using SafeMath for uint256; | ||
|
||
struct Payee { | ||
address addr; | ||
uint256 shares; | ||
} | ||
|
||
uint256 public totalShares = 0; | ||
uint256 public maxPayees = 0; | ||
|
||
mapping(address => uint256) payeeIndex; | ||
Payee[] payees; | ||
|
||
/** | ||
* @dev Constructor | ||
* @param _maxPayees Total number of payees allowed. Zero for no limit. | ||
*/ | ||
function SplitPayment(uint256 _maxPayees) { | ||
maxPayees = _maxPayees; | ||
} | ||
|
||
/** | ||
* @dev Modifier that throws if you want to distribute funds and you are not a payee. | ||
*/ | ||
modifier canDistribute() { | ||
require(isPayee(msg.sender)); | ||
_; | ||
} | ||
|
||
/** | ||
* @dev Modifier that throws if not allowed to update payees. | ||
* Override from child contract with your own requirements for access control. | ||
*/ | ||
modifier canUpdate() { | ||
_; | ||
} | ||
|
||
/** | ||
* @dev Add a new payee to the contract. | ||
* @param _payee The address of the payee to add. | ||
* @param _shares The number of shares owned by the payee. | ||
*/ | ||
function addPayee(address _payee, uint256 _shares) public canUpdate { | ||
require(_payee != address(0)); | ||
require(_shares > 0); | ||
require(!isPayee(_payee)); | ||
require(maxPayees == 0 || payees.length.add(1) <= maxPayees); | ||
|
||
payees.push(Payee(_payee, _shares)); | ||
payeeIndex[_payee] = payees.length; | ||
totalShares = totalShares.add(_shares); | ||
} | ||
|
||
/** | ||
* @dev Add multiple payees to the contract. | ||
* @param _payees An array of addresses of payees to add. | ||
* @param _shares An array of the shares corresponding to each payee in the _payees array. | ||
*/ | ||
function addPayeeMany(address[] _payees, uint256[] _shares) public canUpdate { | ||
require(_payees.length == _shares.length); | ||
require(maxPayees == 0 || payees.length.add(_payees.length) <= maxPayees); | ||
|
||
for (uint256 i = 0; i < _payees.length; i++) { | ||
addPayee(_payees[i], _shares[i]); | ||
} | ||
} | ||
|
||
/** | ||
* @dev Return true if the payee is in the contract. | ||
* @param _payee The address of the payee to check. | ||
*/ | ||
function isPayee(address _payee) public constant returns (bool) { | ||
return payeeIndex[_payee] > 0; | ||
} | ||
|
||
/** | ||
* @dev Return the number of payees in the contract. | ||
*/ | ||
function getPayeeCount() public constant returns (uint256) { | ||
return payees.length; | ||
} | ||
|
||
/** | ||
* @dev Return the address of the payee and its shares. | ||
* Throws if the payee is not in the contract. | ||
* @param _payee The address of the payee to get. | ||
*/ | ||
function getPayee(address _payee) public constant returns (address, uint256) { | ||
require(isPayee(_payee)); | ||
|
||
return getPayeeAtIndex(payeeIndex[_payee] - 1); | ||
} | ||
|
||
/** | ||
* @dev Return the address of the payee and its shares by index. | ||
* Allows iterating through the payee list from a client by knowing the payee count. | ||
* @param _idx The index of the payee in the internal list. | ||
*/ | ||
function getPayeeAtIndex(uint256 _idx) public constant returns (address, uint256) { | ||
require(_idx < payees.length); | ||
|
||
return (payees[_idx].addr, payees[_idx].shares); | ||
} | ||
|
||
/** | ||
* @dev Perform the payment to a payee. | ||
* This can be overriden to provide different transfer mechanisms. | ||
* @param _payee The address of the payee to be paid. | ||
* @param _amount The amount for the payment. | ||
*/ | ||
function pay(address _payee, uint256 _amount) internal { | ||
_payee.transfer(_amount); | ||
} | ||
|
||
/** | ||
* @dev Return the total amount of funds available for distribution. | ||
*/ | ||
function toDistribute() internal returns (uint256) { | ||
return this.balance; | ||
} | ||
|
||
/** | ||
* @dev Send payments to the registered payees according to their shares and the total | ||
* amount of funds to distribute. | ||
*/ | ||
function distributeFunds() public canDistribute nonReentrant { | ||
uint256 amountDistribute = toDistribute(); | ||
assert(amountDistribute > 0); | ||
|
||
Payee memory payee; | ||
for (uint256 i = 0; i < payees.length; i++) { | ||
payee = payees[i]; | ||
|
||
uint256 amount = amountDistribute.mul(payee.shares).div(totalShares); | ||
pay(payee.addr, amount); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
const BigNumber = web3.BigNumber | ||
|
||
const should = require('chai') | ||
.use(require('chai-as-promised')) | ||
.use(require('chai-bignumber')(BigNumber)) | ||
.should() | ||
|
||
const EVMThrow = require('./helpers/EVMThrow.js') | ||
const SplitPaymentMock = artifacts.require('./helpers/SplitPaymentMock.sol') | ||
|
||
contract('SplitPayment', function ([owner, payee1, payee2, payee3, nonpayee1, payer1]) { | ||
const amount = web3.toWei(1.0, 'ether') | ||
|
||
beforeEach(async function () { | ||
this.payees = [payee1, payee2, payee3] | ||
this.shares = [20, 10, 70] | ||
|
||
this.contract = await SplitPaymentMock.new() | ||
await this.contract.addPayeeMany(this.payees, this.shares) | ||
}) | ||
|
||
it('should accept payments', async function () { | ||
await web3.eth.sendTransaction({ from: owner, to: this.contract.address, value: amount }) | ||
|
||
const balance = web3.eth.getBalance(this.contract.address) | ||
balance.should.be.bignumber.equal(amount) | ||
}) | ||
|
||
it('should return if address is payee', async function () { | ||
const isPayee = await this.contract.isPayee.call(payee1) | ||
isPayee.should.equal(true) | ||
}) | ||
|
||
it('should return if address is not payee', async function () { | ||
const isPayee = await this.contract.isPayee.call(nonpayee1) | ||
isPayee.should.equal(false) | ||
}) | ||
|
||
it('should return the correct payee by address', async function () { | ||
const payeeIdx = 0 | ||
const [payee, shares] = await this.contract.getPayee.call(this.payees[payeeIdx]) | ||
payee.should.be.equal(payee1) | ||
shares.should.be.bignumber.equal(this.shares[payeeIdx]) | ||
}) | ||
|
||
it('should return the correct payee by index', async function () { | ||
const payeeIdx = 1 | ||
const [payee, shares] = await this.contract.getPayeeAtIndex.call(payeeIdx) | ||
payee.should.be.equal(payee2) | ||
shares.should.be.bignumber.equal(this.shares[payeeIdx]) | ||
}) | ||
|
||
it('should throw if payees and shares array have different sizes', async function () { | ||
const payees = [payee1, payee2, payee3] | ||
const shares = [50, 50] | ||
await this.contract.addPayeeMany(payees, shares).should.be.rejectedWith(EVMThrow) | ||
}) | ||
|
||
it('should throw if try to add same payee multiple times', async function () { | ||
const payees = [payee1, payee1] | ||
const shares = [50, 50] | ||
await this.contract.addPayeeMany(payees, shares).should.be.rejectedWith(EVMThrow) | ||
}) | ||
|
||
it('should throw if try to add payee with zero shares', async function () { | ||
await this.contract.addPayee(nonpayee1, 0).should.be.rejectedWith(EVMThrow) | ||
}) | ||
|
||
it('should throw if no funds to distribute', async function () { | ||
await this.contract.distributeFunds({from: payee1}).should.be.rejectedWith(EVMThrow) | ||
}) | ||
|
||
it('should distribute funds to payees', async function () { | ||
await web3.eth.sendTransaction({from: payer1, to: this.contract.address, value: amount}) | ||
|
||
const initBalance = web3.eth.getBalance(this.contract.address) | ||
initBalance.should.be.bignumber.equal(amount) | ||
|
||
const initAmount1 = web3.eth.getBalance(payee1) | ||
const initAmount2 = web3.eth.getBalance(payee2) | ||
const initAmount3 = web3.eth.getBalance(payee3) | ||
|
||
await this.contract.distributeFunds({from: payee1}) | ||
|
||
// Contract should have zero balance after distribution | ||
const afterBalance = web3.eth.getBalance(this.contract.address) | ||
afterBalance.should.be.bignumber.equal(0) | ||
|
||
const profit1 = web3.eth.getBalance(payee1) - initAmount1 | ||
const profit2 = web3.eth.getBalance(payee2) - initAmount2 | ||
const profit3 = web3.eth.getBalance(payee3) - initAmount3 | ||
|
||
assert(Math.abs(profit1 - web3.toWei(0.20, 'ether')) < 1e16); | ||
assert(Math.abs(profit2 - web3.toWei(0.10, 'ether')) < 1e16); | ||
assert(Math.abs(profit3 - web3.toWei(0.70, 'ether')) < 1e16); | ||
}) | ||
|
||
it('should throw if non-payee want to distribute funds', async function () { | ||
await web3.eth.sendTransaction({from: payer1, to: this.contract.address, value: amount}) | ||
await this.contract.distributeFunds({from: nonpayee1}).should.be.rejectedWith(EVMThrow) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
pragma solidity ^0.4.15; | ||
|
||
import '../../contracts/payment/SplitPayment.sol'; | ||
|
||
// mock class using SplitPayment | ||
contract SplitPaymentMock is SplitPayment { | ||
function SplitPaymentMock() SplitPayment(0) payable { } | ||
function () payable {} | ||
} |