diff --git a/contracts/schemes/ReputationFromToken.sol b/contracts/schemes/ReputationFromToken.sol index 6581a6aa..27639564 100644 --- a/contracts/schemes/ReputationFromToken.sol +++ b/contracts/schemes/ReputationFromToken.sol @@ -3,6 +3,8 @@ pragma solidity ^0.5.11; import "../controller/ControllerInterface.sol"; 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"; /** * @title A scheme for reputation allocation according to token balances @@ -10,6 +12,8 @@ import "./CurveInterface.sol"; */ contract ReputationFromToken { + using ECDSA for bytes32; + using SafeMath for uint256; IERC20 public tokenContract; CurveInterface public curve; @@ -17,8 +21,16 @@ contract ReputationFromToken { mapping(address => bool) public redeems; Avatar public avatar; - event Redeem(address indexed _beneficiary, address indexed _sender, uint256 _amount); + // Digest describing the data the user signs according EIP 712. + // Needs to match what is passed to Metamask. + bytes32 public constant DELEGATION_HASH_EIP712 = + keccak256(abi.encodePacked( + "address ReputationFromTokenAddress", + "address Beneficiary" + )); + event Redeem(address indexed _beneficiary, address indexed _sender, uint256 _amount); + /** * @dev initialize * @param _avatar the avatar to mint reputation from @@ -38,22 +50,73 @@ contract ReputationFromToken { * @param _beneficiary the beneficiary address to redeem for * @return uint256 minted reputation */ - function redeem(address _beneficiary) public returns(uint256) { + function redeem(address _beneficiary) external returns(uint256) { + return _redeem(_beneficiary, msg.sender); + } + + /** + * @dev redeemWithSignature function + * @param _beneficiary the beneficiary address to redeem for + * @param _signatureType signature type + 1 - for web3.eth.sign + 2 - for eth_signTypedData according to EIP #712. + * @param _signature - signed data by the staker + * @return uint256 minted reputation + */ + function redeemWithSignature( + address _beneficiary, + uint256 _signatureType, + bytes calldata _signature + ) + external + returns(uint256) + { + // Recreate the digest the user signed + bytes32 delegationDigest; + if (_signatureType == 2) { + delegationDigest = keccak256( + abi.encodePacked( + DELEGATION_HASH_EIP712, keccak256( + abi.encodePacked( + address(this), + _beneficiary) + ) + ) + ); + } else { + delegationDigest = keccak256( + abi.encodePacked( + address(this), + _beneficiary) + ).toEthSignedMessageHash(); + } + address redeemer = delegationDigest.recover(_signature); + require(redeemer != address(0), "redeemer address cannot be 0"); + return _redeem(_beneficiary, redeemer); + } + + /** + * @dev redeem function + * @param _beneficiary the beneficiary address to redeem for + * @param _redeemer the redeemer address + * @return uint256 minted reputation + */ + function _redeem(address _beneficiary, address _redeemer) private returns(uint256) { require(avatar != Avatar(0), "should initialize first"); - require(redeems[msg.sender] == false, "redeeming twice from the same account is not allowed"); - redeems[msg.sender] = true; - uint256 tokenAmount = tokenContract.balanceOf(msg.sender); + require(redeems[_redeemer] == false, "redeeming twice from the same account is not allowed"); + redeems[_redeemer] = true; + uint256 tokenAmount = tokenContract.balanceOf(_redeemer); if (curve != CurveInterface(0)) { tokenAmount = curve.calc(tokenAmount); } if (_beneficiary == address(0)) { - _beneficiary = msg.sender; + _beneficiary = _redeemer; } require( ControllerInterface( avatar.owner()) .mintReputation(tokenAmount, _beneficiary, address(avatar)), "mint reputation should succeed"); - emit Redeem(_beneficiary, msg.sender, tokenAmount); + emit Redeem(_beneficiary, _redeemer, tokenAmount); return tokenAmount; } } diff --git a/test/reputationfromtoken.js b/test/reputationfromtoken.js index 682c60a0..0dd4d7e3 100644 --- a/test/reputationfromtoken.js +++ b/test/reputationfromtoken.js @@ -10,7 +10,7 @@ var PolkaCurve = artifacts.require("./PolkaCurve.sol"); var NectarRepAllocation = artifacts.require("./NectarRepAllocation.sol"); const NectarToken = artifacts.require('./Reputation.sol'); - +var ethereumjs = require('ethereumjs-abi'); const setupNectar = async function (accounts) { var testSetup = new helpers.TestSetup(); @@ -66,7 +66,25 @@ const setup = async function (accounts, _initialize = true) { 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) { + var textMsg = "0x"+ethereumjs.soliditySHA3( + ["address","address"], + [_testSetup.reputationFromToken.address, _beneficiary] + ).toString("hex"); + //https://github.com/ethereum/wiki/wiki/JavaScript-API#web3ethsign + let signature = await web3.eth.sign(textMsg , _redeemer); + const signature1 = signature.substring(0, signature.length-2); + var v = signature.substring(signature.length-2, signature.length); + + if (v === '00') { + signature = signature1+'1b'; + } else { + signature = signature1+'1c'; + } + return (await _testSetup.reputationFromToken.redeemWithSignature(_beneficiary,signatureType,signature + ,{from:_fromAccount})); +}; contract('ReputationFromToken and RepAllocation', accounts => { it("initialize", async () => { let testSetup = await setup(accounts); @@ -132,6 +150,22 @@ contract('ReputationFromToken and RepAllocation', accounts => { assert.equal(await testSetup.org.reputation.balanceOf(accounts[1]),expected); }); + it("redeemWithSignature", async () => { + let testSetup = await setup(accounts); + var tx = await redeem(testSetup,accounts[1],accounts[0],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; + + assert.equal(tx.logs.length,1); + assert.equal(tx.logs[0].event,"Redeem"); + assert.equal(tx.logs[0].args._beneficiary,accounts[1]); + assert.equal(tx.logs[0].args._amount.toString(),expected); + assert.equal(tx.logs[0].args._sender,accounts[0]); + assert.equal(await testSetup.org.reputation.balanceOf(accounts[0]),1000); + assert.equal(await testSetup.org.reputation.balanceOf(accounts[1]),expected); + }); + it("redeem with no beneficiary", async () => { let testSetup = await setup(accounts); var tx = await testSetup.reputationFromToken.redeem(helpers.NULL_ADDRESS);