diff --git a/contracts/schemes/ReputationAdmin.sol b/contracts/schemes/ReputationAdmin.sol new file mode 100644 index 00000000..08e4fef2 --- /dev/null +++ b/contracts/schemes/ReputationAdmin.sol @@ -0,0 +1,111 @@ +pragma solidity 0.6.10; +// SPDX-License-Identifier: GPL-3.0 + +import "@openzeppelin/contracts-ethereum-package/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol"; +import "../controller/Controller.sol"; +import "./ArcScheme.sol"; + +/** + * @title A scheme for reputation minting/burning by an authorized account + */ + +contract ReputationAdmin is OwnableUpgradeSafe, ArcScheme { + using SafeMath for uint256; + + uint256 public activationStartTime; + uint256 public activationEndTime; + uint256 public repRewardLeft; + uint256 public limitRepReward; + + /** + * @dev initialize + * @param _avatar the avatar to mint reputation from + * @param _activationStartTime start time for allowing minting + * @param _activationEndTime end time for allowing minting + * @param _maxRepReward maximum reputation mintable by this scheme + */ + function initialize( + Avatar _avatar, + uint256 _activationStartTime, + uint256 _activationEndTime, + uint256 _maxRepReward, + address _owner + ) external { + require(_activationStartTime < _activationEndTime, "_activationStartTime < _activationEndTime"); + super._initialize(_avatar); + activationStartTime = _activationStartTime; + activationEndTime = _activationEndTime; + repRewardLeft = _maxRepReward; + limitRepReward = _maxRepReward; + __Ownable_init_unchained(); + transferOwnership(_owner); + } + + /** + * @dev reputationBurn function + * @param _beneficiaries the beneficiaries address to mint reputation from + * @param _amounts the amounts of reputation to mint for beneficiaries + */ + function reputationMint(address[] calldata _beneficiaries, uint256[] calldata _amounts) external onlyOwner { + require(_beneficiaries.length == _amounts.length, "Arrays length mismatch"); + for (uint256 i=0; i < _beneficiaries.length; i++) { + _reputationMint(_beneficiaries[i], _amounts[i]); + } + } + + /** + * @dev reputationBurn function + * @param _beneficiaries the beneficiaries address to burm reputation from + * @param _amounts the amounts of reputation to burn for beneficiaries + */ + function reputationBurn(address[] calldata _beneficiaries, uint256[] calldata _amounts) external onlyOwner { + require(_beneficiaries.length == _amounts.length, "Arrays length mismatch"); + for (uint256 i=0; i < _beneficiaries.length; i++) { + _reputationBurn(_beneficiaries[i], _amounts[i]); + } + } + + /** + * @dev reputationMint function + * @param _beneficiary the beneficiary address to mint reputation for + * @param _amount the amount of reputation to mint the the beneficirary + */ + function _reputationMint(address _beneficiary, uint256 _amount) private { + // solhint-disable-next-line not-rely-on-time + require(now >= activationStartTime, "Minting period did not start yet"); + // solhint-disable-next-line not-rely-on-time + require(now < activationEndTime, "Minting period ended."); + + if (limitRepReward > 0) { + repRewardLeft = repRewardLeft.sub(_amount); + } + + require( + Controller(avatar.owner()).mintReputation(_amount, _beneficiary), + "Minting reputation should succeed" + ); + } + + /** + * @dev reputationBurn function + * @param _beneficiary the beneficiary address to burm reputation from + * @param _amount the amount of reputation to burn for a beneficirary + */ + function _reputationBurn(address _beneficiary, uint256 _amount) private { + // solhint-disable-next-line not-rely-on-time + require(now >= activationStartTime, "Burning period did not start yet"); + // solhint-disable-next-line not-rely-on-time + require(now < activationEndTime, "Burning period ended."); + + if (limitRepReward > 0) { + require(_amount <= limitRepReward.sub(repRewardLeft), "Cannot burn more than minted"); + repRewardLeft = repRewardLeft.add(_amount); + } + + require( + Controller(avatar.owner()).burnReputation(_amount, _beneficiary), + "Burn reputation should succeed" + ); + } +} diff --git a/package-lock.json b/package-lock.json index e4e54df5..b1cea8d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@daostack/arc-experimental", - "version": "0.1.2-rc.1", + "version": "0.1.2-rc.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index cad4cd54..30fbedfa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@daostack/arc-experimental", - "version": "0.1.2-rc.1", + "version": "0.1.2-rc.2", "description": "A platform for building DAOs", "files": [ "contracts/", diff --git a/test/helpers.js b/test/helpers.js index 9f0c4f07..8d8f75a0 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -37,6 +37,7 @@ const ARCVotingMachineCallbacksMock = artifacts.require("./ARCVotingMachineCallb const JoinAndQuit = artifacts.require("./JoinAndQuit.sol"); const FundingRequest = artifacts.require("./FundingRequest.sol"); const Dictator = artifacts.require("./Dictator.sol"); +const ReputationAdmin = artifacts.require("./ReputationAdmin.sol"); const MAX_UINT_256 = '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'; @@ -157,6 +158,7 @@ const SOME_ADDRESS = '0x1000000000000000000000000000000000000000'; registration.fundingRequest = await FundingRequest.new(); registration.rewarderMock = await RewarderMock.new(); registration.dictator = await Dictator.new(); + registration.reputationAdmin = await ReputationAdmin.new(); await implementationDirectory.setImplementation("DAOToken",registration.daoToken.address); @@ -188,6 +190,7 @@ const SOME_ADDRESS = '0x1000000000000000000000000000000000000000'; await implementationDirectory.setImplementation("JoinAndQuit",registration.joinAndQuit.address); await implementationDirectory.setImplementation("FundingRequest",registration.fundingRequest.address); await implementationDirectory.setImplementation("Dictator",registration.dictator.address); + await implementationDirectory.setImplementation("ReputationAdmin",registration.reputationAdmin.address); registration.implementationDirectory = implementationDirectory; diff --git a/test/reputationadmin.js b/test/reputationadmin.js new file mode 100644 index 00000000..67990f85 --- /dev/null +++ b/test/reputationadmin.js @@ -0,0 +1,248 @@ +const helpers = require('./helpers'); +const constants = require('./constants'); +var ReputationAdmin = artifacts.require('./ReputationAdmin.sol'); + +class ReputationAdminParams { + constructor() { + } +} + +const setup = async function( + accounts, + _activationStartTime = 0, + _activationEndTime = 3000, + _maxRepReward = 100, + _initialize = true +) { + var testSetup = new helpers.TestSetup(); + testSetup.proxyAdmin = accounts[5]; + var registration = await helpers.registerImplementation(); + testSetup.reputationAdminParams = new ReputationAdminParams(); + testSetup.activationStartTime = (await web3.eth.getBlock('latest')).timestamp + _activationStartTime; + testSetup.activationEndTime = (await web3.eth.getBlock('latest')).timestamp + _activationEndTime; + testSetup.maxRepReward = _maxRepReward; + + if (_initialize === true) { + testSetup.reputationAdminParams.initdata = await new web3.eth.Contract(registration.reputationAdmin.abi) + .methods + .initialize(helpers.NULL_ADDRESS, + testSetup.activationStartTime, + testSetup.activationEndTime, + testSetup.maxRepReward, + accounts[0]) + .encodeABI(); + } else { + testSetup.reputationAdminParams.initdata = Buffer.from(''); + } + + var permissions = "0x00000000"; + [testSetup.org,tx] = await helpers.setupOrganizationWithArraysDAOFactory( + testSetup.proxyAdmin, + accounts, + registration, + [accounts[0]], + [0], + [0], + 0, + [web3.utils.fromAscii("ReputationAdmin")], + testSetup.reputationAdminParams.initdata, + [helpers.getBytesLength(testSetup.reputationAdminParams.initdata)], + [permissions], + "metaData" + ); + testSetup.reputationAdmin = await ReputationAdmin.at(await helpers.getSchemeAddress(registration.daoFactory.address,tx)); + return testSetup; + }; + +contract('reputationAdmin', accounts => { + it('initialize', async () => { + let testSetup = await setup(accounts); + + assert.equal( + await testSetup.reputationAdmin.repRewardLeft(), + testSetup.maxRepReward + ); + assert.equal( + await testSetup.reputationAdmin.activationStartTime(), + testSetup.activationStartTime + ); + assert.equal( + await testSetup.reputationAdmin.activationEndTime(), + testSetup.activationEndTime + ); + assert.equal(await testSetup.reputationAdmin.owner(), accounts[0]); + }); + + it('initialize _activationStartTime >= activationEndTime is not allowed', async () => { + let testSetup = await setup(accounts); + let reputationAdmin = await ReputationAdmin.new(); + try { + await reputationAdmin.initialize( + testSetup.org.avatar.address, + testSetup.activationStartTime, + testSetup.activationStartTime - 1, + testSetup.maxRepReward, + accounts[0], + { gas: constants.ARC_GAS_LIMIT } + ); + assert(false, '_redeemEnableTime < auctionsEndTime is not allowed'); + } catch (error) { + helpers.assertVMException(error); + } + }); + + it('mint reputation', async () => { + let testSetup = await setup(accounts); + await testSetup.reputationAdmin.reputationMint([accounts[2]],[1]); + + assert.equal(await testSetup.org.reputation.balanceOf(accounts[2]), 1); + }); + + it('burn reputation', async () => { + let testSetup = await setup(accounts); + await testSetup.reputationAdmin.reputationMint([accounts[2]],[1]); + + assert.equal(await testSetup.org.reputation.balanceOf(accounts[2]), 1); + await testSetup.reputationAdmin.reputationBurn([accounts[2]],[1]); + assert.equal(await testSetup.org.reputation.balanceOf(accounts[2]), 0); + + }); + + it('burn only if minted reputation', async () => { + let testSetup = await setup(accounts); + try { + await testSetup.reputationAdmin.reputationBurn([accounts[2]],[1]); + assert(false, 'burn only if minted reputation'); + } catch (error) { + helpers.assertVMException(error); + } + + }); + + it('mint reputation by unauthorized account should fail', async () => { + let testSetup = await setup(accounts); + try { + await testSetup.reputationAdmin.reputationMint([accounts[2]], [1], { + from: accounts[1] + }); + assert(false, 'mint reputation by unauthorized account should fail'); + } catch (error) { + helpers.assertVMException(error); + } + }); + + it('mint without initialize should fail', async () => { + let testSetup = await setup(accounts, 0, 3000, 100, false); + try { + await testSetup.reputationAdmin.reputationMint([accounts[2]], [1]); + assert(false, 'mint without initialize should fail'); + } catch (error) { + helpers.assertVMException(error); + } + }); + + it('mint before _activationStartTime should fail', async () => { + let testSetup = await setup(accounts, 2000, 3000, 100, true); + try { + await testSetup.reputationAdmin.reputationMint([accounts[2]],[1]); + assert(false, 'mint before _activationStartTime should fail'); + } catch (error) { + helpers.assertVMException(error); + } + }); + + it('mint after _activationEndTime should revert', async () => { + let testSetup = await setup(accounts); + await helpers.increaseTime(3001); + try { + await testSetup.reputationAdmin.reputationMint([accounts[2]], [1]); + assert(false, 'mint after _activationEndTime should revert'); + } catch (error) { + helpers.assertVMException(error); + } + }); + + it('mint more than _maxRepReward should fail', async () => { + let testSetup = await setup(accounts); + try { + await testSetup.reputationAdmin.reputationMint([accounts[2]], [101]); + assert(false, 'mint more than _maxRepReward should fail'); + } catch (error) { + helpers.assertVMException(error); + } + }); + + it('mint with un-matching array lengths should fail', async () => { + let testSetup = await setup(accounts); + try { + await testSetup.reputationAdmin.reputationMint([accounts[2], accounts[3]], [1]); + assert(false, 'mint with un-matching array lengths should fail'); + } catch (error) { + helpers.assertVMException(error); + } + }); + + it('burn with un-matching array lengths should fail', async () => { + let testSetup = await setup(accounts); + await testSetup.reputationAdmin.reputationMint([accounts[2], accounts[3]], [1, 1]); + + assert.equal(await testSetup.org.reputation.balanceOf(accounts[2]), 1); + assert.equal(await testSetup.org.reputation.balanceOf(accounts[3]), 1); + try { + await testSetup.reputationAdmin.reputationBurn([accounts[2], accounts[3]], [1]); + assert(false, 'burn with un-matching array lengths should fail'); + } catch (error) { + helpers.assertVMException(error); + } + }); + + it('burn before _activationStartTime should fail', async () => { + let testSetup = await setup(accounts, 2000, 3000, 100, true); + try { + await testSetup.reputationAdmin.reputationBurn([accounts[2]],[1]); + assert(false, 'burn before _activationStartTime should fail'); + } catch (error) { + helpers.assertVMException(error); + } + }); + + it('burn after _activationEndTime should revert', async () => { + let testSetup = await setup(accounts); + await testSetup.reputationAdmin.reputationMint([accounts[2]], [1]); + assert.equal(await testSetup.org.reputation.balanceOf(accounts[2]), 1); + await helpers.increaseTime(3001); + try { + await testSetup.reputationAdmin.reputationBurn([accounts[2]], [1]); + assert(false, 'burn after _activationEndTime should revert'); + } catch (error) { + helpers.assertVMException(error); + } + }); + + it('mint and burn reputation with 0 _maxRepReward should allow any amount', async () => { + let testSetup = await setup(accounts, 0, 3000, 0, true); + await testSetup.reputationAdmin.reputationMint([accounts[2]], [1000]); + + assert.equal(await testSetup.org.reputation.balanceOf(accounts[2]), 1000); + await testSetup.reputationAdmin.reputationBurn([accounts[2]], [1000]); + + assert.equal(await testSetup.org.reputation.balanceOf(accounts[2]), 0); + }); + + it('cannot initialize twice', async () => { + let testSetup = await setup(accounts); + try { + await testSetup.reputationAdmin.initialize( + testSetup.org.avatar.address, + testSetup.activationStartTime, + testSetup.activationEndTime, + testSetup.maxRepReward, + accounts[0], + { gas: constants.ARC_GAS_LIMIT } + ); + assert(false, 'cannot initialize twice'); + } catch (error) { + helpers.assertVMException(error); + } + }); +});