diff --git a/contracts/Curation.sol b/contracts/Curation.sol index 2cbce58..ff041ca 100644 --- a/contracts/Curation.sol +++ b/contracts/Curation.sol @@ -45,21 +45,28 @@ contract Curation is AragonApp { struct Challenge { address challenger; uint64 date; - bool resolved; uint256 amount; uint256 lockId; uint256 voteId; uint256 dispensationPct; + } + + struct Vote { + bool closed; + bool result; + uint256 votersRewardPool; + uint256 totalWinningStake; mapping(address => bool) claims; // participants who already claimed their reward } mapping(bytes32 => Application) applications; mapping(bytes32 => Challenge) challenges; + mapping(uint256 => Vote) votes; mapping(address => mapping(uint256 => bool)) usedLocks; - event NewApplication(bytes32 entryId, address applicant); - event NewChallenge(bytes32 entryId, address challenger); - event ResolvedChallenge(bytes32 entryId, bool result); + event NewApplication(bytes32 indexed entryId, address applicant); + event NewChallenge(bytes32 indexed entryId, address challenger); + event ResolvedChallenge(bytes32 indexed entryId, bool result); /** * @notice Initializes Curation app with @@ -118,158 +125,168 @@ contract Curation is AragonApp { function challengeApplication(bytes32 entryId, uint256 lockId) isInitialized public returns(bytes32) { // check application doesn't have an ongoing challenge require(!challengeExists(entryId)); - // check locked tokens - uint256 amount = _checkLock(msg.sender, lockId, getTimestamp().add(applyStageLen)); // touch-and-remove case Application memory application = applications[entryId]; if (application.amount < minDeposit) { - registry.remove(entryId); staking.unlock(application.applicant, application.lockId); - staking.unlock(msg.sender, lockId); + delete(applications[entryId]); + registry.remove(entryId); return 0; } + // check locked tokens + uint256 amount = _checkLock(msg.sender, lockId, getTimestamp().add(applyStageLen)); + // create vote // TODO: metadata // script to call `resolveChallenge(entryId)` uint256 scriptLength = 64; // 4 (spec) + 20 (address) + 4 (calldataLength) + 4 (signature) + 32 (input) bytes4 spec = bytes4(0x01); - address target = address(this); - bytes memory targetBytes = new bytes(32); bytes4 calldataLength = bytes4(0x24); // 4 + 32 bytes4 signature = this.resolveChallenge.selector; bytes memory executionScript = new bytes(scriptLength); // concatenate spec + address(this) + calldataLength + calldata - // TODO: should we put this somewhere in aragonOS to be able ti reuse? (if it's not already there) + // TODO: should we put this somewhere in aragonOS to be able to reuse it? (if it's not already there) assembly { mstore(add(executionScript, 0x20), spec) - mstore(add(targetBytes, 0x20), target) - mstore(add(executionScript, 0x24), mload(add(targetBytes, 0x2C))) + mstore(add(executionScript, 0x24), mul(address, exp(2,96))) mstore(add(executionScript, 0x38), calldataLength) mstore(add(executionScript, 0x3C), signature) mstore(add(executionScript, 0x40), entryId) } + uint256 voteId = voting.newVote(executionScript, ""); challenges[entryId] = Challenge({ challenger: msg.sender, date: getTimestamp(), - resolved: false, amount: amount, lockId: lockId, voteId: voteId, dispensationPct: dispensationPct }); + votes[voteId] = Vote({ + closed: false, + result: false, + totalWinningStake: 0, + votersRewardPool: 0 + }); + NewChallenge(entryId, msg.sender); return entryId; } function resolveChallenge(bytes32 entryId) isInitialized public { + require(challengeExists(entryId)); Challenge storage challenge = challenges[entryId]; Application storage application = applications[entryId]; + Vote storage vote = votes[challenge.voteId]; - require(!challenge.resolved); - // to avoid resolving and redistributing twice - challenge.resolved = true; - // TODO: canExecute?? require(voting.isClosed(challenge.voteId)); + vote.closed = true; + (vote.result, vote.totalWinningStake,) = voting.getVoteResult(challenge.voteId); - bool voteResult; - (voteResult,,) = voting.getVoteResult(challenge.voteId); + address winner; + address loser; + uint256 loserLockId; + uint256 amount; + if (vote.result == false) { // challenge not accepted (application remains) + winner = application.applicant; + loser = challenge.challenger; + loserLockId = challenge.lockId; + amount = challenge.amount; - uint256 reward; - if (voteResult == false) { // challenge not accepted (application remains) // it's still in application phase (not registered yet) if (!application.registered) { + application.registered = true; // insert in Registry app registry.add(application.data); - application.registered = true; } - // Remove applicant (winner) used lock - delete(usedLocks[application.applicant][application.lockId]); - // Unlock challenger tokens from Staking app - reward = challenge.amount.mul(dispensationPct) / PCT_BASE; - // Redistribute tokens - staking.unlockAndMoveTokens(challenge.challenger, challenge.lockId, application.applicant, reward); } else { // challenge accepted (application rejected) - // it has been already registered - if (application.registered) { - // remove from Registry app - registry.remove(entryId); - application.registered = false; - } - // Remove challenger (winner) used lock - delete(usedLocks[challenge.challenger][challenge.lockId]); - // Unlock applicant tokens from Staking app - reward = application.amount.mul(dispensationPct) / PCT_BASE; - // Redistribute tokens - staking.unlockAndMoveTokens(application.applicant, application.lockId, challenge.challenger, reward); + winner = challenge.challenger; + loser = application.applicant; + loserLockId = application.lockId; + amount = application.amount; + + // Remove from Registry app + application.registered = false; + registry.remove(entryId); + } + // compute rewards + uint256 reward = amount.mul(dispensationPct) / PCT_BASE; + vote.votersRewardPool = amount - reward; + + // unlock tokens from Staking app + staking.unlock(application.applicant, application.lockId); + staking.unlock(challenge.challenger, challenge.lockId); + + // redistribute tokens + staking.moveTokens(loser, winner, reward); + staking.moveTokens(loser, address(this), vote.votersRewardPool); + + // Remove used locks + delete(usedLocks[application.applicant][application.lockId]); + delete(usedLocks[challenge.challenger][challenge.lockId]); + + // Remove challenge, and application if needed + if (vote.result == true) { + delete(applications[entryId]); } + delete(challenges[entryId]); - ResolvedChallenge(entryId, voteResult); + ResolvedChallenge(entryId, vote.result); } - function claimReward(bytes32 entryId) isInitialized public { - require(isChallengeResolved(entryId)); + function claimReward(uint256 voteId) isInitialized public { + require(votes[voteId].closed); - Challenge storage challenge = challenges[entryId]; - Application memory application = applications[entryId]; + Vote storage vote = votes[voteId]; // avoid claiming twice - require(!challenge.claims[msg.sender]); + require(!vote.claims[msg.sender]); // register claim to avoid claiming it again - challenge.claims[msg.sender] = true; - - bool voteResult; - uint256 totalWinningStake; - (voteResult, totalWinningStake,) = voting.getVoteResult(challenge.voteId); - - address loser; - uint256 loserLockId; - uint256 amount; - if (voteResult == false) { - loser = challenge.challenger; - loserLockId = challenge.lockId; - amount = challenge.amount; - } else { // voteResult == true - loser = application.applicant; - loserLockId = application.lockId; - amount = application.amount; - } + vote.claims[msg.sender] = true; // reward as a voter - uint256 voterWinningStake = voting.getVoterWinningStake(challenge.voteId, msg.sender); + uint256 voterWinningStake = voting.getVoterWinningStake(voteId, msg.sender); require(voterWinningStake > 0); - // amount * (voter / total) * (1 - dispensationPct) - uint256 reward = amount.mul(voterWinningStake).mul(PCT_BASE.sub(dispensationPct)) / (totalWinningStake * PCT_BASE); + // rewardPool * (voter / total) + uint256 reward = vote.votersRewardPool.mul(voterWinningStake) / vote.totalWinningStake; // Redistribute tokens - staking.unlockAndMoveTokens(loser, loserLockId, msg.sender, reward); - - // check if lock can be released - uint256 remainingLockAmount; - (remainingLockAmount, ) = staking.getLock(loser, loserLockId); - if (remainingLockAmount == 0) { // TODO: with truncating, this may never happen!! - delete(usedLocks[loser][loserLockId]); - // Remove application, if it lost, as redistribution is done - if (voteResult == true) { - delete(applications[entryId]); - } - } + staking.moveTokens(address(this), msg.sender, reward); } - function registerApplication(bytes32 entryId) isInitialized public { + function registerUnchallengedApplication(bytes32 entryId) isInitialized public { require(canBeRegistered(entryId)); Application storage application = applications[entryId]; require(!application.registered); + application.registered = true; // insert in Registry app registry.add(application.data); - application.registered = true; + } + + function removeApplication(bytes32 entryId) isInitialized public { + // check application doesn't have an ongoing challenge + require(!challengeExists(entryId)); + + Application memory application = applications[entryId]; + // check sender is owner + require(application.applicant == msg.sender); + + // unlock applicant lock + staking.unlock(application.applicant, application.lockId); + // remove applicant used lock + delete(usedLocks[application.applicant][application.lockId]); + // delete application + delete(applications[entryId]); + // remove from registry + registry.remove(entryId); } function setVotingApp(IVoting _voting) authP(CHANGE_VOTING_APP_ROLE, arr(voting, _voting)) public { @@ -294,9 +311,8 @@ contract Curation is AragonApp { } function canBeRegistered(bytes32 entryId) view public returns (bool) { - // no challenges if (getTimestamp() > applications[entryId].date.add(applyStageLen) && - challenges[entryId].challenger == address(0)) + !challengeExists(entryId)) { return true; } @@ -304,10 +320,6 @@ contract Curation is AragonApp { return false; } - function isChallengeResolved(bytes32 entryId) view public returns (bool) { - return challenges[entryId].resolved; - } - function getApplication( bytes32 entryId ) @@ -341,7 +353,6 @@ contract Curation is AragonApp { returns ( address challenger, uint64 date, - bool resolved, uint256 amount, uint256 lockId, uint256 voteId, @@ -352,7 +363,6 @@ contract Curation is AragonApp { return ( challenge.challenger, challenge.date, - challenge.resolved, challenge.amount, challenge.lockId, challenge.voteId, @@ -360,15 +370,36 @@ contract Curation is AragonApp { ); } + function getVote( + uint256 voteId + ) + view + public + returns ( + bool closed, + bool result, + uint256 totalWinningStake, + uint256 votersRewardPool + ) + { + Vote memory vote = votes[voteId]; + return ( + vote.closed, + vote.result, + vote.totalWinningStake, + vote.votersRewardPool + ); + } + function _checkLock(address user, uint256 lockId, uint64 date) internal returns (uint256) { - // check lockId was not used before - require(!usedLocks[user][lockId]); // get the lock uint256 amount; uint8 lockUnit; uint64 lockEnds; address unlocker; (amount, lockUnit, lockEnds, unlocker, ) = staking.getLock(msg.sender, lockId); + // check lockId was not used before + require(!usedLocks[user][lockId]); // check unlocker require(unlocker == address(this)); // check enough amount diff --git a/package.json b/package.json index 645b7eb..728f5b8 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "homepage": "https://github.com/aragonlabs/curation#readme", "devDependencies": { "@aragon/apps-registry": "^1.0.0", + "@aragon/apps-staking": "^1.0.0", "@aragon/test-helpers": "^1.0.0", "eth-gas-reporter": "^0.1.5", "ganache-cli": "^6.1.0", diff --git a/test/curation.js b/test/curation.js index 8589847..7bf3fd6 100644 --- a/test/curation.js +++ b/test/curation.js @@ -1,5 +1,7 @@ const { assertRevert } = require('@aragon/test-helpers/assertThrow') +const { checkUnlocked, checkMovedTokens } = require('./helpers.js') + const getContract = name => artifacts.require(name) const getEvent = (receipt, event, arg) => { return receipt.logs.filter(l => l.event == event)[0].args[arg] } const pct16 = x => new web3.BigNumber(x).times(new web3.BigNumber(10).toPower(16)) @@ -17,6 +19,7 @@ contract('Curation', ([owner, applicant, challenger, voter, _]) => { const TIME_UNIT_BLOCKS = 0 const TIME_UNIT_SECONDS = 1 + context('Regular App', async () => { const appLockId = 1 const challengeLockId = 2 @@ -27,6 +30,7 @@ contract('Curation', ([owner, applicant, challenger, voter, _]) => { registry = await getContract('RegistryApp').new() staking = await getContract('StakingMock').new() voting = await getContract('VotingMock').new() + await voting.setVoteId(voteId) curation = await getContract('CurationMock').new() MAX_UINT64 = await curation.MAX_UINT64() @@ -181,11 +185,16 @@ contract('Curation', ([owner, applicant, challenger, voter, _]) => { const challenge = await curation.getChallenge.call(entryId) assert.equal(challenge[0], challenger, "Challenger should match") //assert.equal(challenge[1], , "Date should match") - assert.equal(challenge[2], false, "Resolved bool should match") - assert.equal(challenge[3], minDeposit, "Amount should match") - assert.equal(challenge[4], challengeLockId, "LockId should match") - assert.equal(challenge[5], voteId, "Vote Id should match") - assert.equal(challenge[6].toString(), dispensationPct.toString(), "Dipsensation Pct should match") + assert.equal(challenge[2], minDeposit, "Amount should match") + assert.equal(challenge[3], challengeLockId, "LockId should match") + assert.equal(challenge[4], voteId, "Vote Id should match") + assert.equal(challenge[5].toString(), dispensationPct.toString(), "Dispensation Pct should match") + // vote + const vote = await curation.getVote.call(entryId) + assert.equal(vote[0], false, "Closed bool should match") + assert.equal(vote[1], false, "Result bool should match") + assert.equal(vote[2], 0, "Total winning stake Pct should match") + assert.equal(vote[3], 0, "Voters reward pool Pct should match") }) it('challenges touch-and-remove application', async () => { @@ -194,19 +203,18 @@ contract('Curation', ([owner, applicant, challenger, voter, _]) => { // increase minDeposit await curation.setMinDeposit(minDeposit + 1) - // mock lock - await staking.setLock(minDeposit + 1, TIME_UNIT_SECONDS, (await curation.getTimestampExt.call()).add(applyStageLen + 1000), curation.address, "") + // mock lock - no need for lock + await staking.setLock(0, TIME_UNIT_SECONDS, 0, zeroAddress, "") // challenge - await curation.challengeApplication(entryId, challengeLockId, { from: challenger }) - const challenge = await curation.getChallenge.call(entryId) + const receipt = await curation.challengeApplication(entryId, challengeLockId, { from: challenger }) // no challenge has been created - assert.equal(challenge[0], zeroAddress, "Challenger should be zero") - assert.equal(challenge[1], 0, "Date should be zero") - assert.equal(challenge[2], false, "Resolved should be false") - assert.equal(challenge[3], 0, "Amount should be zero") - assert.equal(challenge[4], 0, "LockId should be zero") - assert.equal(challenge[5], 0, "Vote Id should be zero") - assert.equal(challenge[6], 0, "Dipsensation Pct should be zero") + const challenge = await curation.getChallenge.call(entryId) + assert.equal(challenge[0], zeroAddress, "Challenge should be empty") + // application has been removed + const application = await curation.getApplication.call(entryId) + assert.equal(application[0], zeroAddress, "Applicant should be empty") + // applicant lock has been unlocked + assert.isTrue(checkUnlocked(receipt, applicant, curation.address, appLockId), "Applicant lock should have been unlocked") }) it('fails challenging with an already used lock', async () => { @@ -282,31 +290,6 @@ contract('Curation', ([owner, applicant, challenger, voter, _]) => { // ----------- Resolve challenges -------------- - // checks if a log for moving tokens was generated with the given params - // if amount is 0, it will check for any - /* this doesn't work, only shows called contract logs - const checkMovedTokens = (receipt, from, to, amount) => { - const logs = receipt.logs.filter( - l => - l.event == 'MovedTokens' && - l.args['from'] == from && - l.args['to'] == to && - (l.args['amount'] == amount || amount == 0) - ) - return logs.length == 1 || (amount == 0 && logs.length >= 1) - } - */ - const checkMovedTokens = (receipt, from, to, amount) => { - const logs = receipt.receipt.logs.filter( - l => - l.topics[0] == web3.sha3('MovedTokens(address,address,uint256)') && - '0x' + l.topics[1].slice(26) == from && - '0x' + l.topics[2].slice(26) == to && - (web3.toDecimal(l.data) == amount || amount == 0) - ) - return logs.length == 1 || (amount == 0 && logs.length >= 1) - } - const applyChallengeAndResolve = async (result) => { const entryId = await applyAndChallenge() // mock vote result @@ -328,7 +311,7 @@ contract('Curation', ([owner, applicant, challenger, voter, _]) => { assert.isTrue(application[2], "Registered should be true") // challenge const challenge = await curation.getChallenge.call(entryId) - assert.isTrue(challenge[2], "Resolved should be true") + assert.equal(challenge[0], zeroAddress, "Challenge should be empty") // redistribution const amount = new web3.BigNumber(minDeposit).mul(dispensationPct).dividedToIntegerBy(1e18) assert.isFalse(checkMovedTokens(receipt, applicant, challenger, 0)) @@ -336,8 +319,8 @@ contract('Curation', ([owner, applicant, challenger, voter, _]) => { // used locks const appUsedLock = await curation.getUsedLock.call(applicant, appLockId) const challengeUsedLock = await curation.getUsedLock.call(challenger, challengeLockId) - assert.isFalse(appUsedLock) - assert.isTrue(challengeUsedLock) + assert.isFalse(appUsedLock, "There should be no lock for application") + assert.isFalse(challengeUsedLock, "There should be no lock for challenge") }) it('resolves application challenge, accepted', async () => { @@ -348,19 +331,23 @@ contract('Curation', ([owner, applicant, challenger, voter, _]) => { assert.isFalse(await registry.exists(data), "Data should not have been registered") // application const application = await curation.getApplication.call(entryId) - assert.isFalse(application[2], "Registered should be false") + assert.equal(application[0], zeroAddress, "Application should be empty") // challenge const challenge = await curation.getChallenge.call(entryId) - assert.isTrue(challenge[2], "Resolved should be true") + assert.equal(challenge[0], zeroAddress, "Challenge should be empty") // redistribution const amount = new web3.BigNumber(minDeposit).mul(dispensationPct).dividedToIntegerBy(1e18) - assert.isFalse(checkMovedTokens(receipt, challenger, applicant, 0)) - assert.isTrue(checkMovedTokens(receipt, applicant, challenger, amount)) + const votersRewardPool = new web3.BigNumber(minDeposit).minus(amount) + assert.isTrue(checkUnlocked(receipt, applicant, curation.address, appLockId), "Applicant lock should have been unlocked") + assert.isTrue(checkUnlocked(receipt, challenger, curation.address, challengeLockId), "Challenger lock should have been unlocked") + assert.isFalse(checkMovedTokens(receipt, challenger, applicant, 0), "No challenger tokens should have been moved") + assert.isTrue(checkMovedTokens(receipt, applicant, challenger, amount), "Applicant tokens should have been moved to challenger") + assert.isTrue(checkMovedTokens(receipt, applicant, curation.address, votersRewardPool), "Applicant tokens should have been moved to Curation app") // used locks const appUsedLock = await curation.getUsedLock.call(applicant, appLockId) const challengeUsedLock = await curation.getUsedLock.call(challenger, challengeLockId) - assert.isTrue(appUsedLock) - assert.isFalse(challengeUsedLock) + assert.isFalse(appUsedLock, "There should be no lock for application") + assert.isFalse(challengeUsedLock, "There should be no lock for challenge") }) const applyRegisterChallengeAndResolve = async (result) => { @@ -369,7 +356,7 @@ contract('Curation', ([owner, applicant, challenger, voter, _]) => { await curation.addTime(applyStageLen + 1) // register - await curation.registerApplication(entryId) + await curation.registerUnchallengedApplication(entryId) // mock lock await staking.setLock(minDeposit, TIME_UNIT_SECONDS, (await curation.getTimestampExt.call()).add(applyStageLen + 1000), curation.address, "") @@ -399,16 +386,20 @@ contract('Curation', ([owner, applicant, challenger, voter, _]) => { assert.isTrue(application[2], "Registered should be true") // challenge const challenge = await curation.getChallenge.call(entryId) - assert.isTrue(challenge[2], "Resolved should be true") + assert.equal(challenge[0], zeroAddress, "Challenge should be empty") // redistribution const amount = new web3.BigNumber(minDeposit).mul(dispensationPct).dividedToIntegerBy(1e18) - assert.isFalse(checkMovedTokens(receipt, applicant, challenger, 0)) - assert.isTrue(checkMovedTokens(receipt, challenger, applicant, amount)) + const votersRewardPool = new web3.BigNumber(minDeposit).minus(amount) + assert.isTrue(checkUnlocked(receipt, applicant, curation.address, appLockId), "Applicant lock should have been unlocked") + assert.isTrue(checkUnlocked(receipt, challenger, curation.address, challengeLockId), "Challenger lock should have been unlocked") + assert.isFalse(checkMovedTokens(receipt, applicant, challenger, 0), "No Applicant tokens should have been moved") + assert.isTrue(checkMovedTokens(receipt, challenger, applicant, amount), "Challenger tokens should have been moved to Applicant") + assert.isTrue(checkMovedTokens(receipt, challenger, curation.address, votersRewardPool), "Challenger tokens should have been moved to Curation app") // used locks const appUsedLock = await curation.getUsedLock.call(applicant, appLockId) const challengeUsedLock = await curation.getUsedLock.call(challenger, challengeLockId) - assert.isFalse(appUsedLock) - assert.isTrue(challengeUsedLock) + assert.isFalse(appUsedLock, "There should be no lock for application") + assert.isFalse(challengeUsedLock, "There should be no lock for challenge") }) it('resolves registry challenge, accepted', async () => { @@ -419,19 +410,19 @@ contract('Curation', ([owner, applicant, challenger, voter, _]) => { assert.isFalse(await registry.exists(data), "Data should have been removed from register") // application const application = await curation.getApplication.call(entryId) - assert.isFalse(application[2], "Registered should be false") + assert.equal(application[0], zeroAddress, "Application should be empty") // challenge const challenge = await curation.getChallenge.call(entryId) - assert.isTrue(challenge[2], "Resolved should be true") + assert.equal(challenge[0], zeroAddress, "Challenge should be empty") // redistribution const amount = new web3.BigNumber(minDeposit).mul(dispensationPct).dividedToIntegerBy(1e18) - assert.isFalse(checkMovedTokens(receipt, challenger, applicant, 0)) - assert.isTrue(checkMovedTokens(receipt, applicant, challenger, amount)) + assert.isFalse(checkMovedTokens(receipt, challenger, applicant, 0), "challenger tokens should have been moved") + assert.isTrue(checkMovedTokens(receipt, applicant, challenger, amount, "applicant tokens should have been moved")) // used locks const appUsedLock = await curation.getUsedLock.call(applicant, appLockId) const challengeUsedLock = await curation.getUsedLock.call(challenger, challengeLockId) - assert.isTrue(appUsedLock) - assert.isFalse(challengeUsedLock) + assert.isFalse(appUsedLock, "There should be no lock for application") + assert.isFalse(challengeUsedLock, "There should be no lock for challenge") }) it('fails resolving challenge if vote has not ended', async () => { @@ -463,17 +454,12 @@ contract('Curation', ([owner, applicant, challenger, voter, _]) => { // ----------- Claim rewards -------------- - const claimReward = async (result, lastClaim=false) => { + const claimReward = async (result) => { const {entryId} = await applyChallengeAndResolve(result) await voting.setVoterWinningStake(voter, VOTER_WINNING_STAKE) - if (lastClaim) { - // mock Staking to pretend losing party tokens were already distributed - await staking.setLock(0, TIME_UNIT_SECONDS, (await curation.getTimestampExt.call()).add(applyStageLen + 1000), curation.address, "") - } - // claim reward - const receipt = await curation.claimReward(entryId, { from: voter }) + const receipt = await curation.claimReward(voteId, { from: voter }) const reward = new web3.BigNumber(minDeposit).mul(VOTER_WINNING_STAKE).mul(1e18 - dispensationPct).dividedToIntegerBy(WINNING_STAKE).dividedToIntegerBy(1e18) return { entryId: entryId, @@ -484,55 +470,29 @@ contract('Curation', ([owner, applicant, challenger, voter, _]) => { it('claims reward as voter in the winning party', async () => { const {receipt, reward} = await claimReward(true) - assert.isTrue(checkMovedTokens(receipt, applicant, voter, reward), "Reward should be payed") + assert.isTrue(checkMovedTokens(receipt, curation.address, voter, reward), "Reward should be payed") }) it('fails claiming reward if not voter in the winning party', async () => { const {entryId} = await applyChallengeAndResolve(true) await voting.setVoterWinningStake(voter, 0) return assertRevert(async () => { - await curation.claimReward(entryId, { from: voter }) + await curation.claimReward(voteId, { from: voter }) }) }) - it('claims reward and lock can be released, challenge rejected', async () => { - const {receipt, reward} = await claimReward(false, true) - - // checks - assert.isTrue(checkMovedTokens(receipt, challenger, voter, reward), "Reward should be payed") - const appUsedLock = await curation.getUsedLock.call(applicant, appLockId) - const challengeUsedLock = await curation.getUsedLock.call(challenger, challengeLockId) - assert.isFalse(appUsedLock, "app lock should have been freed") - assert.isFalse(challengeUsedLock, "challenge lock should have been freed") - }) - - it('claims reward and lock can be released, challenge accepted', async () => { - const {entryId, receipt, reward} = await claimReward(true, true) - - // checks - assert.isTrue(checkMovedTokens(receipt, applicant, voter, reward), "Reward should be payed") - const application = await curation.getApplication.call(entryId) - assert.equal(application[0], zeroAddress, "Application should be empty") - const appUsedLock = await curation.getUsedLock.call(applicant, appLockId) - const challengeUsedLock = await curation.getUsedLock.call(challenger, challengeLockId) - assert.isFalse(appUsedLock) - assert.isFalse(challengeUsedLock) - }) - it('fails claiming reward if challenge is not resolved', async () => { const entryId = await applyAndChallenge() return assertRevert(async () => { - await curation.claimReward(entryId, { from: challenger }) + await curation.claimReward(voteId, { from: challenger }) }) }) it('fails claiming a reward twice', async () => { - const {entryId} = await applyChallengeAndResolve(true) - await voting.setVoterWinningStake(voter, VOTER_WINNING_STAKE) - await curation.claimReward(entryId, { from: voter }) + const {entryId} = await claimReward(true) return assertRevert(async () => { - await curation.claimReward(entryId, { from: voter }) + await curation.claimReward(voteId, { from: voter }) }) }) @@ -545,7 +505,7 @@ contract('Curation', ([owner, applicant, challenger, voter, _]) => { await curation.addTime(applyStageLen + 1) // register - await curation.registerApplication(entryId) + await curation.registerUnchallengedApplication(entryId) // checks // registry @@ -564,7 +524,7 @@ contract('Curation', ([owner, applicant, challenger, voter, _]) => { // register return assertRevert(async () => { - await curation.registerApplication(entryId) + await curation.registerUnchallengedApplication(entryId) }) }) @@ -583,7 +543,7 @@ contract('Curation', ([owner, applicant, challenger, voter, _]) => { // register return assertRevert(async () => { - await curation.registerApplication(entryId) + await curation.registerUnchallengedApplication(entryId) }) }) @@ -594,10 +554,38 @@ contract('Curation', ([owner, applicant, challenger, voter, _]) => { await curation.addTime(applyStageLen + 1) // register - await curation.registerApplication(entryId) + await curation.registerUnchallengedApplication(entryId) + + return assertRevert(async () => { + await curation.registerUnchallengedApplication(entryId) + }) + }) + + // ----------- Remove application by applicant -------------- + it('removes application', async () => { + const entryId = await createApplication() + + const receipt = await curation.removeApplication(entryId, { from: applicant }) + // tokens have been unlocked + assert.isTrue(checkUnlocked(receipt, applicant, curation.address, appLockId), "Applicant lock in Staking app should have been unlocked") + // used lock has been removed + const appUsedLock = await curation.getUsedLock.call(applicant, appLockId) + assert.isFalse(appUsedLock, "Applicant used lock should have been freed") + }) + + it('fails removing application with an ongoing challenge', async () => { + const entryId = await applyAndChallenge() + + return assertRevert(async () => { + await curation.removeApplication(entryId) + }) + }) + + it('fails removing application by non-applicant', async () => { + const entryId = await createApplication() return assertRevert(async () => { - await curation.registerApplication(entryId) + await curation.removeApplication(entryId, { from: challenger }) }) }) @@ -642,10 +630,12 @@ contract('Curation', ([owner, applicant, challenger, voter, _]) => { }) context('Without init', async () => { + const voteId = 1 beforeEach(async () => { registry = await getContract('RegistryApp').new() staking = await getContract('StakingMock').new() voting = await getContract('VotingMock').new() + await voting.setVoteId(voteId) curation = await getContract('Curation').new() }) @@ -677,14 +667,21 @@ contract('Curation', ([owner, applicant, challenger, voter, _]) => { it('fails registering an application', async () => { return assertRevert(async () => { const entryId = 1 - await curation.registerApplication(entryId) + await curation.registerUnchallengedApplication(entryId) }) }) it('fails claiming reward', async () => { return assertRevert(async () => { const entryId = 1 - await curation.claimReward(entryId) + await curation.claimReward(voteId) + }) + }) + + it('fails trying to remove entry', async () => { + return assertRevert(async () => { + const entryId = 1 + await curation.removeApplication(entryId) }) }) diff --git a/test/helpers.js b/test/helpers.js new file mode 100644 index 0000000..77e0199 --- /dev/null +++ b/test/helpers.js @@ -0,0 +1,26 @@ +module.exports = { + // checks if a log for unlocking tokens was generated with the given params + checkUnlocked: (receipt, account, unlocker, lockId) => { + const logs = receipt.receipt.logs.filter( + l => + l.topics[0] == web3.sha3('Unlocked(address,address,uint256)') && + '0x' + l.topics[1].slice(26) == account && + '0x' + l.topics[2].slice(26) == unlocker && + web3.toDecimal(l.data) == lockId + ) + return logs.length == 1 + }, + + // checks if a log for moving tokens was generated with the given params + // if amount is 0, it will check for any + checkMovedTokens: (receipt, from, to, amount) => { + const logs = receipt.receipt.logs.filter( + l => + l.topics[0] == web3.sha3('MovedTokens(address,address,uint256)') && + '0x' + l.topics[1].slice(26) == from && + '0x' + l.topics[2].slice(26) == to && + (web3.toDecimal(l.data) == amount || amount == 0) + ) + return logs.length == 1 || (amount == 0 && logs.length >= 1) + } +} diff --git a/test/mocks/StakingMock.sol b/test/mocks/StakingMock.sol index 6835b83..c4ca63a 100644 --- a/test/mocks/StakingMock.sol +++ b/test/mocks/StakingMock.sol @@ -10,6 +10,8 @@ contract StakingMock is IStaking { address unlocker; bytes32 metadata; + event Unlocked(address indexed account, address indexed unlocker, uint256 oldLockId); + event UnlockedPartial(address indexed account, address indexed unlocker, uint256 indexed lockId, uint256 amount); event MovedTokens(address indexed from, address indexed to, uint256 amount); function setLock(uint256 _amount, uint8 _lockUnit, uint64 _lockEnds, address _unlocker, bytes32 _metadata) public { @@ -20,14 +22,15 @@ contract StakingMock is IStaking { } function unlock(address acct, uint256 lockId) public { - // do nothing + Unlocked(acct, msg.sender, lockId); } function moveTokens(address _from, address _to, uint256 _amount) public { MovedTokens(_from, _to, _amount); } - function unlockAndMoveTokens(address _from, uint256 _lockId, address _to, uint256 _amount) external { + function unlockPartialAndMoveTokens(address _from, uint256 _lockId, address _to, uint256 _amount) external { + UnlockedPartial(_from, msg.sender, _lockId, amount); MovedTokens(_from, _to, _amount); } diff --git a/test/script.js b/test/script.js index 74bf4c8..da40527 100644 --- a/test/script.js +++ b/test/script.js @@ -1,5 +1,7 @@ const { assertRevert } = require('@aragon/test-helpers/assertThrow') +const { checkMovedTokens } = require('./helpers.js') + const getContract = name => artifacts.require(name) const getEvent = (receipt, event, arg) => { return receipt.logs.filter(l => l.event == event)[0].args[arg] } const pct16 = x => new web3.BigNumber(x).times(new web3.BigNumber(10).toPower(16)) @@ -67,17 +69,6 @@ contract('Curation', ([owner, applicant, challenger, voter, _]) => { return { entryId: entryId, receipt: receipt } } - const checkMovedTokens = (receipt, from, to, amount) => { - const logs = receipt.receipt.logs.filter( - l => - l.topics[0] == web3.sha3('MovedTokens(address,address,uint256)') && - '0x' + l.topics[1].slice(26) == from && - '0x' + l.topics[2].slice(26) == to && - (web3.toDecimal(l.data) == amount || amount == 0) - ) - return logs.length == 1 || (amount == 0 && logs.length >= 1) - } - context('Using voting script', async () => { let daoFact @@ -121,7 +112,7 @@ contract('Curation', ([owner, applicant, challenger, voter, _]) => { assert.isTrue(application[2], "Registered should be true") // challenge const challenge = await curation.getChallenge.call(entryId) - assert.isTrue(challenge[2], "Resolved should be true") + assert.equal(challenge[0], zeroAddress, "Challenge should be empty") // redistribution const amount = new web3.BigNumber(minDeposit).mul(dispensationPct).dividedToIntegerBy(1e18) assert.isFalse(checkMovedTokens(receipt, applicant, challenger, 0)) @@ -129,8 +120,8 @@ contract('Curation', ([owner, applicant, challenger, voter, _]) => { // used locks const appUsedLock = await curation.getUsedLock.call(applicant, appLockId) const challengeUsedLock = await curation.getUsedLock.call(challenger, challengeLockId) - assert.isFalse(appUsedLock) - assert.isTrue(challengeUsedLock) + assert.isFalse(appUsedLock, "There should be no lock for application") + assert.isFalse(challengeUsedLock, "There should be no lock for challenge") }) it('challenges application and executes, challenge accepted', async () => { @@ -140,10 +131,10 @@ contract('Curation', ([owner, applicant, challenger, voter, _]) => { assert.isFalse(await registry.exists(data), "Data should not have been registered") // application const application = await curation.getApplication.call(entryId) - assert.isFalse(application[2], "Registered should be false") + assert.equal(application[0], zeroAddress, "Application should be empty") // challenge const challenge = await curation.getChallenge.call(entryId) - assert.isTrue(challenge[2], "Resolved should be true") + assert.equal(challenge[0], zeroAddress, "Challenge should be empty") // redistribution const amount = new web3.BigNumber(minDeposit).mul(dispensationPct).dividedToIntegerBy(1e18) assert.isFalse(checkMovedTokens(receipt, challenger, applicant, 0)) @@ -151,8 +142,8 @@ contract('Curation', ([owner, applicant, challenger, voter, _]) => { // used locks const appUsedLock = await curation.getUsedLock.call(applicant, appLockId) const challengeUsedLock = await curation.getUsedLock.call(challenger,challengeLockId) - assert.isTrue(appUsedLock) - assert.isFalse(challengeUsedLock) + assert.isFalse(appUsedLock, "There should be no lock for application") + assert.isFalse(challengeUsedLock, "There should be no lock for challenge") }) }) })