add more openzeppelin-solidity library contracts

This commit is contained in:
Francisco Giordano
2018-05-18 20:54:43 -03:00
parent be101154fa
commit 39fe05dfad
17 changed files with 899 additions and 0 deletions

76
contracts/ECRecovery.sol Normal file
View File

@ -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
);
}
}

35
contracts/MerkleProof.sol Normal file
View File

@ -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;
}
}

24
contracts/math/Math.sol Normal file
View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
// _;
// }
}

View File

@ -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);
}
}

View File

@ -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];
}
}

View File

@ -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);
}
}

View File

@ -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));
});
});
});

43
test/library/Math.test.js Normal file
View File

@ -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);
});
});

View File

@ -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');
});
});
});

View File

@ -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 })
);
});
});
});

View File

@ -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);
});
});