diff --git a/contracts/globalConstraints/EtherGC.sol b/contracts/globalConstraints/EtherGC.sol new file mode 100644 index 00000000..15fbb03e --- /dev/null +++ b/contracts/globalConstraints/EtherGC.sol @@ -0,0 +1,81 @@ +pragma solidity 0.5.17; + +import "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; +import "./GlobalConstraintInterface.sol"; +import "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import "../controller/Avatar.sol"; + + +/** + * @title EtherGC ether constraint per period + */ +contract EtherGC is GlobalConstraintInterface { + using SafeMath for uint256; + + uint256 public periodLength; //the period length in seconds + uint256 public amountAllowedPerPeriod; + Avatar public avatar; + uint256 public startTime; + uint256 public avatarBalanceBefore; + // a mapping from period indexes to amounts + mapping(uint256=>uint256) public totalAmountSentPerPeriod; + + /** + * @dev initialize + * @param _avatar the avatar to enforce the constraint on + * @param _periodLength the periodLength in seconds + * @param _amountAllowedPerPeriod the amount of eth to constraint for each period + */ + function initialize( + Avatar _avatar, + uint256 _periodLength, + uint256 _amountAllowedPerPeriod + ) + external + { + require(avatar == Avatar(0), "can be called only one time"); + require(_avatar != Avatar(0), "avatar cannot be zero"); + avatar = _avatar; + periodLength = _periodLength; + amountAllowedPerPeriod = _amountAllowedPerPeriod; + // solhint-disable-next-line not-rely-on-time + startTime = now; + } + + /** + * @dev check the constraint before the action. + * @return true + */ + function pre(address, bytes32, bytes32) public returns(bool) { + require(msg.sender == avatar.owner(), "only avatar owner is authorize to call"); + avatarBalanceBefore = address(avatar).balance; + return true; + } + + /** + * @dev check the allowance of ether sent per period + * and throws an error if the constraint is violated + * @return bool which represents a success + */ + function post(address, bytes32, bytes32) public returns(bool) { + require(msg.sender == avatar.owner(), "only avatar owner is authorize to call"); + if (avatarBalanceBefore > address(avatar).balance) { + // solhint-disable-next-line not-rely-on-time + uint256 currentPeriodIndex = (now - startTime)/periodLength; + totalAmountSentPerPeriod[currentPeriodIndex] = + totalAmountSentPerPeriod[currentPeriodIndex].add(avatarBalanceBefore.sub(address(avatar).balance)); + require(totalAmountSentPerPeriod[currentPeriodIndex] <= amountAllowedPerPeriod, + "Violation of Global constraint EtherGC:amount sent exceed in current period"); + } + avatarBalanceBefore = 0; //save gas + return true; + } + + /** + * @dev when return if this globalConstraints is pre, post or both. + * @return CallPhase enum indication Pre, Post or PreAndPost. + */ + function when() public pure returns(GlobalConstraintInterface.CallPhase) { + return GlobalConstraintInterface.CallPhase.PreAndPost; + } +} diff --git a/contracts/globalConstraints/GlobalConstraintInterface.sol b/contracts/globalConstraints/GlobalConstraintInterface.sol index 3e2d6694..21a431e4 100644 --- a/contracts/globalConstraints/GlobalConstraintInterface.sol +++ b/contracts/globalConstraints/GlobalConstraintInterface.sol @@ -11,5 +11,5 @@ contract GlobalConstraintInterface { * @dev when return if this globalConstraints is pre, post or both. * @return CallPhase enum indication Pre, Post or PreAndPost. */ - function when() public returns(CallPhase); + function when() public pure returns(CallPhase); } diff --git a/contracts/globalConstraints/ReputationGC.sol b/contracts/globalConstraints/ReputationGC.sol new file mode 100644 index 00000000..56a94b77 --- /dev/null +++ b/contracts/globalConstraints/ReputationGC.sol @@ -0,0 +1,109 @@ +pragma solidity 0.5.17; + +import "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; +import "./GlobalConstraintInterface.sol"; +import "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import "../controller/Avatar.sol"; + + +/** + * @title ReputationGC reputation mint/butn constraint per period + */ +contract ReputationGC is GlobalConstraintInterface { + using SafeMath for uint256; + + uint256 public periodLength; //the period length in seconds + uint256 public percentageAllowedPerPeriod; + Avatar public avatar; + uint256 public startTime; + uint256 public totalRepSupplyBefore; + + // a mapping from period indexes to amounts + mapping(uint256=>uint256) public totalRepMintedPerPeriod; + mapping(uint256=>uint256) public totalRepBurnedPerPeriod; + // a mapping from period to totalSupply + mapping(uint256=>uint256) public totalRepSupplyPerPeriod; + + /** + * @dev initialize + * @param _avatar the avatar to enforce the constraint on + * @param _periodLength the periodLength in seconds + * @param _percentageAllowedPerPeriod the amount of reputation to constraint for each period (brun and mint) + */ + function initialize( + Avatar _avatar, + uint256 _periodLength, + uint256 _percentageAllowedPerPeriod + ) + external + { + require(avatar == Avatar(0), "can be called only one time"); + require(_avatar != Avatar(0), "avatar cannot be zero"); + require(_percentageAllowedPerPeriod <= 100, "precentage allowed cannot be greated than 100"); + avatar = _avatar; + periodLength = _periodLength; + percentageAllowedPerPeriod = _percentageAllowedPerPeriod; + // solhint-disable-next-line not-rely-on-time + startTime = now; + } + + /** + * @dev check the constraint before the action. + * @return true + */ + function pre(address, bytes32, bytes32) public returns(bool) { + require(msg.sender == avatar.owner(), "only avatar owner is authorize to call"); + totalRepSupplyBefore = (avatar.nativeReputation()).totalSupply(); + // solhint-disable-next-line not-rely-on-time + uint256 currentPeriodIndex = (now - startTime)/periodLength; + if (totalRepSupplyPerPeriod[currentPeriodIndex] == 0) { + totalRepSupplyPerPeriod[currentPeriodIndex] = totalRepSupplyBefore; + } + + return true; + } + + /** + * @dev check the allowance of reputation minted or burned per period + * and throws an error if the constraint is violated + * @return bool which represents a success + */ + function post(address, bytes32, bytes32) public returns(bool) { + require(msg.sender == avatar.owner(), "only avatar owner is authorize to call"); + uint256 currentRepTotalSupply = (avatar.nativeReputation()).totalSupply(); + if (totalRepSupplyBefore != currentRepTotalSupply) { + // solhint-disable-next-line not-rely-on-time + uint256 currentPeriodIndex = (now - startTime)/periodLength; + uint256 repAllowedForCurrentPeriod = totalRepSupplyPerPeriod[currentPeriodIndex] + .mul(percentageAllowedPerPeriod) + .div(100); + + if (totalRepSupplyBefore > currentRepTotalSupply) { + //reputation was burned + uint256 burnedReputation = totalRepSupplyBefore.sub(currentRepTotalSupply); + totalRepBurnedPerPeriod[currentPeriodIndex] = + totalRepBurnedPerPeriod[currentPeriodIndex].add(burnedReputation); + + require(totalRepBurnedPerPeriod[currentPeriodIndex] <= repAllowedForCurrentPeriod, + "Violation of Global constraint ReputationGC:amount of reputation burned exceed in current period"); + } else { + // reputation was minted + uint256 mintedReputation = currentRepTotalSupply.sub(totalRepSupplyBefore); + totalRepMintedPerPeriod[currentPeriodIndex] = + totalRepMintedPerPeriod[currentPeriodIndex].add(mintedReputation); + require(totalRepMintedPerPeriod[currentPeriodIndex] <= repAllowedForCurrentPeriod, + "Violation of Global constraint ReputationGC:amount of reputation minted exceed in current period"); + } + } + totalRepSupplyBefore = 0; //save gas + return true; + } + + /** + * @dev when return if this globalConstraints is pre, post or both. + * @return CallPhase enum indication Pre, Post or PreAndPost. + */ + function when() public pure returns(GlobalConstraintInterface.CallPhase) { + return GlobalConstraintInterface.CallPhase.PreAndPost; + } +} diff --git a/contracts/schemes/GlobalConstraintAddOrRemove.sol b/contracts/schemes/GlobalConstraintAddOrRemove.sol new file mode 100644 index 00000000..345766f1 --- /dev/null +++ b/contracts/schemes/GlobalConstraintAddOrRemove.sol @@ -0,0 +1,53 @@ +pragma solidity 0.5.17; + +import "../controller/Controller.sol"; + +/** + * @title A scheme for adding or removing a global constraint + * The scheme will unregister itself after register the globalConstraint + * This scheme should be register to the dao with permission 0x00000004 + */ + +contract GlobalConstraintAddOrRemove { + + Avatar public avatar; + address public globalConstraint; + bytes32 public paramsHash; + + /** + * @dev initialize + * @param _avatar the avatar to mint reputation from + * @param _globalConstraint the globalConstraint address + * @param _paramsHash globalConstraint paramsHash + */ + function initialize( + Avatar _avatar, + address _globalConstraint, + bytes32 _paramsHash + ) external { + require(avatar == Avatar(0), "can be called only one time"); + require(_avatar != Avatar(0), "avatar cannot be zero"); + avatar = _avatar; + globalConstraint = _globalConstraint; + paramsHash = _paramsHash; + } + + /** + * @dev add globalConstraint + * and remove itsef. + */ + function add() external { + Controller(avatar.owner()).addGlobalConstraint(globalConstraint, paramsHash, address(avatar)); + Controller(avatar.owner()).unregisterSelf(address(avatar)); + } + + /** + * @dev remove globalConstraint + * and remove itsef. + */ + function remove() external { + Controller(avatar.owner()).removeGlobalConstraint(globalConstraint, address(avatar)); + Controller(avatar.owner()).unregisterSelf(address(avatar)); + } + +} diff --git a/test/etherGC.js b/test/etherGC.js new file mode 100644 index 00000000..94f08533 --- /dev/null +++ b/test/etherGC.js @@ -0,0 +1,113 @@ +import * as helpers from './helpers'; +const DAOToken = artifacts.require("./DAOToken.sol"); +const EtherGC = artifacts.require('./globalConstraints/EtherGC.sol'); +const Controller = artifacts.require("./Controller.sol"); +const Reputation = artifacts.require("./Reputation.sol"); +const Avatar = artifacts.require("./Avatar.sol"); +const ActionMock = artifacts.require("./ActionMock.sol"); +var constants = require('../test/constants'); + + +let reputation, avatar,token,controller,etherGC; +var periodLengthConst = 1000; +const setup = async function () { + token = await DAOToken.new("TEST","TST",0); + // set up a reputation system + reputation = await Reputation.new(); + avatar = await Avatar.new('name', token.address, reputation.address); + controller = await Controller.new(avatar.address,{gas: constants.ARC_GAS_LIMIT}); + await avatar.transferOwnership(controller.address); + etherGC = await EtherGC.new(); + await etherGC.initialize(avatar.address,periodLengthConst,web3.utils.toWei('5', "ether")); //periodLengthConst seconds ,5 eth +}; + +contract('EtherGC', accounts => { + it("initialize", async () => { + await setup(); + assert.equal(await etherGC.avatar(),avatar.address); + assert.equal(await etherGC.amountAllowedPerPeriod(),web3.utils.toWei('5', "ether")); + assert.equal(await etherGC.periodLength(),1000); + }); + + it("send ether check", async () => { + + await setup(); + try { + await etherGC.initialize(avatar.address,periodLengthConst,web3.utils.toWei('5', "ether")); //periodLengthConst seconds ,5 eth + assert(false,"cannpt init twice "); + } catch(ex){ + helpers.assertVMException(ex); + } + var startTime = await etherGC.startTime(); + + await controller.addGlobalConstraint(etherGC.address,helpers.NULL_HASH,avatar.address); + //move 10 ether to avatar + await web3.eth.sendTransaction({from:accounts[0],to:avatar.address, value: web3.utils.toWei('10', "ether")}); + await controller.sendEther(web3.utils.toWei('1', "ether"), accounts[2],avatar.address); + await controller.sendEther(web3.utils.toWei('4', "ether"), accounts[2],avatar.address); + + try { + await controller.sendEther(web3.utils.toWei('1', "ether"), accounts[2],avatar.address); + assert(false,"sendEther should fail due to the etherGC global constraint "); + } + catch(ex){ + helpers.assertVMException(ex); + } + helpers.increaseTime(periodLengthConst+1); + await controller.sendEther(web3.utils.toWei('1', "ether"), accounts[2],avatar.address); + await controller.sendEther(web3.utils.toWei('4', "ether"), accounts[2],avatar.address); + await web3.eth.sendTransaction({from:accounts[0],to:avatar.address, value: web3.utils.toWei('10', "ether")}); + var diff = ((await web3.eth.getBlock("latest")).timestamp - startTime.toNumber())% periodLengthConst; + //increment time for next period + helpers.increaseTime(periodLengthConst-diff); + await controller.sendEther(web3.utils.toWei('4', "ether"), accounts[2],avatar.address); + }); + + it("genericCall check", async () => { + + await setup(); + try { + await etherGC.initialize(avatar.address,periodLengthConst,web3.utils.toWei('5', "ether")); //periodLengthConst seconds ,5 eth + assert(false,"cannpt init twice "); + } catch(ex){ + helpers.assertVMException(ex); + } + var startTime = await etherGC.startTime(); + + await controller.addGlobalConstraint(etherGC.address,helpers.NULL_HASH,avatar.address); + //move 10 ether to avatar + await web3.eth.sendTransaction({from:accounts[0],to:avatar.address, value: web3.utils.toWei('10', "ether")}); + + let actionMock = await ActionMock.new(); + let a = 7; + let b = actionMock.address; + let c = "0x1234"; + const encodeABI = await new web3.eth.Contract(actionMock.abi).methods.test(a,b,c).encodeABI(); + await controller.genericCall(actionMock.address,encodeABI,avatar.address,web3.utils.toWei('1', "ether")); + await controller.genericCall(actionMock.address,encodeABI,avatar.address,web3.utils.toWei('4', "ether")); + + try { + await controller.sendEther(web3.utils.toWei('1', "ether"), accounts[2],avatar.address); + assert(false,"sendEther should fail due to the etherGC global constraint "); + } + catch(ex){ + helpers.assertVMException(ex); + } + + try { + await controller.genericCall(actionMock.address,encodeABI,avatar.address,web3.utils.toWei('1', "ether")); + assert(false,"sendEther should fail due to the etherGC global constraint "); + } + catch(ex){ + helpers.assertVMException(ex); + } + helpers.increaseTime(periodLengthConst+1); + await controller.genericCall(actionMock.address,encodeABI,avatar.address,web3.utils.toWei('1', "ether")); + await controller.genericCall(actionMock.address,encodeABI,avatar.address,web3.utils.toWei('4', "ether")); + await web3.eth.sendTransaction({from:accounts[0],to:avatar.address, value: web3.utils.toWei('10', "ether")}); + var diff = ((await web3.eth.getBlock("latest")).timestamp - startTime.toNumber())% periodLengthConst; + //increment time for next period + helpers.increaseTime(periodLengthConst-diff); + await controller.genericCall(actionMock.address,encodeABI,avatar.address,web3.utils.toWei('4', "ether")); + }); +}); diff --git a/test/globalconstraintaddorremove.js b/test/globalconstraintaddorremove.js new file mode 100644 index 00000000..5e575e2f --- /dev/null +++ b/test/globalconstraintaddorremove.js @@ -0,0 +1,64 @@ +const helpers = require('./helpers'); +const Controller = artifacts.require("./Controller.sol"); +const Reputation = artifacts.require("./Reputation.sol"); +const Avatar = artifacts.require("./Avatar.sol"); +const DAOToken = artifacts.require("./DAOToken.sol"); +const GlobalConstraintAddOrRemove = artifacts.require('./GlobalConstraintAddOrRemove.sol'); +const EtherGC = artifacts.require('./EtherGC.sol'); +var constants = require('../test/constants'); + +let reputation, avatar,token,controller,etherGC,globalConstraintAddEtherGC,globalConstraintRemoveEtherGC; + +const setup = async function (accounts) { + token = await DAOToken.new("TEST","TST",0); + // set up a reputation system + reputation = await Reputation.new(); + avatar = await Avatar.new('name', token.address, reputation.address); + controller = await Controller.new(avatar.address,{from:accounts[0], gas: constants.ARC_GAS_LIMIT}); + avatar.transferOwnership(controller.address); + etherGC = await EtherGC.new(); + await etherGC.initialize(avatar.address,10,web3.utils.toWei('5', "ether")); //10 blocks ,5 eth + + globalConstraintAddEtherGC = await GlobalConstraintAddOrRemove.new(); + await globalConstraintAddEtherGC.initialize(avatar.address,etherGC.address, helpers.NULL_HASH); + + await controller.registerScheme(globalConstraintAddEtherGC.address, helpers.NULL_HASH, "0x00000004" ,avatar.address); + globalConstraintRemoveEtherGC = await GlobalConstraintAddOrRemove.new(); + await globalConstraintRemoveEtherGC.initialize(avatar.address,etherGC.address, helpers.NULL_HASH); + await controller.registerScheme(globalConstraintRemoveEtherGC.address, helpers.NULL_HASH, "0x00000004" ,avatar.address); + + +}; + +contract('GlobalConstraintAddOrRemove', accounts => { + + it("initialize", async() => { + await setup(accounts); + assert.equal(await globalConstraintAddEtherGC.avatar(),avatar.address); + assert.equal(await globalConstraintAddEtherGC.globalConstraint(),etherGC.address); + try { + await globalConstraintAddEtherGC.initialize(avatar.address,etherGC.address, helpers.NULL_HASH); + assert(false, 'cannot init twice'); + } catch (ex) { + helpers.assertVMException(ex); + } + + }); + + it("register global constraint", async () => { + await setup(accounts); + assert.equal(await controller.isGlobalConstraintRegistered(etherGC.address,avatar.address),false); + await globalConstraintAddEtherGC.add(); + assert.equal(await controller.isGlobalConstraintRegistered(etherGC.address,avatar.address),true); + assert.equal(await controller.isSchemeRegistered(globalConstraintAddEtherGC.address,avatar.address),false); + }); + + it("remove global constraint", async () => { + await setup(accounts); + await globalConstraintAddEtherGC.add(); + assert.equal(await controller.isGlobalConstraintRegistered(etherGC.address,avatar.address),true); + await globalConstraintRemoveEtherGC.remove(); + assert.equal(await controller.isGlobalConstraintRegistered(etherGC.address,avatar.address),false); + assert.equal(await controller.isSchemeRegistered(globalConstraintRemoveEtherGC.address,avatar.address),false); + }); +}); diff --git a/test/reputationGC.js b/test/reputationGC.js new file mode 100644 index 00000000..36ab0176 --- /dev/null +++ b/test/reputationGC.js @@ -0,0 +1,102 @@ +import * as helpers from './helpers'; +const DAOToken = artifacts.require("./DAOToken.sol"); +const ReputationGC = artifacts.require('./globalConstraints/ReputationGC.sol'); +const Controller = artifacts.require("./Controller.sol"); +const Reputation = artifacts.require("./Reputation.sol"); +const Avatar = artifacts.require("./Avatar.sol"); +var constants = require('../test/constants'); + + +let reputation, avatar,token,controller,reputationGC; +var periodLengthConst = 1000; +const setup = async function (accounts) { + token = await DAOToken.new("TEST","TST",0); + // set up a reputation system + reputation = await Reputation.new(); + avatar = await Avatar.new('name', token.address, reputation.address); + controller = await Controller.new(avatar.address,{gas: constants.ARC_GAS_LIMIT}); + await avatar.transferOwnership(controller.address); + await reputation.transferOwnership(controller.address); + reputationGC = await ReputationGC.new(); + //mint 1000 reputation before the global constraint registration + await controller.mintReputation(1000, accounts[2],avatar.address); + await reputationGC.initialize(avatar.address,periodLengthConst,5); //1000 seconds ,5% +}; + +contract('ReputationGC', accounts => { + it("initialize", async () => { + await setup(accounts); + assert.equal(await reputationGC.avatar(),avatar.address); + assert.equal(await reputationGC.percentageAllowedPerPeriod(),5); + assert.equal(await reputationGC.periodLength(),periodLengthConst); + }); + + it("mint/burn reputation check", async () => { + + await setup(accounts); + try { + await reputationGC.initialize(avatar.address,periodLengthConst,5); //1000 seconds ,5% + assert(false,"cannpt init twice "); + } catch(ex){ + helpers.assertVMException(ex); + } + var startTime = await reputationGC.startTime(); + await controller.addGlobalConstraint(reputationGC.address,helpers.NULL_HASH,avatar.address); + + + try { + //try to mint more than 5 percentage + await controller.mintReputation(51, accounts[2],avatar.address); + assert(false,"mint rep should fail due to the reputationGC global constraint "); + } + catch(ex){ + helpers.assertVMException(ex); + } + + await controller.mintReputation(50, accounts[2],avatar.address); + assert.equal(await reputation.totalSupply(),1050); + + try { + await controller.burnReputation(51, accounts[2],avatar.address); + assert(false,"burn rep should fail due to the reputationGC global constraint "); + } + catch(ex){ + helpers.assertVMException(ex); + } + + await controller.burnReputation(50, accounts[2],avatar.address); + assert.equal(await reputation.totalSupply(),1000); + + try { + await controller.mintReputation(10, accounts[2],avatar.address); + assert(false,"mint rep should fail due to the reputationGC global constraint "); + } + catch(ex){ + helpers.assertVMException(ex); + } + var diff = ((await web3.eth.getBlock("latest")).timestamp - startTime.toNumber())% periodLengthConst; + //increment time for next period + helpers.increaseTime(periodLengthConst-diff); + + await controller.mintReputation(10, accounts[2],avatar.address); + assert.equal(await reputation.totalSupply(),1010); + + await controller.burnReputation(50, accounts[2],avatar.address); + assert.equal(await reputation.totalSupply(),960); + + try { + await controller.burnReputation(10, accounts[2],avatar.address); + assert(false,"burn rep should fail due to the reputationGC global constraint "); + } + catch(ex){ + helpers.assertVMException(ex); + } + + diff = ((await web3.eth.getBlock("latest")).timestamp - startTime.toNumber())% periodLengthConst; + //increment time for next period + helpers.increaseTime(periodLengthConst-diff); + await controller.burnReputation(10, accounts[2],avatar.address); + assert.equal(await reputation.totalSupply(),950); + + }); +});