diff --git a/contracts/schemes/ReputationFromToken.sol b/contracts/schemes/ReputationFromToken.sol index 27639564..ab5b1ae9 100644 --- a/contracts/schemes/ReputationFromToken.sol +++ b/contracts/schemes/ReputationFromToken.sol @@ -5,13 +5,14 @@ import "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; import "./CurveInterface.sol"; import "openzeppelin-solidity/contracts/cryptography/ECDSA.sol"; import "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import "./Agreement.sol"; /** * @title A scheme for reputation allocation according to token balances * This contract is assuming that the token contract is paused, and one cannot transfer its tokens. */ -contract ReputationFromToken { +contract ReputationFromToken is Agreement { using ECDSA for bytes32; using SafeMath for uint256; @@ -26,32 +27,36 @@ contract ReputationFromToken { bytes32 public constant DELEGATION_HASH_EIP712 = keccak256(abi.encodePacked( "address ReputationFromTokenAddress", - "address Beneficiary" + "address Beneficiary", + "bytes32 AgreementHash" )); event Redeem(address indexed _beneficiary, address indexed _sender, uint256 _amount); - + /** * @dev initialize * @param _avatar the avatar to mint reputation from * @param _tokenContract the token contract + * @param _agreementHash is a hash of agreement required to be added to the TX by participants */ - function initialize(Avatar _avatar, IERC20 _tokenContract, CurveInterface _curve) external + function initialize(Avatar _avatar, IERC20 _tokenContract, CurveInterface _curve, bytes32 _agreementHash) external { require(avatar == Avatar(0), "can be called only one time"); require(_avatar != Avatar(0), "avatar cannot be zero"); tokenContract = _tokenContract; avatar = _avatar; curve = _curve; + super.setAgreementHash(_agreementHash); } /** * @dev redeem function * @param _beneficiary the beneficiary address to redeem for + * @param _agreementHash the agreementHash hash * @return uint256 minted reputation */ - function redeem(address _beneficiary) external returns(uint256) { - return _redeem(_beneficiary, msg.sender); + function redeem(address _beneficiary, bytes32 _agreementHash) external returns(uint256) { + return _redeem(_beneficiary, msg.sender, _agreementHash); } /** @@ -65,6 +70,7 @@ contract ReputationFromToken { */ function redeemWithSignature( address _beneficiary, + bytes32 _agreementHash, uint256 _signatureType, bytes calldata _signature ) @@ -79,7 +85,8 @@ contract ReputationFromToken { DELEGATION_HASH_EIP712, keccak256( abi.encodePacked( address(this), - _beneficiary) + _beneficiary, + _agreementHash) ) ) ); @@ -87,12 +94,13 @@ contract ReputationFromToken { delegationDigest = keccak256( abi.encodePacked( address(this), - _beneficiary) + _beneficiary, + _agreementHash) ).toEthSignedMessageHash(); } address redeemer = delegationDigest.recover(_signature); require(redeemer != address(0), "redeemer address cannot be 0"); - return _redeem(_beneficiary, redeemer); + return _redeem(_beneficiary, redeemer, _agreementHash); } /** @@ -101,7 +109,10 @@ contract ReputationFromToken { * @param _redeemer the redeemer address * @return uint256 minted reputation */ - function _redeem(address _beneficiary, address _redeemer) private returns(uint256) { + function _redeem(address _beneficiary, address _redeemer, bytes32 _agreementHash) + private + onlyAgree(_agreementHash) + returns(uint256) { require(avatar != Avatar(0), "should initialize first"); require(redeems[_redeemer] == false, "redeeming twice from the same account is not allowed"); redeems[_redeemer] = true; diff --git a/package-lock.json b/package-lock.json index 391b911f..e7577600 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@daostack/arc", - "version": "0.0.1-rc.32", + "version": "0.0.1-rc.33", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -83,9 +83,9 @@ } }, "@babel/parser": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.7.2.tgz", - "integrity": "sha512-DDaR5e0g4ZTb9aP7cpSZLkACEBdoLGwJDWgHtBhrGX7Q1RjhdoMOfexICj5cqTAtpowjGQWfcvfnQG7G2kAB5w==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.7.3.tgz", + "integrity": "sha512-bqv+iCo9i+uLVbI0ILzKkvMorqxouI+GbV13ivcARXn9NNEabi2IEz912IgNpT/60BNXac5dgcfjb94NjsF33A==", "dev": true }, "@babel/template": { @@ -200,6 +200,19 @@ "@types/babel-types": "*" } }, + "@types/bn.js": { + "version": "4.11.5", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.5.tgz", + "integrity": "sha512-AEAZcIZga0JgVMHNtl1CprA/hXX7/wPt79AgR4XqaDt7jyj3QWYw6LPoOiznPtugDmlubUnAahMs2PFxGcQrng==", + "requires": { + "@types/node": "*" + } + }, + "@types/node": { + "version": "12.12.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.9.tgz", + "integrity": "sha512-kV3w4KeLsRBW+O2rKhktBwENNJuqAUQHS3kf4ia2wIaF/MN6U7ANgTsx7tGremcA0Pk3Yh0Hl0iKiLPuBdIgmw==" + }, "acorn": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz", @@ -1958,9 +1971,9 @@ } }, "es-to-primitive": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", - "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", "dev": true, "requires": { "is-callable": "^1.1.4", @@ -2323,16 +2336,16 @@ } }, "ethereumjs-util": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-6.1.0.tgz", - "integrity": "sha512-URESKMFbDeJxnAxPppnk2fN6Y3BIatn9fwn76Lm8bQlt+s52TpG8dN9M66MLPuRAiAOIqL3dfwqWJf0sd0fL0Q==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-6.2.0.tgz", + "integrity": "sha512-vb0XN9J2QGdZGIEKG2vXM+kUdEivUfU6Wmi5y0cg+LRhDYKnXIZ/Lz7XjFbHRR9VIKq2lVGLzGBkA++y2nOdOQ==", "requires": { + "@types/bn.js": "^4.11.3", "bn.js": "^4.11.0", "create-hash": "^1.1.2", "ethjs-util": "0.1.6", - "keccak": "^1.0.2", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1", + "keccak": "^2.0.0", + "rlp": "^2.2.3", "secp256k1": "^3.0.1" } }, @@ -3995,9 +4008,9 @@ "dev": true }, "has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", "dev": true }, "has-value": { @@ -4107,9 +4120,9 @@ "dev": true }, "import-fresh": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.1.0.tgz", - "integrity": "sha512-PpuksHKGt8rXfWEr9m9EHIpgyyaltBy8+eF6GJM0QCAxMgxCfucMF3mjecK2QsJr0amJW7gTqh5/wht0z2UhEQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", "dev": true, "requires": { "parent-module": "^1.0.0", @@ -4538,9 +4551,9 @@ } }, "keccak": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/keccak/-/keccak-1.4.0.tgz", - "integrity": "sha512-eZVaCpblK5formjPjeTBik7TAg+pqnDrMHIffSvi9Lh7PQgM1+hSzakUeZFCk9DVVG0dacZJuaz2ntwlzZUIBw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-2.0.0.tgz", + "integrity": "sha512-rKe/lRr0KGhjoz97cwg+oeT1Rj/Y4cjae6glArioUC8JBF9ROGZctwIaaruM7d7naovME4Q8WcQSO908A8qcyQ==", "requires": { "bindings": "^1.2.1", "inherits": "^2.0.3", @@ -4722,16 +4735,16 @@ } }, "mime-db": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" + "version": "1.42.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.42.0.tgz", + "integrity": "sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ==" }, "mime-types": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "version": "2.1.25", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.25.tgz", + "integrity": "sha512-5KhStqB5xpTAeGqKBAMgwaYMnQik7teQN4IAzC7npDv6kzeU6prfkR67bc87J1kWMPGkoaZSq1npmexMgkmEVg==", "requires": { - "mime-db": "1.40.0" + "mime-db": "1.42.0" } }, "mimic-fn": { @@ -5016,9 +5029,9 @@ } }, "object-inspect": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.6.0.tgz", - "integrity": "sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", "dev": true }, "object-keys": { @@ -5429,9 +5442,9 @@ "dev": true }, "prettier": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.18.2.tgz", - "integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", "dev": true, "optional": true }, @@ -5639,9 +5652,9 @@ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" }, "react-is": { - "version": "16.11.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.11.0.tgz", - "integrity": "sha512-gbBVYR2p8mnriqAwWx9LbuUrShnAuSCNnuPGyc7GJrMVQtPDAh8iLpv7FRuMPFb56KkaVZIYSz1PrjI9q0QPCw==", + "version": "16.12.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz", + "integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==", "dev": true }, "read": { diff --git a/package.json b/package.json index e4e05c5c..86089603 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@daostack/arc", - "version": "0.0.1-rc.32", + "version": "0.0.1-rc.33", "description": "A platform for building DAOs", "files": [ "contracts/", diff --git a/test/reputationfromtoken.js b/test/reputationfromtoken.js index 0dd4d7e3..dca63264 100644 --- a/test/reputationfromtoken.js +++ b/test/reputationfromtoken.js @@ -12,7 +12,7 @@ var NectarRepAllocation = artifacts.require("./NectarRepAllocation.sol"); const NectarToken = artifacts.require('./Reputation.sol'); var ethereumjs = require('ethereumjs-abi'); -const setupNectar = async function (accounts) { +const setupNectar = async function (accounts,_agreementHash = helpers.SOME_HASH) { var testSetup = new helpers.TestSetup(); var controllerCreator = await ControllerCreator.new({gas: constants.ARC_GAS_LIMIT}); var daoTracker = await DAOTracker.new({gas: constants.ARC_GAS_LIMIT}); @@ -30,20 +30,19 @@ const setupNectar = async function (accounts) { 0, testSetup.blockReference, testSetup.nectarToken.address); - testSetup.reputationFromToken = await ReputationFromToken.new(); testSetup.curve = helpers.NULL_ADDRESS; await testSetup.reputationFromToken.initialize(testSetup.org.avatar.address, testSetup.nectarRepAllocation.address, - helpers.NULL_ADDRESS); - - + helpers.NULL_ADDRESS, + _agreementHash); + testSetup.agreementHash = _agreementHash; var permissions = "0x00000000"; await testSetup.daoCreator.setSchemes(testSetup.org.avatar.address,[testSetup.reputationFromToken.address],[helpers.NULL_HASH],[permissions],"metaData"); return testSetup; }; -const setup = async function (accounts, _initialize = true) { +const setup = async function (accounts, _initialize = true, _agreementHash = helpers.SOME_HASH) { var testSetup = new helpers.TestSetup(); var controllerCreator = await ControllerCreator.new({gas: constants.ARC_GAS_LIMIT}); var daoTracker = await DAOTracker.new({gas: constants.ARC_GAS_LIMIT}); @@ -59,18 +58,19 @@ const setup = async function (accounts, _initialize = true) { if (_initialize === true) { await testSetup.reputationFromToken.initialize(testSetup.org.avatar.address, testSetup.repAllocation.address, - testSetup.curve.address); + testSetup.curve.address, + _agreementHash); } - + testSetup.agreementHash = _agreementHash; var permissions = "0x00000000"; await testSetup.daoCreator.setSchemes(testSetup.org.avatar.address,[testSetup.reputationFromToken.address],[helpers.NULL_HASH],[permissions],"metaData"); return testSetup; }; const signatureType = 1; -const redeem = async function(_testSetup,_beneficiary,_redeemer,_fromAccount) { +const redeem = async function(_testSetup,_beneficiary,_redeemer,_agreementHash,_fromAccount) { var textMsg = "0x"+ethereumjs.soliditySHA3( - ["address","address"], - [_testSetup.reputationFromToken.address, _beneficiary] + ["address","address","bytes32"], + [_testSetup.reputationFromToken.address, _beneficiary,_agreementHash] ).toString("hex"); //https://github.com/ethereum/wiki/wiki/JavaScript-API#web3ethsign let signature = await web3.eth.sign(textMsg , _redeemer); @@ -82,7 +82,7 @@ const redeem = async function(_testSetup,_beneficiary,_redeemer,_fromAccount) { } else { signature = signature1+'1c'; } - return (await _testSetup.reputationFromToken.redeemWithSignature(_beneficiary,signatureType,signature + return (await _testSetup.reputationFromToken.redeemWithSignature(_beneficiary,_agreementHash,signatureType,signature ,{from:_fromAccount})); }; contract('ReputationFromToken and RepAllocation', accounts => { @@ -136,7 +136,7 @@ contract('ReputationFromToken and RepAllocation', accounts => { it("redeem", async () => { let testSetup = await setup(accounts); - var tx = await testSetup.reputationFromToken.redeem(accounts[1]); + var tx = await testSetup.reputationFromToken.redeem(accounts[1],testSetup.agreementHash); var total_reputation = await testSetup.curve.TOTAL_REPUTATION(); var sum_of_sqrt = await testSetup.curve.SUM_OF_SQRTS(); var expected = Math.floor(((10*total_reputation)/sum_of_sqrt) * 1000000000) * 1000000000; @@ -152,7 +152,7 @@ contract('ReputationFromToken and RepAllocation', accounts => { it("redeemWithSignature", async () => { let testSetup = await setup(accounts); - var tx = await redeem(testSetup,accounts[1],accounts[0],accounts[2]); + var tx = await redeem(testSetup,accounts[1],accounts[0],testSetup.agreementHash,accounts[2]); var total_reputation = await testSetup.curve.TOTAL_REPUTATION(); var sum_of_sqrt = await testSetup.curve.SUM_OF_SQRTS(); var expected = Math.floor(((10*total_reputation)/sum_of_sqrt) * 1000000000) * 1000000000; @@ -168,7 +168,7 @@ contract('ReputationFromToken and RepAllocation', accounts => { it("redeem with no beneficiary", async () => { let testSetup = await setup(accounts); - var tx = await testSetup.reputationFromToken.redeem(helpers.NULL_ADDRESS); + var tx = await testSetup.reputationFromToken.redeem(helpers.NULL_ADDRESS,testSetup.agreementHash); var total_reputation = await testSetup.curve.TOTAL_REPUTATION(); var sum_of_sqrt = await testSetup.curve.SUM_OF_SQRTS(); var expected = Math.floor(((10*total_reputation)/sum_of_sqrt) * 1000000000) * 1000000000; @@ -187,8 +187,8 @@ contract('ReputationFromToken and RepAllocation', accounts => { try { await testSetup.reputationFromToken.initialize(testSetup.org.avatar.address, testSetup.repAllocation.address, - testSetup.curve.address - ); + testSetup.curve.address, + testSetup.agreementHash); assert(false, "cannot initialize twice"); } catch(error) { helpers.assertVMException(error); @@ -197,7 +197,13 @@ contract('ReputationFromToken and RepAllocation', accounts => { it("redeem nectar", async () => { let testSetup = await setupNectar(accounts); - var tx = await testSetup.reputationFromToken.redeem(accounts[1],{from:accounts[1]}); + try { + await testSetup.reputationFromToken.redeem(accounts[1],helpers.NULL_HASH,{from:accounts[1]}); + assert(false, "redeem with wrong agreement hash should fail"); + } catch(error) { + helpers.assertVMException(error); + } + var tx = await testSetup.reputationFromToken.redeem(accounts[1],testSetup.agreementHash,{from:accounts[1]}); var expected = Math.floor((200*testSetup.reputationReward)/300); assert.equal(tx.logs.length,1); assert.equal(tx.logs[0].event,"Redeem");