diff --git a/contracts/schemes/ContinuousLockingToken4Reputation.sol b/contracts/schemes/ContinuousLockingToken4Reputation.sol new file mode 100644 index 00000000..a6edc6c3 --- /dev/null +++ b/contracts/schemes/ContinuousLockingToken4Reputation.sol @@ -0,0 +1,306 @@ +pragma solidity ^0.5.11; + +import "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import "openzeppelin-solidity/contracts/math/Math.sol"; +import "../controller/ControllerInterface.sol"; +import "../libs/SafeERC20.sol"; +import "./Agreement.sol"; +import { RealMath } from "@daostack/infra/contracts/libs/RealMath.sol"; + +/** + * @title A scheme for continuous locking ERC20 Token for reputation + */ + +contract ContinuousLocking4Reputation is Agreement { + using SafeMath for uint256; + using SafeERC20 for address; + using RealMath for uint216; + using RealMath for uint256; + using Math for uint256; + + event Redeem(bytes32 indexed _lockingId, address indexed _beneficiary, uint256 _amount); + event Release(bytes32 indexed _lockingId, address indexed _beneficiary, uint256 _amount); + event LockToken(address indexed _locker, bytes32 indexed _lockingId, uint256 _amount, uint256 _period); + event ExtendLocking(address indexed _locker, bytes32 indexed _lockingId, uint256 _extendPeriod); + + struct Batch { + uint256 totalScore; + // A mapping from locker addresses to their locking score. + mapping(address=>uint256) scores; + } + + struct Lock { + uint256 amount; + uint256 lockingTime; + uint256 period; + } + + // A mapping from lockers addresses to their locks. + mapping(address => mapping(bytes32=>Lock)) public lockers; + //A mapping from batch index to batch. + mapping(uint256 => Batch) public batches; + + Avatar public avatar; + uint256 public reputationRewardLeft; + uint256 public startTime; + uint256 public redeemEnableTime; + uint256 public maxLockingBatches; + uint256 public batchTime; + IERC20 public token; + uint256 public batchesCounter; // Total number of batches + uint256 public totalLockedLeft; + uint256 public repRewardConstA; + uint256 public repRewardConstB; + uint256 public batchesIndexCap; + + uint256 constant private REAL_FBITS = 40; + /** + * What's the first non-fractional bit + */ + + uint256 constant private REAL_ONE = uint256(1) << REAL_FBITS; + uint256 constant private BATCHES_INDEX_HARDCAP = 100; + uint256 constant public MAX_LOCKING_BATCHES_HARDCAP = 24; + + /** + * @dev initialize + * @param _avatar the avatar to mint reputation from + * @param _reputationReward the reputation reward per locking batch that this contract will reward + * for the token locking + * @param _startTime locking period start time + * @param _batchTime batch time (e.g 30 days). + * @param _redeemEnableTime redeem enable time . + * redeem reputation can be done after this time. + * @param _maxLockingBatches - maximum number of locking batches (in _batchTime units) + * @param _repRewardConstA - reputation allocation per batch is calculated by : + * _repRewardConstA * (_repRewardConstB ** batchIndex) + * @param _repRewardConstB - reputation allocation per batch is calculated by : + * _repRewardConstA * (_repRewardConstB ** batchIndex) + * @param _batchesIndexCap the max batch index which allows to lock in . this value capped by BATCHES_HARDCAP + * @param _token the locking token + * @param _agreementHash is a hash of agreement required to be added to the TX by participants + */ + function initialize( + Avatar _avatar, + uint256 _reputationReward, + uint256 _startTime, + uint256 _batchTime, + uint256 _redeemEnableTime, + uint256 _maxLockingBatches, + uint256 _repRewardConstA, + uint256 _repRewardConstB, + uint256 _batchesIndexCap, + IERC20 _token, + bytes32 _agreementHash ) + external + { + require(avatar == Avatar(0), "can be called only one time"); + require(_avatar != Avatar(0), "avatar cannot be zero"); + //_batchTime should be greater than block interval + require(_batchTime > 15, "batchTime should be > 15"); + require(_maxLockingBatches <= MAX_LOCKING_BATCHES_HARDCAP, + "maxLockingBatches should be <= MAX_LOCKING_BATCHES_HARDCAP"); + require(_redeemEnableTime >= _startTime+_batchTime, + "_redeemEnableTime >= _startTime+_batchTime"); + require(_batchesIndexCap <= BATCHES_INDEX_HARDCAP, "_batchesIndexCap > BATCHES_INDEX_HARDCAP"); + token = _token; + avatar = _avatar; + startTime = _startTime; + reputationRewardLeft = _reputationReward; + redeemEnableTime = _redeemEnableTime; + maxLockingBatches = _maxLockingBatches; + batchTime = _batchTime; + require(_repRewardConstB < 1000, "_repRewardConstB should be < 1000"); + require(repRewardConstA < _reputationReward, "repRewardConstA should be < _reputationReward"); + repRewardConstA = toReal(uint216(_repRewardConstA)); + repRewardConstB = uint216(_repRewardConstB).fraction(uint216(1000)); + batchesIndexCap = _batchesIndexCap; + super.setAgreementHash(_agreementHash); + } + + /** + * @dev redeem reputation function + * @param _beneficiary the beneficiary to redeem. + * @param _lockingId the lockingId to redeem from. + * @return uint256 reputation rewarded + */ + function redeem(address _beneficiary, bytes32 _lockingId) public returns(uint256 reputation) { + // solhint-disable-next-line not-rely-on-time + require(now > redeemEnableTime, "now > redeemEnableTime"); + Lock storage locker = lockers[_beneficiary][_lockingId]; + uint256 batchIndexToRedeemFrom = (locker.lockingTime - startTime) / batchTime; + // solhint-disable-next-line not-rely-on-time + uint256 currentBatch = (now - startTime) / batchTime; + uint256 lastBatchIndexToRedeem = currentBatch.min(batchIndexToRedeemFrom + locker.period); + for (batchIndexToRedeemFrom; batchIndexToRedeemFrom < lastBatchIndexToRedeem; batchIndexToRedeemFrom++) { + Batch storage locking = batches[batchIndexToRedeemFrom]; + uint256 score = locking.scores[_beneficiary]; + if (score > 0) { + locking.scores[_beneficiary] = 0; + uint256 batchReputationReward = repRewardPerBatch(batchIndexToRedeemFrom); + uint256 repRelation = mul(toReal(uint216(score)), batchReputationReward); + reputation = reputation.add(div(repRelation, toReal(uint216(locking.totalScore)))); + } + } + reputation = uint256(fromReal(reputation)); + require(reputation > 0, "reputation to redeem is 0"); + // check that the reputation is sum zero + reputationRewardLeft = reputationRewardLeft.sub(reputation); + require( + ControllerInterface(avatar.owner()) + .mintReputation(reputation, _beneficiary, address(avatar)), "mint reputation should succeed"); + emit Redeem(_lockingId, _beneficiary, reputation); + } + + /** + * @dev lock function + * @param _amount the amount of token to lock + * @param _period the period to lock. in batchTime units + * @param _batchIndexToLockIn the locking id to lock in. + * @return lockingId + */ + function lock(uint256 _amount, uint256 _period, uint256 _batchIndexToLockIn, bytes32 _agreementHash) + public + onlyAgree(_agreementHash) + returns(bytes32 lockingId) + { + require(_amount > 0, "locking amount should be > 0"); + // solhint-disable-next-line not-rely-on-time + require(now >= startTime, "locking is enable only after locking startTime"); + require(_period <= maxLockingBatches, "period exceed the maximum allowed"); + require(_period > 0, "period equal to zero"); + require((_batchIndexToLockIn + _period) <= batchesIndexCap, "exceed max allowed batches"); + address(token).safeTransferFrom(msg.sender, address(this), _amount); + // solhint-disable-next-line not-rely-on-time + uint256 batchIndexToLockIn = (now - startTime) / batchTime; + require(batchIndexToLockIn == _batchIndexToLockIn, "locking is not active"); + uint256 j = _period; + //fill in the next batches scores. + for (int256 i = int256(batchIndexToLockIn + _period - 1); i >= int256(batchIndexToLockIn); i--) { + Batch storage batch = batches[uint256(i)]; + uint256 score = (_period - j + 1) * _amount; + j--; + batch.totalScore = batch.totalScore.add(score); + batch.scores[msg.sender] = score; + } + + lockingId = keccak256(abi.encodePacked(address(this), batchesCounter)); + batchesCounter = batchesCounter.add(1); + + Lock storage locker = lockers[msg.sender][lockingId]; + locker.amount = _amount; + locker.period = _period; + // solhint-disable-next-line not-rely-on-time + locker.lockingTime = now; + totalLockedLeft = totalLockedLeft.add(_amount); + emit LockToken(msg.sender, lockingId, _amount, _period); + } + + /** + * @dev extendLocking function + * @param _extendPeriod the period to extend the locking. in batchTime. + * @param _batchIndexToLockIn the locking id to lock at . + * @param _lockingId the locking id to extend + */ + function extendLocking( + uint256 _extendPeriod, + uint256 _batchIndexToLockIn, + bytes32 _lockingId, + bytes32 _agreementHash) + public + onlyAgree(_agreementHash) + { + Lock storage locker = lockers[msg.sender][_lockingId]; + require(locker.lockingTime != 0, "wrong locking id"); + uint256 remainBatches = + ((locker.lockingTime + (locker.period*batchTime) - startTime)/batchTime).sub(_batchIndexToLockIn); + uint256 batchesCountFromCurrent = remainBatches + _extendPeriod; + require(batchesCountFromCurrent <= maxLockingBatches, "locking period exceed the maximum allowed"); + require(_extendPeriod > 0, "extend locking period equal to zero"); + require((_batchIndexToLockIn + batchesCountFromCurrent) <= batchesIndexCap, + "exceed max allowed batches"); + // solhint-disable-next-line not-rely-on-time + uint256 batchIndexToLockIn = (now - startTime) / batchTime; + require(batchIndexToLockIn == _batchIndexToLockIn, "locking is not active"); + uint256 j = batchesCountFromCurrent; + //fill in the next batche scores. + for (int256 i = int256(batchIndexToLockIn + batchesCountFromCurrent - 1); + i >= int256(batchIndexToLockIn); + i--) { + Batch storage batch = batches[uint256(i)]; + uint256 score = (batchesCountFromCurrent - j + 1) * locker.amount; + j--; + batch.totalScore = batch.totalScore.add(score).sub(batch.scores[msg.sender]); + batch.scores[msg.sender] = score; + } + locker.period = locker.period + _extendPeriod; + emit ExtendLocking(msg.sender, _lockingId, _extendPeriod); + } + + /** + * @dev release function + * @param _beneficiary the beneficiary for the release + * @param _lockingId the locking id to release + * @return bool + */ + function release(address _beneficiary, bytes32 _lockingId) public returns(uint256 amount) { + Lock storage locker = lockers[_beneficiary][_lockingId]; + require(locker.amount > 0, "amount should be > 0"); + amount = locker.amount; + locker.amount = 0; + // solhint-disable-next-line not-rely-on-time + require(block.timestamp > locker.lockingTime + (locker.period*batchTime), + "locking period not passed"); + totalLockedLeft = totalLockedLeft.sub(amount); + address(token).safeTransfer(_beneficiary, amount); + emit Release(_lockingId, _beneficiary, amount); + } + + /** + * @dev repRewardPerBatch function + * the calculation is done the following formula: + * RepReward = repRewardConstA * (repRewardConstB**_batchIndex) + * @param _batchIndex the batch number to calc rep reward of + * @return repReward + */ + function repRewardPerBatch(uint256 _batchIndex) public view returns(uint256 repReward) { + if (_batchIndex <= batchesIndexCap) { + repReward = mul(repRewardConstA, repRewardConstB.pow(_batchIndex)); + } + } + + /** + * Multiply one real by another. Truncates overflows. + */ + function mul(uint256 realA, uint256 realB) private pure returns (uint256) { + // When multiplying fixed point in x.y and z.w formats we get (x+z).(y+w) format. + // So we just have to clip off the extra REAL_FBITS fractional bits. + uint256 res = realA * realB; + require(res/realA == realB, "RealMath mul overflow"); + return (res >> REAL_FBITS); + } + + /** + * Convert an integer to a real. Preserves sign. + */ + function toReal(uint216 ipart) private pure returns (uint256) { + return uint256(ipart) * REAL_ONE; + } + + /** + * Convert a real to an integer. Preserves sign. + */ + function fromReal(uint256 _realValue) private pure returns (uint216) { + return uint216(_realValue / REAL_ONE); + } + + /** + * Divide one real by another real. Truncates overflows. + */ + function div(uint256 realNumerator, uint256 realDenominator) private pure returns (uint256) { + // We use the reverse of the multiplication trick: convert numerator from + // x.y to (x+z).(y+w) fixed point, then divide by denom in z.w fixed point. + return uint256((uint256(realNumerator) * REAL_ONE) / uint256(realDenominator)); + } + +} diff --git a/test/continuouslockingtoken4reputation.js b/test/continuouslockingtoken4reputation.js new file mode 100644 index 00000000..7391e3bf --- /dev/null +++ b/test/continuouslockingtoken4reputation.js @@ -0,0 +1,517 @@ +const helpers = require('./helpers'); +const DaoCreator = artifacts.require("./DaoCreator.sol"); +const ControllerCreator = artifacts.require("./ControllerCreator.sol"); +const constants = require('./constants'); +const ERC20Mock = artifacts.require('./test/ERC20Mock.sol'); +var ContinuousLocking4Reputation = artifacts.require("./ContinuousLocking4Reputation.sol"); + + +const setup = async function (accounts, + _initialize = true, + _reputationReward = 850000, + _startTime = 0, + _periodsUnit = (30*60*60), + _redeemEnableTime = (30*60*60), + _maxLockingPeriod = 12, + _repRewardConstA = 85000, + _repRewardConstB = 900, + _periodsCap = 100, + _agreementHash = helpers.SOME_HASH + ) { + var testSetup = new helpers.TestSetup(); + testSetup.lockingToken = await ERC20Mock.new(accounts[0], web3.utils.toWei('100', "ether")); + var controllerCreator = await ControllerCreator.new({gas: constants.ARC_GAS_LIMIT}); + testSetup.daoCreator = await DaoCreator.new(controllerCreator.address,{gas:constants.ARC_GAS_LIMIT}); + + testSetup.org = await helpers.setupOrganization(testSetup.daoCreator,accounts[0],1000,1000); + testSetup.startTime = (await web3.eth.getBlock("latest")).timestamp + _startTime; + testSetup.redeemEnableTime = (await web3.eth.getBlock("latest")).timestamp + _redeemEnableTime; + testSetup.continuousLocking4Reputation = await ContinuousLocking4Reputation.new(); + testSetup.periodsUnit = _periodsUnit; + testSetup.agreementHash = _agreementHash; + testSetup.maxLockingPeriod = _maxLockingPeriod; + + testSetup.repRewardConstA = _repRewardConstA; + testSetup.repRewardConstB = _repRewardConstB; + testSetup.reputationReward = _reputationReward; + testSetup.periodsCap = _periodsCap; + if (_initialize === true ) { + await testSetup.continuousLocking4Reputation.initialize(testSetup.org.avatar.address, + testSetup.reputationReward, + testSetup.startTime, + testSetup.periodsUnit, + testSetup.redeemEnableTime, + testSetup.maxLockingPeriod, + testSetup.repRewardConstA, + testSetup.repRewardConstB, + testSetup.periodsCap, + testSetup.lockingToken.address, + testSetup.agreementHash, + {gas : constants.ARC_GAS_LIMIT}); + } + + var permissions = "0x00000000"; + await testSetup.daoCreator.setSchemes(testSetup.org.avatar.address,[testSetup.continuousLocking4Reputation.address],[web3.utils.asciiToHex("0")],[permissions],"metaData"); + await testSetup.lockingToken.approve(testSetup.continuousLocking4Reputation.address,web3.utils.toWei('100', "ether")); + return testSetup; +}; + +contract('ContinuousLocking4Reputation', accounts => { + it("initialize", async () => { + let testSetup = await setup(accounts); + assert.equal(await testSetup.continuousLocking4Reputation.reputationRewardLeft(),testSetup.reputationReward); + assert.equal(await testSetup.continuousLocking4Reputation.startTime(),testSetup.startTime); + assert.equal(await testSetup.continuousLocking4Reputation.redeemEnableTime(),testSetup.redeemEnableTime); + assert.equal(await testSetup.continuousLocking4Reputation.token(),testSetup.lockingToken.address); + assert.equal(await testSetup.continuousLocking4Reputation.batchTime(),testSetup.periodsUnit); + assert.equal(await testSetup.continuousLocking4Reputation.getAgreementHash(),testSetup.agreementHash); + assert.equal(await testSetup.continuousLocking4Reputation.batchesIndexCap(),testSetup.periodsCap); + }); + + it("initialize periodsUnit <= 15 seconds is not allowed", async () => { + let testSetup = await setup(accounts,false); + try { + await testSetup.continuousLocking4Reputation.initialize(testSetup.org.avatar.address, + testSetup.reputationReward, + testSetup.startTime, + 1, + testSetup.redeemEnableTime, + testSetup.maxLockingPeriod, + testSetup.repRewardConstA, + testSetup.repRewardConstB, + testSetup.periodsCap, + testSetup.lockingToken.address, + testSetup.agreementHash, + {gas : constants.ARC_GAS_LIMIT}); + assert(false, "periodsUnit < 15 is not allowed"); + } catch(error) { + helpers.assertVMException(error); + } + }); + + it("initialize _redeemEnableTime < _startTime+_periodsUnit is not allowed", async () => { + let testSetup = await setup(accounts,false); + try { + await testSetup.continuousLocking4Reputation.initialize(testSetup.org.avatar.address, + testSetup.reputationReward, + testSetup.startTime, + testSetup.periodsUnit, + testSetup.startTime + testSetup.periodsUnit -7, + testSetup.maxLockingPeriod, + testSetup.repRewardConstA, + testSetup.repRewardConstB, + testSetup.periodsCap, + testSetup.lockingToken.address, + testSetup.agreementHash, + {gas : constants.ARC_GAS_LIMIT}); + assert(false, "_redeemEnableTime < _startTime+_periodsUnit is not allowed"); + } catch(error) { + helpers.assertVMException(error); + } + }); + + it("period cap", async () => { + let testSetup = await setup(accounts,false); + try { + await testSetup.continuousLocking4Reputation.initialize(testSetup.org.avatar.address, + testSetup.reputationReward, + testSetup.startTime, + testSetup.periodsUnit, + testSetup.startTime, + testSetup.maxLockingPeriod, + testSetup.repRewardConstA, + testSetup.repRewardConstB, + testSetup.periodsCap +1, + testSetup.lockingToken.address, + testSetup.agreementHash, + {gas : constants.ARC_GAS_LIMIT}); + assert(false, "period cap cannot be greater than 100"); + } catch(error) { + helpers.assertVMException(error); + } + }); + + + it("lock", async () => { + let testSetup = await setup(accounts); + var tx = await testSetup.continuousLocking4Reputation.lock(web3.utils.toWei('1', "ether"), 12 , 0, testSetup.agreementHash); + var id = await helpers.getValueFromLogs(tx, '_lockingId',1); + assert.equal(tx.logs.length,1); + assert.equal(tx.logs[0].event,"LockToken"); + assert.equal(tx.logs[0].args._lockingId,id); + assert.equal(tx.logs[0].args._amount,web3.utils.toWei('1', "ether")); + assert.equal(tx.logs[0].args._locker,accounts[0]); + //test the tokens moved to the wallet. + assert.equal(await testSetup.lockingToken.balanceOf(testSetup.continuousLocking4Reputation.address),web3.utils.toWei('1', "ether")); + }); + + + it("lock without initialize should fail", async () => { + let testSetup = await setup(accounts,false); + try { + await testSetup.continuousLocking4Reputation.lock(web3.utils.toWei('1', "ether"),1,0, testSetup.agreementHash); + assert(false, "lock without initialize should fail"); + } catch(error) { + helpers.assertVMException(error); + } + }); + + it("lock with wrong agreementHash should fail", async () => { + let testSetup = await setup(accounts); + try { + await testSetup.continuousLocking4Reputation.lock(web3.utils.toWei('1', "ether"),1,0, helpers.NULL_HASH); + assert(false, "lock with wrong agreementHash should fail"); + } catch(error) { + helpers.assertVMException(error); + } + }); + + it("lock with value == 0 should revert", async () => { + let testSetup = await setup(accounts); + try { + await testSetup.continuousLocking4Reputation.lock(web3.utils.toWei('0', "ether"),1,0,testSetup.agreementHash); + assert(false, "lock with value == 0 should revert"); + } catch(error) { + helpers.assertVMException(error); + } + }); + + it("lock with period over maxLockingPeriod should revert", async () => { + let testSetup = await setup(accounts); + try { + await testSetup.continuousLocking4Reputation.lock(web3.utils.toWei('0', "ether"),testSetup.maxLockingPeriod +1 ,0,testSetup.agreementHash); + assert(false, "lock with period over maxLockingPeriod should revert"); + } catch(error) { + helpers.assertVMException(error); + } + }); + + it("redeem", async () => { + let testSetup = await setup(accounts); + var period = 12; + var tx = await testSetup.continuousLocking4Reputation.lock(web3.utils.toWei('1', "ether"),period,0,testSetup.agreementHash); + var id = await helpers.getValueFromLogs(tx, '_lockingId',1); + await helpers.increaseTime(testSetup.periodsUnit * period +1); + tx = await testSetup.continuousLocking4Reputation.redeem(accounts[0],id); + var redeemAmount = 0; + for (var lockingPeriodToRedeemFrom = 0; lockingPeriodToRedeemFrom < period; lockingPeriodToRedeemFrom++) { + redeemAmount += testSetup.repRewardConstA * (Math.pow((testSetup.repRewardConstB/1000),lockingPeriodToRedeemFrom)); + } + redeemAmount = Math.floor(redeemAmount); + assert.equal(tx.logs.length,1); + assert.equal(tx.logs[0].event,"Redeem"); + assert.equal(tx.logs[0].args._amount.toNumber(),redeemAmount); + assert.equal(tx.logs[0].args._beneficiary,accounts[0]); + assert.equal(await testSetup.org.reputation.balanceOf(accounts[0]),1000+redeemAmount); + }); + + it("redeem part of the periods", async () => { + let testSetup = await setup(accounts); + var period = 12; + var tx = await testSetup.continuousLocking4Reputation.lock(web3.utils.toWei('1', "ether"),period,0,testSetup.agreementHash); + var id = await helpers.getValueFromLogs(tx, '_lockingId',1); + await helpers.increaseTime(testSetup.periodsUnit * 3 +1); + tx = await testSetup.continuousLocking4Reputation.redeem(accounts[0],id); + var redeemAmount = 230349; + // for (var lockingPeriodToRedeemFrom = 0; lockingPeriodToRedeemFrom < 3; lockingPeriodToRedeemFrom++) { + // redeemAmount += testSetup.repRewardConstA * (Math.pow((testSetup.repRewardConstB/1000),lockingPeriodToRedeemFrom)); + // } + + //redeemAmount = Math.round(redeemAmount); + + assert.equal(tx.logs.length,1); + assert.equal(tx.logs[0].event,"Redeem"); + assert.equal(tx.logs[0].args._amount.toNumber(),redeemAmount); + assert.equal(tx.logs[0].args._beneficiary,accounts[0]); + assert.equal(await testSetup.org.reputation.balanceOf(accounts[0]),1000+redeemAmount); + + await helpers.increaseTime(testSetup.periodsUnit * 9 +1); + tx = await testSetup.continuousLocking4Reputation.redeem(accounts[0],id); + redeemAmount = 0; + for (var lockingPeriodToRedeemFrom = 3; lockingPeriodToRedeemFrom < period; lockingPeriodToRedeemFrom++) { + redeemAmount += testSetup.repRewardConstA * (Math.pow((testSetup.repRewardConstB/1000),lockingPeriodToRedeemFrom)); + } + + redeemAmount = Math.round(redeemAmount) - 1; + assert.equal(tx.logs.length,1); + assert.equal(tx.logs[0].event,"Redeem"); + assert.equal(tx.logs[0].args._amount.toNumber(),redeemAmount); + assert.equal(tx.logs[0].args._beneficiary,accounts[0]); + assert.equal(await testSetup.org.reputation.balanceOf(accounts[0]),1000+redeemAmount + 230349); + + }); + + it("redeem score ", async () => { + let testSetup = await setup(accounts); + var tx = await testSetup.continuousLocking4Reputation.lock(web3.utils.toWei('1', "ether"),1,0,testSetup.agreementHash,{from:accounts[0]}); + var id1 = await helpers.getValueFromLogs(tx, '_lockingId',1); + await testSetup.lockingToken.transfer(accounts[1],web3.utils.toWei('3', "ether")); + await testSetup.lockingToken.approve(testSetup.continuousLocking4Reputation.address,web3.utils.toWei('100', "ether"),{from:accounts[1]}); + tx = await testSetup.continuousLocking4Reputation.lock(web3.utils.toWei('3', "ether"),1,0,testSetup.agreementHash,{from:accounts[1]}); + var id2 = await helpers.getValueFromLogs(tx, '_lockingId',1); + await helpers.increaseTime(testSetup.periodsUnit +1); + await testSetup.continuousLocking4Reputation.redeem(accounts[0],id1); + await testSetup.continuousLocking4Reputation.redeem(accounts[1],id2); + assert.equal(await testSetup.org.reputation.balanceOf(accounts[0]),1000+85000/4); + assert.equal(await testSetup.org.reputation.balanceOf(accounts[1]),85000*3/4); + }); + + it("redeem cannot redeem twice", async () => { + let testSetup = await setup(accounts); + var tx = await testSetup.continuousLocking4Reputation.lock(web3.utils.toWei('1', "ether"),1,0,testSetup.agreementHash); + var id = await helpers.getValueFromLogs(tx, '_lockingId',1); + await helpers.increaseTime(testSetup.periodsUnit +1); + await testSetup.continuousLocking4Reputation.redeem(accounts[0],id); + try { + await testSetup.continuousLocking4Reputation.redeem(accounts[0],id); + assert(false, "cannot redeem twice"); + } catch(error) { + helpers.assertVMException(error); + } + }); + + it("redeem before redeemEnableTime should revert", async () => { + let testSetup = await setup(accounts); + var tx = await testSetup.continuousLocking4Reputation.lock(web3.utils.toWei('1', "ether"),1,0,testSetup.agreementHash); + var id = await helpers.getValueFromLogs(tx, '_lockingId',1); + + try { + await testSetup.continuousLocking4Reputation.redeem(accounts[0],id); + assert(false, "redeem before redeemEnableTime should revert"); + } catch(error) { + helpers.assertVMException(error); + } + await helpers.increaseTime(testSetup.redeemEnableTime); + await testSetup.continuousLocking4Reputation.redeem(accounts[0],id); + }); + + it("lock and redeem from all lockings", async () => { + let testSetup = await setup(accounts); + var tx = await testSetup.continuousLocking4Reputation.lock(web3.utils.toWei('1', "ether"),1,0,testSetup.agreementHash); + var id1 = await helpers.getValueFromLogs(tx, '_lockingId',1); + await helpers.increaseTime(testSetup.periodsUnit+1); + tx = await testSetup.continuousLocking4Reputation.lock(web3.utils.toWei('1', "ether"),1,1,testSetup.agreementHash); + var id2 = await helpers.getValueFromLogs(tx, '_lockingId',1); + await helpers.increaseTime(testSetup.periodsUnit+1); + tx = await testSetup.continuousLocking4Reputation.lock(web3.utils.toWei('1', "ether"),1,2,testSetup.agreementHash); + var id3 = await helpers.getValueFromLogs(tx, '_lockingId',1); + await helpers.increaseTime((testSetup.periodsUnit+1)*3); + //todo oren-- fill this up :) + // var totalBid1 = await testSetup.continuousLocking4Reputation.auctions(id1); + // var totalBid2 = await testSetup.continuousLocking4Reputation.auctions(id2); + // var totalBid3 = await testSetup.continuousLocking4Reputation.auctions(id3); + // assert.equal(web3.utils.BN(totalBid1).eq(web3.utils.BN(totalBid2)),true); + // assert.equal(web3.utils.BN(totalBid1).eq(web3.utils.BN(totalBid3)),true); + // assert.equal(totalBid1,web3.utils.toWei('1', "ether")); + // assert.equal(id1,0); + // assert.equal(id2,1); + // assert.equal(id3,2); + await testSetup.continuousLocking4Reputation.redeem(accounts[0],id1); + await testSetup.continuousLocking4Reputation.redeem(accounts[0],id2); + await testSetup.continuousLocking4Reputation.redeem(accounts[0],id3); + // assert.equal(await testSetup.org.reputation.balanceOf(accounts[0]),1000+300); + }); + + it("cannot initialize twice", async () => { + let testSetup = await setup(accounts); + try { + await testSetup.continuousLocking4Reputation.initialize(testSetup.org.avatar.address, + testSetup.reputationReward, + testSetup.startTime, + testSetup.periodsUnit, + testSetup.redeemEnableTime, + testSetup.maxLockingPeriod, + testSetup.repRewardConstA, + testSetup.repRewardConstB, + testSetup.periodsCap, + testSetup.lockingToken.address, + testSetup.agreementHash, + {gas : constants.ARC_GAS_LIMIT}); + assert(false, "cannot initialize twice"); + } catch(error) { + helpers.assertVMException(error); + } + }); + + it("cannot lock with wrong _lockingPeriodToLockIn", async () => { + var lockingPeriodToLockIn = 2; + let testSetup = await setup(accounts); + try { + await testSetup.continuousLocking4Reputation.lock(web3.utils.toWei('1', "ether"),1,lockingPeriodToLockIn,testSetup.agreementHash); + assert(false, "cannot lock with wrong _lockingPeriodToLockIn"); + } catch(error) { + helpers.assertVMException(error); + } + }); + + it("release", async () => { + let testSetup = await setup(accounts); + var tx = await testSetup.continuousLocking4Reputation.lock(web3.utils.toWei('1', "ether"),1,0,testSetup.agreementHash); + var lockingId = await helpers.getValueFromLogs(tx, '_lockingId',1); + await helpers.increaseTime(testSetup.periodsUnit+1); + tx = await testSetup.continuousLocking4Reputation.release(accounts[0],lockingId); + assert.equal(tx.logs.length,1); + assert.equal(tx.logs[0].event,"Release"); + assert.equal(tx.logs[0].args._lockingId,lockingId); + assert.equal(tx.logs[0].args._amount,web3.utils.toWei('1', "ether")); + assert.equal(tx.logs[0].args._beneficiary,accounts[0]); + }); + + it("release before locking period should revert", async () => { + let testSetup = await setup(accounts); + var tx = await testSetup.continuousLocking4Reputation.lock(web3.utils.toWei('1', "ether"),1,0,testSetup.agreementHash); + var lockingId = await helpers.getValueFromLogs(tx, '_lockingId',1); + try { + await testSetup.continuousLocking4Reputation.release(accounts[0],lockingId); + assert(false, "release before locking period should revert"); + } catch(error) { + helpers.assertVMException(error); + } + }); + + it("release cannot release twice", async () => { + let testSetup = await setup(accounts); + var tx = await testSetup.continuousLocking4Reputation.lock(web3.utils.toWei('1', "ether"),1,0,testSetup.agreementHash); + var lockingId = await helpers.getValueFromLogs(tx, '_lockingId',1); + await helpers.increaseTime(testSetup.periodsUnit+1); + await testSetup.continuousLocking4Reputation.release(accounts[0],lockingId); + try { + await testSetup.continuousLocking4Reputation.release(accounts[0],lockingId); + assert(false, "release cannot release twice"); + } catch(error) { + helpers.assertVMException(error); + } + }); + + it("redeem reward limits 100 periods", async () => { + let testSetup = await setup(accounts); + var repForPeriod = await testSetup.continuousLocking4Reputation.repRewardPerBatch(100); + var REAL_FBITS = 40; + var res = (repForPeriod.shrn(REAL_FBITS).toNumber() + (repForPeriod.maskn(REAL_FBITS)/Math.pow(2,REAL_FBITS))).toFixed(2); + assert.equal(Math.floor(res),Math.floor(testSetup.repRewardConstA* Math.pow(testSetup.repRewardConstB/1000,100))); + assert.equal(await testSetup.continuousLocking4Reputation.repRewardPerBatch(101),0); + }); + + it("redeem limits 100 periods", async () => { + let testSetup = await setup(accounts,false); + var period = 24; + await helpers.increaseTime(testSetup.periodsUnit*90+1); + await testSetup.continuousLocking4Reputation.initialize(testSetup.org.avatar.address, + testSetup.reputationReward, + testSetup.startTime, + testSetup.periodsUnit, + testSetup.redeemEnableTime, + period, + testSetup.repRewardConstA, + testSetup.repRewardConstB, + testSetup.periodsCap, + testSetup.lockingToken.address, + testSetup.agreementHash, + {gas : constants.ARC_GAS_LIMIT}); + await testSetup.continuousLocking4Reputation.lock(web3.utils.toWei('1', "ether"),1,90,testSetup.agreementHash); + try { + await testSetup.continuousLocking4Reputation.lock(web3.utils.toWei('1', "ether"),period,90,testSetup.agreementHash); + assert(false, "exceed max allowe periods"); + } catch(error) { + helpers.assertVMException(error); + } + }); + + it("extend locking withouth lock should fail", async () => { + let testSetup = await setup(accounts); + try { + await testSetup.continuousLocking4Reputation.extendLocking(1,0,helpers.SOME_HASH,testSetup.agreementHash); + assert(false, "extend locking withouth lock should fail"); + } catch(error) { + helpers.assertVMException(error); + } + }); + + it("extend locking ", async () => { + let testSetup = await setup(accounts); + var period = 12; + var tx = await testSetup.continuousLocking4Reputation.lock(web3.utils.toWei('1', "ether"),period,0,testSetup.agreementHash); + var id = await helpers.getValueFromLogs(tx, '_lockingId',1); + //increase time with one period + await helpers.increaseTime(testSetup.periodsUnit*1+1); + tx = await testSetup.continuousLocking4Reputation.extendLocking(1,1,id,testSetup.agreementHash); + assert.equal(tx.logs.length,1); + assert.equal(tx.logs[0].event,"ExtendLocking"); + assert.equal(tx.logs[0].args._lockingId,id); + assert.equal(tx.logs[0].args._extendPeriod,1); + await helpers.increaseTime(testSetup.periodsUnit*11+1); + try { + await testSetup.continuousLocking4Reputation.release(accounts[0],id); + assert(false, "release cannot release before time"); + } catch(error) { + helpers.assertVMException(error); + } + await helpers.increaseTime(testSetup.periodsUnit*1+1); + await testSetup.continuousLocking4Reputation.release(accounts[0],id); + + tx = await testSetup.continuousLocking4Reputation.redeem(accounts[0],id); + var redeemAmount = 0; + for (var lockingPeriodToRedeemFrom = 0; lockingPeriodToRedeemFrom < period+1; lockingPeriodToRedeemFrom++) { + redeemAmount += testSetup.repRewardConstA * (Math.pow((testSetup.repRewardConstB/1000),lockingPeriodToRedeemFrom)); + } + redeemAmount = Math.floor(redeemAmount); + assert.equal(tx.logs.length,1); + assert.equal(tx.logs[0].event,"Redeem"); + assert.equal(tx.logs[0].args._amount.toNumber(),redeemAmount); + assert.equal(tx.logs[0].args._beneficiary,accounts[0]); + assert.equal(await testSetup.org.reputation.balanceOf(accounts[0]),1000+redeemAmount); + + }); + + it("extend locking should not exceed the max period allowed", async () => { + let testSetup = await setup(accounts); + var period = 12; + var tx = await testSetup.continuousLocking4Reputation.lock(web3.utils.toWei('1', "ether"),period,0,testSetup.agreementHash); + var id = await helpers.getValueFromLogs(tx, '_lockingId',1); + try { + await testSetup.continuousLocking4Reputation.extendLocking(1,0,id,testSetup.agreementHash); + assert(false, "extend locking should not exceed the max period allowed"); + } catch(error) { + helpers.assertVMException(error); + } + }); + + it("extend locking limits", async () => { + let testSetup = await setup(accounts); + var period = 12; + var tx = await testSetup.continuousLocking4Reputation.lock(web3.utils.toWei('1', "ether"),period,0,testSetup.agreementHash); + var id = await helpers.getValueFromLogs(tx, '_lockingId',1); + for (var i = 12;i< 96 ;i +=12 ) { + await helpers.increaseTime(testSetup.periodsUnit*12+1); + await testSetup.continuousLocking4Reputation.extendLocking(period,i,id,testSetup.agreementHash); + } + await helpers.increaseTime(testSetup.periodsUnit*12+1); + await testSetup.continuousLocking4Reputation.extendLocking(4,96,id,testSetup.agreementHash); + + await helpers.increaseTime(testSetup.periodsUnit*3+1); + try { + await testSetup.continuousLocking4Reputation.release(accounts[0],id); + assert(false, "release cannot release before time"); + } catch(error) { + helpers.assertVMException(error); + } + await helpers.increaseTime(testSetup.periodsUnit*1+1); + await testSetup.continuousLocking4Reputation.release(accounts[0],id); + + tx = await testSetup.continuousLocking4Reputation.redeem(accounts[0],id); + var redeemAmount = 0; + for (var lockingPeriodToRedeemFrom = 0; lockingPeriodToRedeemFrom < 100; lockingPeriodToRedeemFrom++) { + redeemAmount += testSetup.repRewardConstA * (Math.pow((testSetup.repRewardConstB/1000),lockingPeriodToRedeemFrom)); + } + redeemAmount = Math.floor(redeemAmount); + assert.equal(tx.logs.length,1); + assert.equal(tx.logs[0].event,"Redeem"); + assert.equal(tx.logs[0].args._amount.toNumber(),redeemAmount); + assert.equal(tx.logs[0].args._beneficiary,accounts[0]); + assert.equal(await testSetup.org.reputation.balanceOf(accounts[0]),1000+redeemAmount); + + }); + + + +});