From 39fe05dfad99e95b22c2c355ac142614f7d1a45d Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Fri, 18 May 2018 20:54:43 -0300 Subject: [PATCH] add more openzeppelin-solidity library contracts --- contracts/ECRecovery.sol | 76 +++++++++++++++ contracts/MerkleProof.sol | 35 +++++++ contracts/math/Math.sol | 24 +++++ contracts/mocks/ECRecoveryMock.sol | 25 +++++ contracts/mocks/MathMock.sol | 26 +++++ contracts/mocks/MerkleProofWrapper.sol | 11 +++ contracts/mocks/PullPaymentMock.sol | 17 ++++ contracts/mocks/RBACMock.sol | 69 +++++++++++++ contracts/ownership/rbac/RBAC.sol | 108 +++++++++++++++++++++ contracts/ownership/rbac/RBACWithAdmin.sol | 60 ++++++++++++ contracts/ownership/rbac/Roles.sol | 55 +++++++++++ contracts/payment/PullPayment.sol | 43 ++++++++ test/library/ECRecovery.test.js | 77 +++++++++++++++ test/library/Math.test.js | 43 ++++++++ test/library/MerkleProof.test.js | 61 ++++++++++++ test/ownership/rbac/RBAC.test.js | 98 +++++++++++++++++++ test/payment/PullPayment.test.js | 71 ++++++++++++++ 17 files changed, 899 insertions(+) create mode 100644 contracts/ECRecovery.sol create mode 100644 contracts/MerkleProof.sol create mode 100644 contracts/math/Math.sol create mode 100644 contracts/mocks/ECRecoveryMock.sol create mode 100644 contracts/mocks/MathMock.sol create mode 100644 contracts/mocks/MerkleProofWrapper.sol create mode 100644 contracts/mocks/PullPaymentMock.sol create mode 100644 contracts/mocks/RBACMock.sol create mode 100644 contracts/ownership/rbac/RBAC.sol create mode 100644 contracts/ownership/rbac/RBACWithAdmin.sol create mode 100644 contracts/ownership/rbac/Roles.sol create mode 100644 contracts/payment/PullPayment.sol create mode 100644 test/library/ECRecovery.test.js create mode 100644 test/library/Math.test.js create mode 100644 test/library/MerkleProof.test.js create mode 100644 test/ownership/rbac/RBAC.test.js create mode 100644 test/payment/PullPayment.test.js diff --git a/contracts/ECRecovery.sol b/contracts/ECRecovery.sol new file mode 100644 index 000000000..b2076f0e3 --- /dev/null +++ b/contracts/ECRecovery.sol @@ -0,0 +1,76 @@ +pragma solidity ^0.4.21; + + +/** + * @title Eliptic curve signature operations + * + * @dev Based on https://gist.github.com/axic/5b33912c6f61ae6fd96d6c4a47afde6d + * + * TODO Remove this library once solidity supports passing a signature to ecrecover. + * See https://github.com/ethereum/solidity/issues/864 + * + */ + +library ECRecovery { + + /** + * @dev Recover signer address from a message by using their signature + * @param hash bytes32 message, the hash is the signed message. What is recovered is the signer address. + * @param sig bytes signature, the signature is generated using web3.eth.sign() + */ + function recover(bytes32 hash, bytes sig) + internal + pure + returns (address) + { + bytes32 r; + bytes32 s; + uint8 v; + + // Check the signature length + if (sig.length != 65) { + return (address(0)); + } + + // Divide the signature in r, s and v variables + // ecrecover takes the signature parameters, and the only way to get them + // currently is to use assembly. + // solium-disable-next-line security/no-inline-assembly + assembly { + r := mload(add(sig, 32)) + s := mload(add(sig, 64)) + v := byte(0, mload(add(sig, 96))) + } + + // Version of signature should be 27 or 28, but 0 and 1 are also possible versions + if (v < 27) { + v += 27; + } + + // If the version is correct return the signer address + if (v != 27 && v != 28) { + return (address(0)); + } else { + // solium-disable-next-line arg-overflow + return ecrecover(hash, v, r, s); + } + } + + /** + * toEthSignedMessageHash + * @dev prefix a bytes32 value with "\x19Ethereum Signed Message:" + * @dev and hash the result + */ + function toEthSignedMessageHash(bytes32 hash) + internal + pure + returns (bytes32) + { + // 32 is the length in bytes of hash, + // enforced by the type signature above + return keccak256( + "\x19Ethereum Signed Message:\n32", + hash + ); + } +} diff --git a/contracts/MerkleProof.sol b/contracts/MerkleProof.sol new file mode 100644 index 000000000..a7000c6f2 --- /dev/null +++ b/contracts/MerkleProof.sol @@ -0,0 +1,35 @@ +pragma solidity ^0.4.21; + + +/* + * @title MerkleProof + * @dev Merkle proof verification + * @note Based on https://github.com/ameensol/merkle-tree-solidity/blob/master/src/MerkleProof.sol + */ +library MerkleProof { + /* + * @dev Verifies a Merkle proof proving the existence of a leaf in a Merkle tree. Assumes that each pair of leaves + * and each pair of pre-images is sorted. + * @param _proof Merkle proof containing sibling hashes on the branch from the leaf to the root of the Merkle tree + * @param _root Merkle root + * @param _leaf Leaf of Merkle tree + */ + function verifyProof(bytes32[] _proof, bytes32 _root, bytes32 _leaf) internal pure returns (bool) { + bytes32 computedHash = _leaf; + + for (uint256 i = 0; i < _proof.length; i++) { + bytes32 proofElement = _proof[i]; + + if (computedHash < proofElement) { + // Hash(current computed hash + current element of the proof) + computedHash = keccak256(computedHash, proofElement); + } else { + // Hash(current element of the proof + current computed hash) + computedHash = keccak256(proofElement, computedHash); + } + } + + // Check if the computed hash (root) is equal to the provided root + return computedHash == _root; + } +} diff --git a/contracts/math/Math.sol b/contracts/math/Math.sol new file mode 100644 index 000000000..0532ecb30 --- /dev/null +++ b/contracts/math/Math.sol @@ -0,0 +1,24 @@ +pragma solidity ^0.4.21; + + +/** + * @title Math + * @dev Assorted math operations + */ +library Math { + function max64(uint64 a, uint64 b) internal pure returns (uint64) { + return a >= b ? a : b; + } + + function min64(uint64 a, uint64 b) internal pure returns (uint64) { + return a < b ? a : b; + } + + function max256(uint256 a, uint256 b) internal pure returns (uint256) { + return a >= b ? a : b; + } + + function min256(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } +} diff --git a/contracts/mocks/ECRecoveryMock.sol b/contracts/mocks/ECRecoveryMock.sol new file mode 100644 index 000000000..091055161 --- /dev/null +++ b/contracts/mocks/ECRecoveryMock.sol @@ -0,0 +1,25 @@ +pragma solidity ^0.4.21; + + +import "../ECRecovery.sol"; + + +contract ECRecoveryMock { + using ECRecovery for bytes32; + + function recover(bytes32 hash, bytes sig) + public + pure + returns (address) + { + return hash.recover(sig); + } + + function toEthSignedMessageHash(bytes32 hash) + public + pure + returns (bytes32) + { + return hash.toEthSignedMessageHash(); + } +} diff --git a/contracts/mocks/MathMock.sol b/contracts/mocks/MathMock.sol new file mode 100644 index 000000000..c2c9f3fba --- /dev/null +++ b/contracts/mocks/MathMock.sol @@ -0,0 +1,26 @@ +pragma solidity ^0.4.21; + + +import "../../contracts/math/Math.sol"; + + +contract MathMock { + uint64 public result64; + uint256 public result256; + + function max64(uint64 a, uint64 b) public { + result64 = Math.max64(a, b); + } + + function min64(uint64 a, uint64 b) public { + result64 = Math.min64(a, b); + } + + function max256(uint256 a, uint256 b) public { + result256 = Math.max256(a, b); + } + + function min256(uint256 a, uint256 b) public { + result256 = Math.min256(a, b); + } +} diff --git a/contracts/mocks/MerkleProofWrapper.sol b/contracts/mocks/MerkleProofWrapper.sol new file mode 100644 index 000000000..aa830e63a --- /dev/null +++ b/contracts/mocks/MerkleProofWrapper.sol @@ -0,0 +1,11 @@ +pragma solidity ^0.4.21; + +import { MerkleProof } from "../MerkleProof.sol"; + + +contract MerkleProofWrapper { + + function verifyProof(bytes32[] _proof, bytes32 _root, bytes32 _leaf) public pure returns (bool) { + return MerkleProof.verifyProof(_proof, _root, _leaf); + } +} diff --git a/contracts/mocks/PullPaymentMock.sol b/contracts/mocks/PullPaymentMock.sol new file mode 100644 index 000000000..fb4a1231b --- /dev/null +++ b/contracts/mocks/PullPaymentMock.sol @@ -0,0 +1,17 @@ +pragma solidity ^0.4.21; + + +import "../payment/PullPayment.sol"; + + +// mock class using PullPayment +contract PullPaymentMock is PullPayment { + + function PullPaymentMock() public payable { } + + // test helper function to call asyncSend + function callSend(address dest, uint256 amount) public { + asyncSend(dest, amount); + } + +} diff --git a/contracts/mocks/RBACMock.sol b/contracts/mocks/RBACMock.sol new file mode 100644 index 000000000..7dfa2a985 --- /dev/null +++ b/contracts/mocks/RBACMock.sol @@ -0,0 +1,69 @@ +pragma solidity ^0.4.21; + +import "../ownership/rbac/RBACWithAdmin.sol"; + + +contract RBACMock is RBACWithAdmin { + + string constant ROLE_ADVISOR = "advisor"; + + modifier onlyAdminOrAdvisor() + { + require( + hasRole(msg.sender, ROLE_ADMIN) || + hasRole(msg.sender, ROLE_ADVISOR) + ); + _; + } + + function RBACMock(address[] _advisors) + public + { + addRole(msg.sender, ROLE_ADVISOR); + + for (uint256 i = 0; i < _advisors.length; i++) { + addRole(_advisors[i], ROLE_ADVISOR); + } + } + + function onlyAdminsCanDoThis() + onlyAdmin + view + external + { + } + + function onlyAdvisorsCanDoThis() + onlyRole(ROLE_ADVISOR) + view + external + { + } + + function eitherAdminOrAdvisorCanDoThis() + onlyAdminOrAdvisor + view + external + { + } + + function nobodyCanDoThis() + onlyRole("unknown") + view + external + { + } + + // admins can remove advisor's role + function removeAdvisor(address _addr) + onlyAdmin + public + { + // revert if the user isn't an advisor + // (perhaps you want to soft-fail here instead?) + checkRole(_addr, ROLE_ADVISOR); + + // remove the advisor's role + removeRole(_addr, ROLE_ADVISOR); + } +} diff --git a/contracts/ownership/rbac/RBAC.sol b/contracts/ownership/rbac/RBAC.sol new file mode 100644 index 000000000..998d334f8 --- /dev/null +++ b/contracts/ownership/rbac/RBAC.sol @@ -0,0 +1,108 @@ +pragma solidity ^0.4.21; + +import "./Roles.sol"; + + +/** + * @title RBAC (Role-Based Access Control) + * @author Matt Condon (@Shrugs) + * @dev Stores and provides setters and getters for roles and addresses. + * @dev Supports unlimited numbers of roles and addresses. + * @dev See //contracts/mocks/RBACMock.sol for an example of usage. + * This RBAC method uses strings to key roles. It may be beneficial + * for you to write your own implementation of this interface using Enums or similar. + * It's also recommended that you define constants in the contract, like ROLE_ADMIN below, + * to avoid typos. + */ +contract RBAC { + using Roles for Roles.Role; + + mapping (string => Roles.Role) private roles; + + event RoleAdded(address addr, string roleName); + event RoleRemoved(address addr, string roleName); + + /** + * @dev reverts if addr does not have role + * @param addr address + * @param roleName the name of the role + * // reverts + */ + function checkRole(address addr, string roleName) + view + public + { + roles[roleName].check(addr); + } + + /** + * @dev determine if addr has role + * @param addr address + * @param roleName the name of the role + * @return bool + */ + function hasRole(address addr, string roleName) + view + public + returns (bool) + { + return roles[roleName].has(addr); + } + + /** + * @dev add a role to an address + * @param addr address + * @param roleName the name of the role + */ + function addRole(address addr, string roleName) + internal + { + roles[roleName].add(addr); + emit RoleAdded(addr, roleName); + } + + /** + * @dev remove a role from an address + * @param addr address + * @param roleName the name of the role + */ + function removeRole(address addr, string roleName) + internal + { + roles[roleName].remove(addr); + emit RoleRemoved(addr, roleName); + } + + /** + * @dev modifier to scope access to a single role (uses msg.sender as addr) + * @param roleName the name of the role + * // reverts + */ + modifier onlyRole(string roleName) + { + checkRole(msg.sender, roleName); + _; + } + + /** + * @dev modifier to scope access to a set of roles (uses msg.sender as addr) + * @param roleNames the names of the roles to scope access to + * // reverts + * + * @TODO - when solidity supports dynamic arrays as arguments to modifiers, provide this + * see: https://github.com/ethereum/solidity/issues/2467 + */ + // modifier onlyRoles(string[] roleNames) { + // bool hasAnyRole = false; + // for (uint8 i = 0; i < roleNames.length; i++) { + // if (hasRole(msg.sender, roleNames[i])) { + // hasAnyRole = true; + // break; + // } + // } + + // require(hasAnyRole); + + // _; + // } +} diff --git a/contracts/ownership/rbac/RBACWithAdmin.sol b/contracts/ownership/rbac/RBACWithAdmin.sol new file mode 100644 index 000000000..90eb187a1 --- /dev/null +++ b/contracts/ownership/rbac/RBACWithAdmin.sol @@ -0,0 +1,60 @@ +pragma solidity ^0.4.21; + +import "./RBAC.sol"; + + +/** + * @title RBACWithAdmin + * @author Matt Condon (@Shrugs) + * @dev It's recommended that you define constants in the contract, + * @dev like ROLE_ADMIN below, to avoid typos. + */ +contract RBACWithAdmin is RBAC { + /** + * A constant role name for indicating admins. + */ + string public constant ROLE_ADMIN = "admin"; + + /** + * @dev modifier to scope access to admins + * // reverts + */ + modifier onlyAdmin() + { + checkRole(msg.sender, ROLE_ADMIN); + _; + } + + /** + * @dev constructor. Sets msg.sender as admin by default + */ + function RBACWithAdmin() + public + { + addRole(msg.sender, ROLE_ADMIN); + } + + /** + * @dev add a role to an address + * @param addr address + * @param roleName the name of the role + */ + function adminAddRole(address addr, string roleName) + onlyAdmin + public + { + addRole(addr, roleName); + } + + /** + * @dev remove a role from an address + * @param addr address + * @param roleName the name of the role + */ + function adminRemoveRole(address addr, string roleName) + onlyAdmin + public + { + removeRole(addr, roleName); + } +} diff --git a/contracts/ownership/rbac/Roles.sol b/contracts/ownership/rbac/Roles.sol new file mode 100644 index 000000000..c9dbd73af --- /dev/null +++ b/contracts/ownership/rbac/Roles.sol @@ -0,0 +1,55 @@ +pragma solidity ^0.4.21; + + +/** + * @title Roles + * @author Francisco Giordano (@frangio) + * @dev Library for managing addresses assigned to a Role. + * See RBAC.sol for example usage. + */ +library Roles { + struct Role { + mapping (address => bool) bearer; + } + + /** + * @dev give an address access to this role + */ + function add(Role storage role, address addr) + internal + { + role.bearer[addr] = true; + } + + /** + * @dev remove an address' access to this role + */ + function remove(Role storage role, address addr) + internal + { + role.bearer[addr] = false; + } + + /** + * @dev check if an address has this role + * // reverts + */ + function check(Role storage role, address addr) + view + internal + { + require(has(role, addr)); + } + + /** + * @dev check if an address has this role + * @return bool + */ + function has(Role storage role, address addr) + view + internal + returns (bool) + { + return role.bearer[addr]; + } +} diff --git a/contracts/payment/PullPayment.sol b/contracts/payment/PullPayment.sol new file mode 100644 index 000000000..030e971b3 --- /dev/null +++ b/contracts/payment/PullPayment.sol @@ -0,0 +1,43 @@ +pragma solidity ^0.4.21; + + +import "../math/SafeMath.sol"; + + +/** + * @title PullPayment + * @dev Base contract supporting async send for pull payments. Inherit from this + * contract and use asyncSend instead of send or transfer. + */ +contract PullPayment { + using SafeMath for uint256; + + mapping(address => uint256) public payments; + uint256 public totalPayments; + + /** + * @dev Withdraw accumulated balance, called by payee. + */ + function withdrawPayments() public { + address payee = msg.sender; + uint256 payment = payments[payee]; + + require(payment != 0); + require(address(this).balance >= payment); + + totalPayments = totalPayments.sub(payment); + payments[payee] = 0; + + payee.transfer(payment); + } + + /** + * @dev Called by the payer to store the sent amount as credit to be pulled. + * @param dest The destination address of the funds. + * @param amount The amount to transfer. + */ + function asyncSend(address dest, uint256 amount) internal { + payments[dest] = payments[dest].add(amount); + totalPayments = totalPayments.add(amount); + } +} diff --git a/test/library/ECRecovery.test.js b/test/library/ECRecovery.test.js new file mode 100644 index 000000000..7ea55823f --- /dev/null +++ b/test/library/ECRecovery.test.js @@ -0,0 +1,77 @@ + +import { + hashMessage, + signMessage, +} from '../helpers/sign'; +const ECRecoveryMock = artifacts.require('ECRecoveryMock'); + +require('chai') + .use(require('chai-as-promised')) + .should(); + +contract('ECRecovery', function (accounts) { + let ecrecovery; + const TEST_MESSAGE = 'OpenZeppelin'; + + before(async function () { + ecrecovery = await ECRecoveryMock.new(); + }); + + it('recover v0', async function () { + // Signature generated outside ganache with method web3.eth.sign(signer, message) + let signer = '0x2cc1166f6212628a0deef2b33befb2187d35b86c'; + let message = web3.sha3(TEST_MESSAGE); + // eslint-disable-next-line max-len + let signature = '0x5d99b6f7f6d1f73d1a26497f2b1c89b24c0993913f86e9a2d02cd69887d9c94f3c880358579d811b21dd1b7fd9bb01c1d81d10e69f0384e675c32b39643be89200'; + const addrRecovered = await ecrecovery.recover(message, signature); + addrRecovered.should.eq(signer); + }); + + it('recover v1', async function () { + // Signature generated outside ganache with method web3.eth.sign(signer, message) + let signer = '0x1e318623ab09fe6de3c9b8672098464aeda9100e'; + let message = web3.sha3(TEST_MESSAGE); + // eslint-disable-next-line max-len + let signature = '0x331fe75a821c982f9127538858900d87d3ec1f9f737338ad67cad133fa48feff48e6fa0c18abc62e42820f05943e47af3e9fbe306ce74d64094bdf1691ee53e001'; + const addrRecovered = await ecrecovery.recover(message, signature); + addrRecovered.should.eq(signer); + }); + + it('recover using web3.eth.sign()', async function () { + // Create the signature using account[0] + const signature = signMessage(accounts[0], TEST_MESSAGE); + + // Recover the signer address from the generated message and signature. + const addrRecovered = await ecrecovery.recover( + hashMessage(TEST_MESSAGE), + signature + ); + addrRecovered.should.eq(accounts[0]); + }); + + it('recover using web3.eth.sign() should return wrong signer', async function () { + // Create the signature using account[0] + const signature = signMessage(accounts[0], TEST_MESSAGE); + + // Recover the signer address from the generated message and wrong signature. + const addrRecovered = await ecrecovery.recover(hashMessage('Test'), signature); + assert.notEqual(accounts[0], addrRecovered); + }); + + it('recover should fail when a wrong hash is sent', async function () { + // Create the signature using account[0] + let signature = signMessage(accounts[0], TEST_MESSAGE); + + // Recover the signer address from the generated message and wrong signature. + const addrRecovered = await ecrecovery.recover(hashMessage(TEST_MESSAGE).substring(2), signature); + addrRecovered.should.eq('0x0000000000000000000000000000000000000000'); + }); + + context('toEthSignedMessage', () => { + it('should prefix hashes correctly', async function () { + const hashedMessage = web3.sha3(TEST_MESSAGE); + const ethMessage = await ecrecovery.toEthSignedMessageHash(hashedMessage); + ethMessage.should.eq(hashMessage(TEST_MESSAGE)); + }); + }); +}); diff --git a/test/library/Math.test.js b/test/library/Math.test.js new file mode 100644 index 000000000..c671bc85f --- /dev/null +++ b/test/library/Math.test.js @@ -0,0 +1,43 @@ +var MathMock = artifacts.require('MathMock'); + +contract('Math', function (accounts) { + let math; + + before(async function () { + math = await MathMock.new(); + }); + + it('returns max64 correctly', async function () { + let a = 5678; + let b = 1234; + await math.max64(a, b); + let result = await math.result64(); + assert.equal(result, a); + }); + + it('returns min64 correctly', async function () { + let a = 5678; + let b = 1234; + await math.min64(a, b); + let result = await math.result64(); + + assert.equal(result, b); + }); + + it('returns max256 correctly', async function () { + let a = 5678; + let b = 1234; + await math.max256(a, b); + let result = await math.result256(); + assert.equal(result, a); + }); + + it('returns min256 correctly', async function () { + let a = 5678; + let b = 1234; + await math.min256(a, b); + let result = await math.result256(); + + assert.equal(result, b); + }); +}); diff --git a/test/library/MerkleProof.test.js b/test/library/MerkleProof.test.js new file mode 100644 index 000000000..1e8da3c8f --- /dev/null +++ b/test/library/MerkleProof.test.js @@ -0,0 +1,61 @@ + +import MerkleTree from '../helpers/merkleTree.js'; +import { sha3, bufferToHex } from 'ethereumjs-util'; + +var MerkleProofWrapper = artifacts.require('MerkleProofWrapper'); + +contract('MerkleProof', function (accounts) { + let merkleProof; + + before(async function () { + merkleProof = await MerkleProofWrapper.new(); + }); + + describe('verifyProof', function () { + it('should return true for a valid Merkle proof', async function () { + const elements = ['a', 'b', 'c', 'd']; + const merkleTree = new MerkleTree(elements); + + const root = merkleTree.getHexRoot(); + + const proof = merkleTree.getHexProof(elements[0]); + + const leaf = bufferToHex(sha3(elements[0])); + + const result = await merkleProof.verifyProof(proof, root, leaf); + assert.isOk(result, 'verifyProof did not return true for a valid proof'); + }); + + it('should return false for an invalid Merkle proof', async function () { + const correctElements = ['a', 'b', 'c']; + const correctMerkleTree = new MerkleTree(correctElements); + + const correctRoot = correctMerkleTree.getHexRoot(); + + const correctLeaf = bufferToHex(sha3(correctElements[0])); + + const badElements = ['d', 'e', 'f']; + const badMerkleTree = new MerkleTree(badElements); + + const badProof = badMerkleTree.getHexProof(badElements[0]); + + const result = await merkleProof.verifyProof(badProof, correctRoot, correctLeaf); + assert.isNotOk(result, 'verifyProof did not return false for an invalid proof'); + }); + + it('should return false for a Merkle proof of invalid length', async function () { + const elements = ['a', 'b', 'c']; + const merkleTree = new MerkleTree(elements); + + const root = merkleTree.getHexRoot(); + + const proof = merkleTree.getHexProof(elements[0]); + const badProof = proof.slice(0, proof.length - 5); + + const leaf = bufferToHex(sha3(elements[0])); + + const result = await merkleProof.verifyProof(badProof, root, leaf); + assert.isNotOk(result, 'verifyProof did not return false for proof of invalid length'); + }); + }); +}); diff --git a/test/ownership/rbac/RBAC.test.js b/test/ownership/rbac/RBAC.test.js new file mode 100644 index 000000000..e748efba5 --- /dev/null +++ b/test/ownership/rbac/RBAC.test.js @@ -0,0 +1,98 @@ +import expectThrow from '../../helpers/expectThrow'; +import expectEvent from '../../helpers/expectEvent'; + +const RBACMock = artifacts.require('RBACMock'); + +require('chai') + .use(require('chai-as-promised')) + .should(); + +const ROLE_ADVISOR = 'advisor'; + +contract('RBAC', function (accounts) { + let mock; + + const [ + admin, + anyone, + futureAdvisor, + ...advisors + ] = accounts; + + before(async () => { + mock = await RBACMock.new(advisors, { from: admin }); + }); + + context('in normal conditions', () => { + it('allows admin to call #onlyAdminsCanDoThis', async () => { + await mock.onlyAdminsCanDoThis({ from: admin }) + .should.be.fulfilled; + }); + it('allows admin to call #onlyAdvisorsCanDoThis', async () => { + await mock.onlyAdvisorsCanDoThis({ from: admin }) + .should.be.fulfilled; + }); + it('allows advisors to call #onlyAdvisorsCanDoThis', async () => { + await mock.onlyAdvisorsCanDoThis({ from: advisors[0] }) + .should.be.fulfilled; + }); + it('allows admin to call #eitherAdminOrAdvisorCanDoThis', async () => { + await mock.eitherAdminOrAdvisorCanDoThis({ from: admin }) + .should.be.fulfilled; + }); + it('allows advisors to call #eitherAdminOrAdvisorCanDoThis', async () => { + await mock.eitherAdminOrAdvisorCanDoThis({ from: advisors[0] }) + .should.be.fulfilled; + }); + it('does not allow admins to call #nobodyCanDoThis', async () => { + await expectThrow( + mock.nobodyCanDoThis({ from: admin }) + ); + }); + it('does not allow advisors to call #nobodyCanDoThis', async () => { + await expectThrow( + mock.nobodyCanDoThis({ from: advisors[0] }) + ); + }); + it('does not allow anyone to call #nobodyCanDoThis', async () => { + await expectThrow( + mock.nobodyCanDoThis({ from: anyone }) + ); + }); + it('allows an admin to remove an advisor\'s role', async () => { + await mock.removeAdvisor(advisors[0], { from: admin }) + .should.be.fulfilled; + }); + it('allows admins to #adminRemoveRole', async () => { + await mock.adminRemoveRole(advisors[3], ROLE_ADVISOR, { from: admin }) + .should.be.fulfilled; + }); + + it('announces a RoleAdded event on addRole', async () => { + await expectEvent.inTransaction( + mock.adminAddRole(futureAdvisor, ROLE_ADVISOR, { from: admin }), + 'RoleAdded' + ); + }); + + it('announces a RoleRemoved event on removeRole', async () => { + await expectEvent.inTransaction( + mock.adminRemoveRole(futureAdvisor, ROLE_ADVISOR, { from: admin }), + 'RoleRemoved' + ); + }); + }); + + context('in adversarial conditions', () => { + it('does not allow an advisor to remove another advisor', async () => { + await expectThrow( + mock.removeAdvisor(advisors[1], { from: advisors[0] }) + ); + }); + it('does not allow "anyone" to remove an advisor', async () => { + await expectThrow( + mock.removeAdvisor(advisors[0], { from: anyone }) + ); + }); + }); +}); diff --git a/test/payment/PullPayment.test.js b/test/payment/PullPayment.test.js new file mode 100644 index 000000000..8f5a4e41a --- /dev/null +++ b/test/payment/PullPayment.test.js @@ -0,0 +1,71 @@ +var PullPaymentMock = artifacts.require('PullPaymentMock'); + +contract('PullPayment', function (accounts) { + let ppce; + let amount = 17 * 1e18; + + beforeEach(async function () { + ppce = await PullPaymentMock.new({ value: amount }); + }); + + it('can\'t call asyncSend externally', async function () { + assert.isUndefined(ppce.asyncSend); + }); + + it('can record an async payment correctly', async function () { + let AMOUNT = 100; + await ppce.callSend(accounts[0], AMOUNT); + let paymentsToAccount0 = await ppce.payments(accounts[0]); + let totalPayments = await ppce.totalPayments(); + + assert.equal(totalPayments, AMOUNT); + assert.equal(paymentsToAccount0, AMOUNT); + }); + + it('can add multiple balances on one account', async function () { + await ppce.callSend(accounts[0], 200); + await ppce.callSend(accounts[0], 300); + let paymentsToAccount0 = await ppce.payments(accounts[0]); + let totalPayments = await ppce.totalPayments(); + + assert.equal(totalPayments, 500); + assert.equal(paymentsToAccount0, 500); + }); + + it('can add balances on multiple accounts', async function () { + await ppce.callSend(accounts[0], 200); + await ppce.callSend(accounts[1], 300); + + let paymentsToAccount0 = await ppce.payments(accounts[0]); + assert.equal(paymentsToAccount0, 200); + + let paymentsToAccount1 = await ppce.payments(accounts[1]); + assert.equal(paymentsToAccount1, 300); + + let totalPayments = await ppce.totalPayments(); + assert.equal(totalPayments, 500); + }); + + it('can withdraw payment', async function () { + let payee = accounts[1]; + let initialBalance = web3.eth.getBalance(payee); + + await ppce.callSend(payee, amount); + + let payment1 = await ppce.payments(payee); + assert.equal(payment1, amount); + + let totalPayments = await ppce.totalPayments(); + assert.equal(totalPayments, amount); + + await ppce.withdrawPayments({ from: payee }); + let payment2 = await ppce.payments(payee); + assert.equal(payment2, 0); + + totalPayments = await ppce.totalPayments(); + assert.equal(totalPayments, 0); + + let balance = web3.eth.getBalance(payee); + assert(Math.abs(balance - initialBalance - amount) < 1e16); + }); +});