From c780cafdd12295edd1252b70005417e334a17f4a Mon Sep 17 00:00:00 2001 From: Ariel Barmat Date: Thu, 28 May 2020 11:44:42 -0300 Subject: [PATCH] staking: check caller is the channel proxy when settling (#203) * staking: validate settle caller is the channel multisig proxy * disputes: fix and improve tests --- contracts/DisputeManager.sol | 2 +- contracts/Staking.sol | 25 +- test/disputes.test.js | 851 +++++++++++++++++++---------------- test/staking/general.test.js | 116 +++-- 4 files changed, 530 insertions(+), 464 deletions(-) diff --git a/contracts/DisputeManager.sol b/contracts/DisputeManager.sol index 34ac9d793..cb49739ab 100644 --- a/contracts/DisputeManager.sol +++ b/contracts/DisputeManager.sol @@ -408,7 +408,7 @@ contract DisputeManager is Governed { // Get the indexer that created the channel and signed the attestation (address indexer, bytes32 subgraphID) = staking.channels(channelID); - require(indexer != address(0), "Indexer cannot be found with the attestation"); + require(indexer != address(0), "Indexer cannot be found for the attestation"); require(subgraphID == attestation.subgraphID, "Channel and attestation subgraph must match"); // Create a disputeID diff --git a/contracts/Staking.sol b/contracts/Staking.sol index e8676a0c4..dab03f442 100644 --- a/contracts/Staking.sol +++ b/contracts/Staking.sol @@ -49,12 +49,14 @@ contract Staking is Governed { // Indexer stake tracking : indexer => Stake mapping(address => Stakes.Indexer) public stakes; - // Channels : channelID => Channel - mapping(address => Channel) public channels; - // Rebate pools : epoch => Pool mapping(uint256 => Rebates.Pool) public rebates; + // Channels : channelID => Channel + mapping(address => Channel) public channels; + // Channels Proxy : Channel Multisig Proxy => channelID + mapping(address => address) public channelsProxy; + // List of addresses allowed to slash stakes mapping(address => bool) public slashers; @@ -376,12 +378,14 @@ contract Staking is Governed { * @param _subgraphID ID of the subgraph where tokens will be allocated * @param _tokens Amount of tokens to allocate * @param _channelPubKey The public key used by the indexer to setup the off-chain channel + * @param _channelProxy Address of the multisig proxy used to hold channel funds * @param _price Price the `indexer` will charge for serving queries of the `subgraphID` */ function allocate( bytes32 _subgraphID, uint256 _tokens, bytes calldata _channelPubKey, + address _channelProxy, uint256 _price ) external { address indexer = msg.sender; @@ -410,6 +414,7 @@ contract Staking is Governed { alloc.channelID = channelID; alloc.createdAtEpoch = epochManager.currentEpoch(); channels[channelID] = Channel(indexer, _subgraphID); + channelsProxy[_channelProxy] = channelID; emit AllocationCreated( indexer, @@ -424,22 +429,24 @@ contract Staking is Governed { /** * @dev Settle a channel after receiving collected query fees from it - * @param _channelID Address of the indexer in the channel + * Funds are received from a channel multisig proxy contract * @param _tokens Amount of tokens to settle */ - function settle(address _channelID, uint256 _tokens) external { - address channelMultisig = msg.sender; + function settle(uint256 _tokens) external { + // Get the channelID the caller is related + address channelID = channelsProxy[msg.sender]; + delete channelsProxy[msg.sender]; // Remove to avoid re-entrancy // Receive funds from the channel multisig // We use channelID to find the indexer owner of the channel - require(isChannel(_channelID), "Channel: does not exist"); + require(isChannel(channelID), "Channel: does not exist"); // Transfer tokens to settle from multisig to this contract require( - token.transferFrom(channelMultisig, address(this), _tokens), + token.transferFrom(msg.sender, address(this), _tokens), "Channel: Cannot transfer tokens to settle" ); - _settle(_channelID, channelMultisig, _tokens); + _settle(channelID, msg.sender, _tokens); } /** diff --git a/test/disputes.test.js b/test/disputes.test.js index 8f4016fdb..61a0ba154 100644 --- a/test/disputes.test.js +++ b/test/disputes.test.js @@ -1,6 +1,6 @@ const BN = web3.utils.BN const { expect } = require('chai') -const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers') +const { constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers') const { ZERO_ADDRESS } = constants // helpers @@ -12,278 +12,343 @@ const { defaults } = require('./lib/testHelpers') const MAX_PPM = 1000000 const NON_EXISTING_DISPUTE_ID = '0x0' -contract('Disputes', ([me, other, governor, arbitrator, indexer, fisherman, otherIndexer]) => { - beforeEach(async function() { - // Channel keys for account #4 - this.indexerChannelPrivKey = - '0xe9696cbe81b09b796be29055c8694eb422710940b44934b3a1d21c1ca0a03e9a' - this.indexerChannelPubKey = - '0x04417b6be970480e74a55182ee04279fdffa7431002af2150750d367999a59abead903fbd23c0da7bb4233fdbccd732a2f561e66460718b4c50084e736c1601555' - // Channel keys for account #6 - this.otherIndexerChannelPrivKey = - '0xb560ebb22d7369c8ffeb9aec92930adfab16054542eadc76de826bc7db6390c2' - this.otherIndexerChannelPubKey = - '0x0447b5891c07679d40d6dfd3c4f8e1974e068da36ac76a6507dbaf5e432b879b3d4cd8c950b0df035e621f5a55b91a224ecdaef8cc8e6bb8cd8afff4a74c1904cd' - - // Deploy epoch contract - this.epochManager = await deployment.deployEpochManagerContract(governor, { from: me }) - - // Deploy graph token - this.grt = await deployment.deployGRT(governor, { - from: me, +function toGRT(value) { + return new BN(web3.utils.toWei(value)) +} + +contract( + 'Disputes', + ([me, other, governor, arbitrator, indexer, fisherman, otherIndexer, channelProxy]) => { + before(async function() { + // Helpers + this.advanceToNextEpoch = async () => { + const currentBlock = await time.latestBlock() + const epochLength = await this.epochManager.epochLength() + const nextEpochBlock = currentBlock.add(epochLength) + await time.advanceBlockTo(nextEpochBlock) + } }) - // Deploy staking contract - this.staking = await deployment.deployStakingContract( - governor, - this.grt.address, - this.epochManager.address, - ZERO_ADDRESS, - { from: me }, - ) - - // Deploy dispute contract - this.disputeManager = await deployment.deployDisputeManagerContract( - governor, - this.grt.address, - arbitrator, - this.staking.address, - { from: me }, - ) - }) - - describe('state variables functions', () => { - it('should set `governor`', async function() { - // Set right in the constructor - expect(await this.disputeManager.governor()).to.eq(governor) - }) + beforeEach(async function() { + // Channel keys for account #4 + this.indexerChannelPrivKey = + '0xe9696cbe81b09b796be29055c8694eb422710940b44934b3a1d21c1ca0a03e9a' + this.indexerChannelPubKey = + '0x04417b6be970480e74a55182ee04279fdffa7431002af2150750d367999a59abead903fbd23c0da7bb4233fdbccd732a2f561e66460718b4c50084e736c1601555' + // Channel keys for account #6 + this.otherIndexerChannelPrivKey = + '0xb560ebb22d7369c8ffeb9aec92930adfab16054542eadc76de826bc7db6390c2' + this.otherIndexerChannelPubKey = + '0x0447b5891c07679d40d6dfd3c4f8e1974e068da36ac76a6507dbaf5e432b879b3d4cd8c950b0df035e621f5a55b91a224ecdaef8cc8e6bb8cd8afff4a74c1904cd' + + // Deploy epoch contract + this.epochManager = await deployment.deployEpochManagerContract(governor, { from: me }) + + // Deploy graph token + this.grt = await deployment.deployGRT(governor, { + from: me, + }) - it('should set `graphToken`', async function() { - // Set right in the constructor - expect(await this.disputeManager.token()).to.eq(this.grt.address) + // Deploy staking contract + this.staking = await deployment.deployStakingContract( + governor, + this.grt.address, + this.epochManager.address, + ZERO_ADDRESS, + { from: me }, + ) + + // Deploy dispute contract + this.disputeManager = await deployment.deployDisputeManagerContract( + governor, + this.grt.address, + arbitrator, + this.staking.address, + { from: me }, + ) }) - describe('arbitrator', function() { - it('should set `arbitrator`', async function() { + describe('state variables functions', () => { + it('should set `governor`', async function() { // Set right in the constructor - expect(await this.disputeManager.arbitrator()).to.eq(arbitrator) - - // Can set if allowed - await this.disputeManager.setArbitrator(other, { from: governor }) - expect(await this.disputeManager.arbitrator()).to.eq(other) + expect(await this.disputeManager.governor()).to.eq(governor) }) - it('reject set `arbitrator` if not allowed', async function() { - await expectRevert( - this.disputeManager.setArbitrator(arbitrator, { from: other }), - 'Only Governor can call', - ) + it('should set `graphToken`', async function() { + // Set right in the constructor + expect(await this.disputeManager.token()).to.eq(this.grt.address) }) - }) - describe('minimumDeposit', function() { - it('should set `minimumDeposit`', async function() { - const minimumDeposit = defaults.dispute.minimumDeposit - const newMinimumDeposit = web3.utils.toBN(1) + describe('arbitrator', function() { + it('should set `arbitrator`', async function() { + // Set right in the constructor + expect(await this.disputeManager.arbitrator()).to.eq(arbitrator) - // Set right in the constructor - expect(await this.disputeManager.minimumDeposit()).to.be.bignumber.eq(minimumDeposit) + // Can set if allowed + await this.disputeManager.setArbitrator(other, { from: governor }) + expect(await this.disputeManager.arbitrator()).to.eq(other) + }) - // Set new value - await this.disputeManager.setMinimumDeposit(newMinimumDeposit, { - from: governor, + it('reject set `arbitrator` if not allowed', async function() { + await expectRevert( + this.disputeManager.setArbitrator(arbitrator, { from: other }), + 'Only Governor can call', + ) }) - expect(await this.disputeManager.minimumDeposit()).to.be.bignumber.eq(newMinimumDeposit) }) - it('reject set `minimumDeposit` if not allowed', async function() { - await expectRevert( - this.disputeManager.setMinimumDeposit(defaults.dispute.minimumDeposit, { - from: other, - }), - 'Only Governor can call', - ) - }) - }) + describe('minimumDeposit', function() { + it('should set `minimumDeposit`', async function() { + const minimumDeposit = defaults.dispute.minimumDeposit + const newMinimumDeposit = web3.utils.toBN(1) - describe('fishermanRewardPercentage', function() { - it('should set `fishermanRewardPercentage`', async function() { - const fishermanRewardPercentage = defaults.dispute.fishermanRewardPercentage + // Set right in the constructor + expect(await this.disputeManager.minimumDeposit()).to.be.bignumber.eq(minimumDeposit) - // Set right in the constructor - expect(await this.disputeManager.fishermanRewardPercentage()).to.be.bignumber.eq( - fishermanRewardPercentage.toString(), - ) + // Set new value + await this.disputeManager.setMinimumDeposit(newMinimumDeposit, { + from: governor, + }) + expect(await this.disputeManager.minimumDeposit()).to.be.bignumber.eq(newMinimumDeposit) + }) - // Set new value - await this.disputeManager.setFishermanRewardPercentage(0, { from: governor }) - await this.disputeManager.setFishermanRewardPercentage(1, { from: governor }) - await this.disputeManager.setFishermanRewardPercentage(fishermanRewardPercentage, { - from: governor, + it('reject set `minimumDeposit` if not allowed', async function() { + await expectRevert( + this.disputeManager.setMinimumDeposit(defaults.dispute.minimumDeposit, { + from: other, + }), + 'Only Governor can call', + ) }) }) - it('reject set `fishermanRewardPercentage` if out of bounds', async function() { - await expectRevert( - this.disputeManager.setFishermanRewardPercentage(MAX_PPM + 1, { - from: governor, - }), - 'Reward percentage must be below or equal to MAX_PPM', - ) - }) + describe('fishermanRewardPercentage', function() { + it('should set `fishermanRewardPercentage`', async function() { + const fishermanRewardPercentage = defaults.dispute.fishermanRewardPercentage - it('reject set `fishermanRewardPercentage` if not allowed', async function() { - await expectRevert( - this.disputeManager.setFishermanRewardPercentage(50, { from: other }), - 'Only Governor can call', - ) - }) - }) + // Set right in the constructor + expect(await this.disputeManager.fishermanRewardPercentage()).to.be.bignumber.eq( + fishermanRewardPercentage.toString(), + ) - describe('slashingPercentage', function() { - it('should set `slashingPercentage`', async function() { - const slashingPercentage = defaults.dispute.slashingPercentage + // Set new value + await this.disputeManager.setFishermanRewardPercentage(0, { from: governor }) + await this.disputeManager.setFishermanRewardPercentage(1, { from: governor }) + await this.disputeManager.setFishermanRewardPercentage(fishermanRewardPercentage, { + from: governor, + }) + }) - // Set right in the constructor - expect(await this.disputeManager.slashingPercentage()).to.be.bignumber.eq( - slashingPercentage.toString(), - ) + it('reject set `fishermanRewardPercentage` if out of bounds', async function() { + await expectRevert( + this.disputeManager.setFishermanRewardPercentage(MAX_PPM + 1, { + from: governor, + }), + 'Reward percentage must be below or equal to MAX_PPM', + ) + }) - // Set new value - await this.disputeManager.setSlashingPercentage(0, { from: governor }) - await this.disputeManager.setSlashingPercentage(1, { from: governor }) - await this.disputeManager.setSlashingPercentage(slashingPercentage, { - from: governor, + it('reject set `fishermanRewardPercentage` if not allowed', async function() { + await expectRevert( + this.disputeManager.setFishermanRewardPercentage(50, { from: other }), + 'Only Governor can call', + ) }) }) - it('reject set `slashingPercentage` if out of bounds', async function() { - await expectRevert( - this.disputeManager.setSlashingPercentage(MAX_PPM + 1, { + describe('slashingPercentage', function() { + it('should set `slashingPercentage`', async function() { + const slashingPercentage = defaults.dispute.slashingPercentage + + // Set right in the constructor + expect(await this.disputeManager.slashingPercentage()).to.be.bignumber.eq( + slashingPercentage.toString(), + ) + + // Set new value + await this.disputeManager.setSlashingPercentage(0, { from: governor }) + await this.disputeManager.setSlashingPercentage(1, { from: governor }) + await this.disputeManager.setSlashingPercentage(slashingPercentage, { from: governor, - }), - 'Slashing percentage must be below or equal to MAX_PPM', - ) + }) + }) + + it('reject set `slashingPercentage` if out of bounds', async function() { + await expectRevert( + this.disputeManager.setSlashingPercentage(MAX_PPM + 1, { + from: governor, + }), + 'Slashing percentage must be below or equal to MAX_PPM', + ) + }) + + it('reject set `slashingPercentage` if not allowed', async function() { + await expectRevert( + this.disputeManager.setSlashingPercentage(50, { from: other }), + 'Only Governor can call', + ) + }) }) - it('reject set `slashingPercentage` if not allowed', async function() { - await expectRevert( - this.disputeManager.setSlashingPercentage(50, { from: other }), - 'Only Governor can call', - ) + describe('staking', function() { + it('should set `staking`', async function() { + // Set right in the constructor + expect(await this.disputeManager.staking()).to.eq(this.staking.address) + + // Can set if allowed + await this.disputeManager.setStaking(this.grt.address, { from: governor }) + expect(await this.disputeManager.staking()).to.eq(this.grt.address) + }) + + it('reject set `staking` if not allowed', async function() { + await expectRevert( + this.disputeManager.setStaking(this.grt.address, { + from: other, + }), + 'Only Governor can call', + ) + }) }) }) - describe('staking', function() { - it('should set `staking`', async function() { - // Set right in the constructor - expect(await this.disputeManager.staking()).to.eq(this.staking.address) + describe('dispute lifecycle', function() { + beforeEach(async function() { + // Give some funds to the fisherman + this.fishermanTokens = toGRT('100000') + this.fishermanDeposit = toGRT('1000') + await this.grt.mint(fisherman, this.fishermanTokens, { + from: governor, + }) + await this.grt.approve(this.disputeManager.address, this.fishermanTokens, { + from: fisherman, + }) - // Can set if allowed - await this.disputeManager.setStaking(this.grt.address, { from: governor }) - expect(await this.disputeManager.staking()).to.eq(this.grt.address) + // Create a dispute + const receipt = { + requestCID: web3.utils.randomHex(32), + responseCID: web3.utils.randomHex(32), + subgraphID: helpers.randomSubgraphId(), + } + this.dispute = await attestation.createDispute( + receipt, + this.disputeManager.address, + this.indexerChannelPrivKey, + indexer, + ) }) - it('reject set `staking` if not allowed', async function() { + it('reject create a dispute if attestation does not refer to valid indexer', async function() { + // Create dispute await expectRevert( - this.disputeManager.setStaking(this.grt.address, { - from: other, + this.disputeManager.createDispute(this.dispute.attestation, this.fishermanDeposit, { + from: fisherman, }), - 'Only Governor can call', + 'Indexer cannot be found for the attestation', ) }) - }) - }) - describe('dispute lifecycle', function() { - beforeEach(async function() { - // Give some funds to the fisherman - this.fishermanTokens = web3.utils.toWei(new BN('100000')) - this.fishermanDeposit = web3.utils.toWei(new BN('1000')) - await this.grt.mint(fisherman, this.fishermanTokens, { - from: governor, - }) - await this.grt.approve(this.disputeManager.address, this.fishermanTokens, { - from: fisherman, - }) + it('reject create a dispute if indexer has no stake', async function() { + // This tests reproduce the case when someones present a dispute after + // an indexer removed his stake completely and find nothing to slash - // Create a dispute - const receipt = { - requestCID: web3.utils.randomHex(32), - responseCID: web3.utils.randomHex(32), - subgraphID: helpers.randomSubgraphId(), - } - this.dispute = await attestation.createDispute( - receipt, - this.disputeManager.address, - this.indexerChannelPrivKey, - indexer, - ) - }) + const indexerTokens = toGRT('100000') + const indexerAllocatedTokens = toGRT('10000') + const indexerSettledTokens = toGRT('10') + + // Give some funds to the indexer + await this.grt.mint(indexer, indexerTokens, { + from: governor, + }) + await this.grt.approve(this.staking.address, indexerTokens, { + from: indexer, + }) + + // Give some funds to the channel + await this.grt.mint(channelProxy, indexerSettledTokens, { + from: governor, + }) + await this.grt.approve(this.staking.address, indexerSettledTokens, { + from: channelProxy, + }) + + // Set the thawing period to zero to make the test easier + await this.staking.setThawingPeriod(new BN('0'), { from: governor }) + + // Indexer stake funds, allocate, settle, unstake and withdraw the stake fully + await this.staking.stake(indexerTokens, { from: indexer }) + await this.staking.allocate( + this.dispute.receipt.subgraphID, + indexerAllocatedTokens, + this.indexerChannelPubKey, + channelProxy, + new BN('0'), + { from: indexer }, + ) + await this.advanceToNextEpoch() // wait the required one epoch to settle + await this.staking.settle(indexerSettledTokens, { from: channelProxy }) + await this.staking.unstake(indexerTokens, { from: indexer }) + await this.staking.withdraw({ from: indexer }) // no thawing period so we are good - context('> when stake does not exist', function() { - it('reject create a dispute', async function() { // Create dispute await expectRevert( this.disputeManager.createDispute(this.dispute.attestation, this.fishermanDeposit, { from: fisherman, }), - 'Indexer cannot be found with the attestation', + 'Dispute has no stake by the indexer', ) }) - }) - context('> when stake does exist', function() { - beforeEach(async function() { - // Dispute manager is allowed to slash - await this.staking.setSlasher(this.disputeManager.address, true, { - from: governor, - }) - - // Stake - this.indexerTokens = web3.utils.toWei(new BN('100000')) - this.indexerAllocatedTokens = web3.utils.toWei(new BN('10000')) - const indexerList = [ - [indexer, this.indexerChannelPubKey], - [otherIndexer, this.otherIndexerChannelPubKey], - ] - for (const activeIndexer of indexerList) { - const [indexerAddress, indexerPubKey] = activeIndexer - - // Give some funds to the indexer - await this.grt.mint(indexerAddress, this.indexerTokens, { + context('> when indexer has staked', function() { + beforeEach(async function() { + // Dispute manager is allowed to slash + await this.staking.setSlasher(this.disputeManager.address, true, { from: governor, }) - await this.grt.approve(this.staking.address, this.indexerTokens, { - from: indexerAddress, - }) - // Indexer stake funds - await this.staking.stake(this.indexerTokens, { from: indexerAddress }) - await this.staking.allocate( - this.dispute.receipt.subgraphID, - this.indexerAllocatedTokens, - indexerPubKey, - new BN(0), - { from: indexerAddress }, - ) - } - }) + // Stake + this.indexerTokens = toGRT('100000') + this.indexerAllocatedTokens = toGRT('10000') + const indexerList = [ + [indexer, this.indexerChannelPubKey], + [otherIndexer, this.otherIndexerChannelPubKey], + ] + for (const activeIndexer of indexerList) { + const [indexerAddress, indexerPubKey] = activeIndexer + + // Give some funds to the indexer + await this.grt.mint(indexerAddress, this.indexerTokens, { + from: governor, + }) + await this.grt.approve(this.staking.address, this.indexerTokens, { + from: indexerAddress, + }) - describe('reward calculation', function() { - it('should calculate the reward for a stake', async function() { - const stakedAmount = this.indexerTokens - const trueReward = stakedAmount - .mul(defaults.dispute.slashingPercentage) - .div(new BN(MAX_PPM)) - .mul(defaults.dispute.fishermanRewardPercentage) - .div(new BN(MAX_PPM)) - const funcReward = await this.disputeManager.getTokensToReward(indexer) - expect(funcReward).to.be.bignumber.eq(trueReward.toString()) + // Indexer stake funds + await this.staking.stake(this.indexerTokens, { from: indexerAddress }) + await this.staking.allocate( + this.dispute.receipt.subgraphID, + this.indexerAllocatedTokens, + indexerPubKey, + channelProxy, + new BN('0'), + { from: indexerAddress }, + ) + } + }) + + describe('reward calculation', function() { + it('should calculate the reward for a stake', async function() { + const stakedAmount = this.indexerTokens + const trueReward = stakedAmount + .mul(defaults.dispute.slashingPercentage) + .div(new BN(MAX_PPM)) + .mul(defaults.dispute.fishermanRewardPercentage) + .div(new BN(MAX_PPM)) + const funcReward = await this.disputeManager.getTokensToReward(indexer) + expect(funcReward).to.be.bignumber.eq(trueReward.toString()) + }) }) - }) - context('> when dispute is not created', function() { describe('create dispute', function() { it('reject fisherman deposit below minimum required', async function() { // Minimum deposit a fisherman is required to do should be >= reward @@ -318,51 +383,6 @@ contract('Disputes', ([me, other, governor, arbitrator, indexer, fisherman, othe }) }) }) - }) - - context('> when dispute is created', function() { - beforeEach(async function() { - // Create dispute - await this.disputeManager.createDispute(this.dispute.attestation, this.fishermanDeposit, { - from: fisherman, - }) - }) - - describe('create a dispute', function() { - it('should create dispute if receipt is equal but for different indexer', async function() { - // Create dispute (same receipt but different indexer) - const newDispute = await attestation.createDispute( - this.dispute.receipt, - this.disputeManager.address, - this.otherIndexerChannelPrivKey, - otherIndexer, - ) - const { logs } = await this.disputeManager.createDispute( - newDispute.attestation, - this.fishermanDeposit, - { from: fisherman }, - ) - - // Event emitted - expectEvent.inLogs(logs, 'DisputeCreated', { - disputeID: newDispute.id, - subgraphID: newDispute.receipt.subgraphID, - indexer: otherIndexer, - fisherman: fisherman, - tokens: this.fishermanDeposit, - attestation: newDispute.attestation, - }) - }) - - it('reject create duplicated dispute', async function() { - await expectRevert( - this.disputeManager.createDispute(this.dispute.attestation, this.fishermanDeposit, { - from: fisherman, - }), - 'Dispute already created', - ) - }) - }) describe('accept a dispute', function() { it('reject to accept a non-existing dispute', async function() { @@ -373,171 +393,224 @@ contract('Disputes', ([me, other, governor, arbitrator, indexer, fisherman, othe 'Dispute does not exist', ) }) + }) - it('reject to accept a dispute if not the arbitrator', async function() { + describe('reject a dispute', function() { + it('reject to reject a non-existing dispute', async function() { await expectRevert( - this.disputeManager.acceptDispute(this.dispute.id, { - from: me, + this.disputeManager.rejectDispute(NON_EXISTING_DISPUTE_ID, { + from: arbitrator, }), - 'Caller is not the Arbitrator', + 'Dispute does not exist', ) }) + }) - it('reject to accept dispute if DisputeManager is not slasher', async function() { - // Dispute manager is not allowed to slash - await this.staking.setSlasher(this.disputeManager.address, false, { - from: governor, - }) - - // Perform transaction (accept) + describe('draw a dispute', function() { + it('reject to draw a non-existing dispute', async function() { await expectRevert( - this.disputeManager.acceptDispute(this.dispute.id, { + this.disputeManager.drawDispute(NON_EXISTING_DISPUTE_ID, { from: arbitrator, }), - 'Caller is not a Slasher', + 'Dispute does not exist', ) }) + }) - it('reject to accept a dispute if zero tokens to slash', async function() { - await this.disputeManager.setSlashingPercentage(new BN(0), { from: governor }) - await expectRevert( - this.disputeManager.acceptDispute(this.dispute.id, { - from: arbitrator, - }), - 'Dispute has zero tokens to slash', + context('> when dispute is created', function() { + beforeEach(async function() { + // Create dispute + await this.disputeManager.createDispute( + this.dispute.attestation, + this.fishermanDeposit, + { from: fisherman }, ) }) - it('should resolve dispute, slash indexer and reward the fisherman', async function() { - const indexerStakeBefore = await this.staking.getIndexerStakedTokens(indexer) - const tokensToSlash = await this.disputeManager.getTokensToSlash(indexer) - const fishermanBalanceBefore = await this.grt.balanceOf(fisherman) - const totalSupplyBefore = await this.grt.totalSupply() - const reward = await this.disputeManager.getTokensToReward(indexer) - - // Perform transaction (accept) - const { logs } = await this.disputeManager.acceptDispute(this.dispute.id, { - from: arbitrator, + describe('create a dispute', function() { + it('should create dispute if receipt is equal but for other indexer', async function() { + // Create dispute (same receipt but different indexer) + const newDispute = await attestation.createDispute( + this.dispute.receipt, + this.disputeManager.address, + this.otherIndexerChannelPrivKey, + otherIndexer, + ) + const { logs } = await this.disputeManager.createDispute( + newDispute.attestation, + this.fishermanDeposit, + { from: fisherman }, + ) + + // Event emitted + expectEvent.inLogs(logs, 'DisputeCreated', { + disputeID: newDispute.id, + subgraphID: newDispute.receipt.subgraphID, + indexer: otherIndexer, + fisherman: fisherman, + tokens: this.fishermanDeposit, + attestation: newDispute.attestation, + }) }) - // Fisherman reward properly assigned + deposit returned - const fishermanBalanceAfter = await this.grt.balanceOf(fisherman) - expect(fishermanBalanceAfter).to.be.bignumber.eq( - fishermanBalanceBefore.add(this.fishermanDeposit).add(reward), - ) - - // Indexer slashed - const indexerStakeAfter = await this.staking.getIndexerStakedTokens(indexer) - expect(indexerStakeAfter).to.be.bignumber.eq(indexerStakeBefore.sub(tokensToSlash)) - - // Slashed funds burned - const tokensToBurn = tokensToSlash.sub(reward) - const totalSupplyAfter = await this.grt.totalSupply() - expect(totalSupplyAfter).to.be.bignumber.eq(totalSupplyBefore.sub(tokensToBurn)) - - // Event emitted - expectEvent.inLogs(logs, 'DisputeAccepted', { - disputeID: this.dispute.id, - subgraphID: this.dispute.receipt.subgraphID, - indexer: indexer, - fisherman: fisherman, - tokens: this.fishermanDeposit.add(reward), + it('reject create duplicated dispute', async function() { + await expectRevert( + this.disputeManager.createDispute(this.dispute.attestation, this.fishermanDeposit, { + from: fisherman, + }), + 'Dispute already created', + ) }) }) - }) - - describe('reject a dispute', async function() { - it('reject to reject a non-existing dispute', async function() { - await expectRevert( - this.disputeManager.rejectDispute(NON_EXISTING_DISPUTE_ID, { - from: arbitrator, - }), - 'Dispute does not exist', - ) - }) - - it('reject to reject a dispute if not the arbitrator', async function() { - await expectRevert( - this.disputeManager.rejectDispute(this.dispute.id, { - from: me, - }), - 'Caller is not the Arbitrator', - ) - }) - it('should reject a dispute and burn deposit', async function() { - const fishermanBalanceBefore = await this.grt.balanceOf(fisherman) - const totalSupplyBefore = await this.grt.totalSupply() + describe('accept a dispute', function() { + it('reject to accept a dispute if not the arbitrator', async function() { + await expectRevert( + this.disputeManager.acceptDispute(this.dispute.id, { + from: me, + }), + 'Caller is not the Arbitrator', + ) + }) - // Perform transaction (reject) - const { logs } = await this.disputeManager.rejectDispute(this.dispute.id, { - from: arbitrator, + it('reject to accept a dispute if not slasher', async function() { + // Dispute manager is not allowed to slash + await this.staking.setSlasher(this.disputeManager.address, false, { + from: governor, + }) + + // Perform transaction (accept) + await expectRevert( + this.disputeManager.acceptDispute(this.dispute.id, { + from: arbitrator, + }), + 'Caller is not a Slasher', + ) }) - // No change in fisherman balance - const fishermanBalanceAfter = await this.grt.balanceOf(fisherman) - expect(fishermanBalanceAfter).to.be.bignumber.eq(fishermanBalanceBefore) + it('reject to accept a dispute if zero tokens to slash', async function() { + await this.disputeManager.setSlashingPercentage(new BN(0), { from: governor }) + await expectRevert( + this.disputeManager.acceptDispute(this.dispute.id, { + from: arbitrator, + }), + 'Dispute has zero tokens to slash', + ) + }) - // Burn fisherman deposit - const totalSupplyAfter = await this.grt.totalSupply() - const burnedTokens = web3.utils.toBN(this.fishermanDeposit) - expect(totalSupplyAfter).to.be.bignumber.eq(totalSupplyBefore.sub(burnedTokens)) + it('should resolve dispute, slash indexer and reward the fisherman', async function() { + const indexerStakeBefore = await this.staking.getIndexerStakedTokens(indexer) + const tokensToSlash = await this.disputeManager.getTokensToSlash(indexer) + const fishermanBalanceBefore = await this.grt.balanceOf(fisherman) + const totalSupplyBefore = await this.grt.totalSupply() + const reward = await this.disputeManager.getTokensToReward(indexer) - // Event emitted - expectEvent.inLogs(logs, 'DisputeRejected', { - disputeID: this.dispute.id, - subgraphID: this.dispute.receipt.subgraphID, - indexer: indexer, - fisherman: fisherman, - tokens: this.fishermanDeposit, + // Perform transaction (accept) + const { logs } = await this.disputeManager.acceptDispute(this.dispute.id, { + from: arbitrator, + }) + + // Fisherman reward properly assigned + deposit returned + const fishermanBalanceAfter = await this.grt.balanceOf(fisherman) + expect(fishermanBalanceAfter).to.be.bignumber.eq( + fishermanBalanceBefore.add(this.fishermanDeposit).add(reward), + ) + + // Indexer slashed + const indexerStakeAfter = await this.staking.getIndexerStakedTokens(indexer) + expect(indexerStakeAfter).to.be.bignumber.eq(indexerStakeBefore.sub(tokensToSlash)) + + // Slashed funds burned + const tokensToBurn = tokensToSlash.sub(reward) + const totalSupplyAfter = await this.grt.totalSupply() + expect(totalSupplyAfter).to.be.bignumber.eq(totalSupplyBefore.sub(tokensToBurn)) + + // Event emitted + expectEvent.inLogs(logs, 'DisputeAccepted', { + disputeID: this.dispute.id, + subgraphID: this.dispute.receipt.subgraphID, + indexer: indexer, + fisherman: fisherman, + tokens: this.fishermanDeposit.add(reward), + }) }) }) - }) - describe('draw a dispute', async function() { - it('reject to draw a non-existing dispute', async function() { - await expectRevert( - this.disputeManager.drawDispute(NON_EXISTING_DISPUTE_ID, { - from: arbitrator, - }), - 'Dispute does not exist', - ) - }) + describe('reject a dispute', async function() { + it('reject to reject a dispute if not the arbitrator', async function() { + await expectRevert( + this.disputeManager.rejectDispute(this.dispute.id, { + from: me, + }), + 'Caller is not the Arbitrator', + ) + }) - it('reject to draw a dispute if not the arbitrator', async function() { - await expectRevert( - this.disputeManager.drawDispute(this.dispute.id, { - from: me, - }), - 'Caller is not the Arbitrator', - ) - }) + it('should reject a dispute and burn deposit', async function() { + const fishermanBalanceBefore = await this.grt.balanceOf(fisherman) + const totalSupplyBefore = await this.grt.totalSupply() - it('should draw a dispute and return deposit', async function() { - const fishermanBalanceBefore = await this.grt.balanceOf(fisherman) + // Perform transaction (reject) + const { logs } = await this.disputeManager.rejectDispute(this.dispute.id, { + from: arbitrator, + }) + + // No change in fisherman balance + const fishermanBalanceAfter = await this.grt.balanceOf(fisherman) + expect(fishermanBalanceAfter).to.be.bignumber.eq(fishermanBalanceBefore) + + // Burn fisherman deposit + const totalSupplyAfter = await this.grt.totalSupply() + const burnedTokens = new BN(this.fishermanDeposit) + expect(totalSupplyAfter).to.be.bignumber.eq(totalSupplyBefore.sub(burnedTokens)) + + // Event emitted + expectEvent.inLogs(logs, 'DisputeRejected', { + disputeID: this.dispute.id, + subgraphID: this.dispute.receipt.subgraphID, + indexer: indexer, + fisherman: fisherman, + tokens: this.fishermanDeposit, + }) + }) + }) - // Perform transaction (draw) - const { logs } = await this.disputeManager.drawDispute(this.dispute.id, { - from: arbitrator, + describe('draw a dispute', async function() { + it('reject to draw a dispute if not the arbitrator', async function() { + await expectRevert( + this.disputeManager.drawDispute(this.dispute.id, { + from: me, + }), + 'Caller is not the Arbitrator', + ) }) - // Fisherman should see the deposit returned - const fishermanBalanceAfter = await this.grt.balanceOf(fisherman) - expect(fishermanBalanceAfter).to.be.bignumber.eq( - fishermanBalanceBefore.add(this.fishermanDeposit), - ) + it('should draw a dispute and return deposit', async function() { + const fishermanBalanceBefore = await this.grt.balanceOf(fisherman) - // Event emitted - expectEvent.inLogs(logs, 'DisputeDrawn', { - disputeID: this.dispute.id, - subgraphID: this.dispute.receipt.subgraphID, - indexer: indexer, - fisherman: fisherman, - tokens: this.fishermanDeposit, + // Perform transaction (draw) + const { logs } = await this.disputeManager.drawDispute(this.dispute.id, { + from: arbitrator, + }) + + // Fisherman should see the deposit returned + const fishermanBalanceAfter = await this.grt.balanceOf(fisherman) + expect(fishermanBalanceAfter).to.be.bignumber.eq( + fishermanBalanceBefore.add(this.fishermanDeposit), + ) + + // Event emitted + expectEvent.inLogs(logs, 'DisputeDrawn', { + disputeID: this.dispute.id, + subgraphID: this.dispute.receipt.subgraphID, + indexer: indexer, + fisherman: fisherman, + tokens: this.fishermanDeposit, + }) }) }) }) }) }) - }) -}) + }, +) diff --git a/test/staking/general.test.js b/test/staking/general.test.js index 4dd1c3499..b0da55438 100644 --- a/test/staking/general.test.js +++ b/test/staking/general.test.js @@ -20,7 +20,17 @@ function toGRT(value) { return new BN(web3.utils.toWei(value)) } -contract('Staking', ([me, other, governor, indexer, slasher, fisherman]) => { +contract('Staking', ([me, other, governor, indexer, slasher, fisherman, channelProxy]) => { + before(async function() { + // Helpers + this.advanceToNextEpoch = async () => { + const currentBlock = await time.latestBlock() + const epochLength = await this.epochManager.epochLength() + const nextEpochBlock = currentBlock.add(epochLength) + await time.advanceBlockTo(nextEpochBlock) + } + }) + beforeEach(async function() { // Deploy epoch contract this.epochManager = await deployment.deployEpochManagerContract(governor, { from: me }) @@ -49,14 +59,6 @@ contract('Staking', ([me, other, governor, indexer, slasher, fisherman]) => { // Set staking as distributor of funds to curation await this.curation.setStaking(this.staking.address, { from: governor }) - - // Helpers - this.advanceToNextEpoch = async () => { - const currentBlock = await time.latestBlock() - const epochLength = await this.epochManager.epochLength() - const nextEpochBlock = currentBlock.add(epochLength) - await time.advanceBlockTo(nextEpochBlock) - } }) describe('configuration', function() { @@ -182,9 +184,14 @@ contract('Staking', ([me, other, governor, indexer, slasher, fisherman]) => { return this.staking.stake(tokens, { from: indexer }) } this.allocate = function(tokens) { - return this.staking.allocate(this.subgraphID, tokens, this.channelPubKey, this.price, { - from: indexer, - }) + return this.staking.allocate( + this.subgraphID, + tokens, + this.channelPubKey, + channelProxy, + this.price, + { from: indexer }, + ) } this.shouldStake = async function(indexerStake) { // Setup @@ -209,9 +216,9 @@ contract('Staking', ([me, other, governor, indexer, slasher, fisherman]) => { // Setup this.indexerStake = toGRT('100') this.subgraphID = helpers.randomSubgraphId() + this.channelID = '0x6367E9dD7641e0fF221740b57B8C730031d72530' this.channelPubKey = '0x0456708870bfd5d8fc956fe33285dcf59b075cd7a25a21ee00834e480d3754bcda180e670145a290bb4bebca8e105ea7776a7b39e16c4df7d4d1083260c6f05d53' - this.channelID = '0x6367E9dD7641e0fF221740b57B8C730031d72530' this.price = toGRT('0.01') // Give some funds to the indexer @@ -573,9 +580,16 @@ contract('Staking', ([me, other, governor, indexer, slasher, fisherman]) => { const tokensToAllocate = toGRT('10') const subgraphID = helpers.randomSubgraphId() await expectRevert( - this.staking.allocate(subgraphID, tokensToAllocate, this.channelPubKey, this.price, { - from: indexer, - }), + this.staking.allocate( + subgraphID, + tokensToAllocate, + this.channelPubKey, + channelProxy, + this.price, + { + from: indexer, + }, + ), 'Allocation: channel ID already in use', ) }) @@ -589,10 +603,10 @@ contract('Staking', ([me, other, governor, indexer, slasher, fisherman]) => { // Create the allocation to be settled await this.allocate(this.tokensAllocated) - - // Give some funds to the settlor - await this.grt.mint(me, this.tokensToSettle, { from: governor }) - await this.grt.approve(this.staking.address, this.tokensToSettle, { from: me }) + await this.grt.mint(channelProxy, this.tokensToSettle, { from: governor }) + await this.grt.approve(this.staking.address, this.tokensToSettle, { + from: channelProxy, + }) }) it('should settle and distribute funds', async function() { @@ -613,7 +627,9 @@ contract('Staking', ([me, other, governor, indexer, slasher, fisherman]) => { await this.advanceToNextEpoch() // Settle - const { logs } = await this.staking.settle(this.channelID, this.tokensToSettle) + const { logs } = await this.staking.settle(this.tokensToSettle, { + from: channelProxy, + }) // Get epoch information const result = await this.epochManager.epochsSince(allocBefore.createdAtEpoch) @@ -655,7 +671,7 @@ contract('Staking', ([me, other, governor, indexer, slasher, fisherman]) => { epoch: currentEpoch, tokens: this.tokensToSettle, channelID: this.channelID, - from: me, + from: channelProxy, curationFees: curationFees, rebateFees: rebateFees, effectiveAllocation: effectiveAlloc, @@ -667,63 +683,35 @@ contract('Staking', ([me, other, governor, indexer, slasher, fisherman]) => { await this.advanceToNextEpoch() // Settle zero tokens - await this.staking.settle(this.channelID, new BN('0')) + await this.staking.settle(new BN('0'), { from: channelProxy }) }) it('reject settle if channel does not exist', async function() { await expectRevert( - this.staking.settle(ZERO_ADDRESS, this.tokensToSettle), + this.staking.settle(this.tokensToSettle, { from: other }), 'Channel: does not exist', ) }) - it('reject settle if allocation does not have an active channel', async function() { + it('reject settle from an already settled channel', async function() { // Advance blocks to get the channel in epoch where it can be settled await this.advanceToNextEpoch() // Settle the channel - await this.staking.settle(this.channelID, this.tokensToSettle.div(new BN('2'))) + await this.staking.settle(this.tokensToSettle.div(new BN('2')), { + from: channelProxy, + }) // Settle the same channel to force an error await expectRevert( - this.staking.settle(this.channelID, this.tokensToSettle.div(new BN('2'))), - 'Channel: The allocation has no channel, or the channel was already settled', - ) - }) - - it('reject settle using an already settled channel', async function() { - const newChannelPubKey = - '0x04f2ba8222e1dbe3ebe6a72bac637cfe7eb9ea282c6f3319fd239ca3c69a088aabcbbf2a4484c49fe86b3165c61b13922376435961cf83e7e756b1d3fe3ead0206' - const channelID1 = this.channelID - - // Advance blocks to get the channel in epoch where it can be settled - await this.advanceToNextEpoch() - - // Settle the channel - await this.staking.settle(channelID1, this.tokensToSettle.div(new BN('2'))) - - // We allocate to the same subgraph with new channel - await this.staking.allocate( - this.subgraphID, - this.tokensAllocated, - newChannelPubKey, - this.price, - { from: indexer }, - ) - - // Advance blocks to get the channel in epoch where it can be settled - await this.advanceToNextEpoch() - - // Settle the channel using the old channel should be invalid - await expectRevert( - this.staking.settle(channelID1, this.tokensToSettle.div(new BN('2'))), - 'Channel: The allocation has no channel, or the channel was already settled', + this.staking.settle(this.tokensToSettle.div(new BN('2')), { from: channelProxy }), + 'Channel: does not exist', ) }) it('reject settle if an epoch has not passed', async function() { await expectRevert( - this.staking.settle(this.channelID, this.tokensToSettle), + this.staking.settle(this.tokensToSettle, { from: channelProxy }), 'Channel: Can only settle after one epoch passed', ) }) @@ -776,10 +764,8 @@ contract('Staking', ([me, other, governor, indexer, slasher, fisherman]) => { // Create the allocation to be settled await this.allocate(this.tokensAllocated) - - // Give some tokens to the settlor - await this.grt.mint(me, this.tokensToSettle, { from: governor }) - await this.grt.approve(this.staking.address, this.tokensToSettle, { from: me }) + await this.grt.mint(channelProxy, this.tokensToSettle, { from: governor }) + await this.grt.approve(this.staking.address, this.tokensToSettle, { from: channelProxy }) // Advance blocks to get the channel in epoch where it can be settled await this.advanceToNextEpoch() @@ -813,7 +799,7 @@ contract('Staking', ([me, other, governor, indexer, slasher, fisherman]) => { // Settle zero tokens this.tokensToSettle = new BN('0') - await this.staking.settle(this.channelID, this.tokensToSettle) + await this.staking.settle(this.tokensToSettle, { from: channelProxy }) this.rebateEpoch = await this.epochManager.currentEpoch() // Advance blocks to get the channel in epoch where it can be claimed @@ -832,7 +818,7 @@ contract('Staking', ([me, other, governor, indexer, slasher, fisherman]) => { context('> when settled', function() { beforeEach(async function() { // Settle - await this.staking.settle(this.channelID, this.tokensToSettle, { from: me }) + await this.staking.settle(this.tokensToSettle, { from: channelProxy }) this.rebateEpoch = await this.epochManager.currentEpoch() // Advance blocks to get the channel in epoch where it can be claimed