diff --git a/contracts/curation/Curation.sol b/contracts/curation/Curation.sol index fc59b33d1..d46a40020 100644 --- a/contracts/curation/Curation.sol +++ b/contracts/curation/Curation.sol @@ -257,7 +257,7 @@ contract Curation is CurationV1Storage, GraphUpgradeable { // Update curation pool curationPool.tokens = curationPool.tokens.add(_tokensIn.sub(curationTax)); - curationPool.gcs.mint(curator, signalOut); + curationPool.gcs.mint(curator, signalOut, _tokensIn.sub(curationTax)); emit Signalled(curator, _subgraphDeploymentID, _tokensIn, signalOut, curationTax); @@ -269,14 +269,14 @@ contract Curation is CurationV1Storage, GraphUpgradeable { * @notice Burn _signal from the SubgraphDeployment curation pool * @param _subgraphDeploymentID SubgraphDeployment the curator is returning signal * @param _signalIn Amount of signal to return - * @param _tokensOutMin Expected minimum amount of tokens to receive * @return Tokens returned */ - function burn( - bytes32 _subgraphDeploymentID, - uint256 _signalIn, - uint256 _tokensOutMin - ) external override notPartialPaused returns (uint256) { + function burn(bytes32 _subgraphDeploymentID, uint256 _signalIn) + external + override + notPartialPaused + returns (uint256) + { address curator = msg.sender; // Validations @@ -286,11 +286,9 @@ contract Curation is CurationV1Storage, GraphUpgradeable { "Cannot burn more signal than you own" ); - // Get the amount of tokens to refund based on returned signal - uint256 tokensOut = signalToTokens(_subgraphDeploymentID, _signalIn); - - // Slippage protection - require(tokensOut >= _tokensOutMin, "Slippage protection"); + // Get the amount of tokens to refund based on the amount + // deposited by the curator and unsignal amount + uint256 tokensOut = signalToTokens(_subgraphDeploymentID, _signalIn, curator); // Trigger update rewards calculation _updateRewards(_subgraphDeploymentID); @@ -433,30 +431,31 @@ contract Curation is CurationV1Storage, GraphUpgradeable { * @param _signalIn Amount of signal to burn * @return Amount of tokens to get for an amount of signal */ - function signalToTokens(bytes32 _subgraphDeploymentID, uint256 _signalIn) - public - view - override - returns (uint256) - { + function signalToTokens( + bytes32 _subgraphDeploymentID, + uint256 _signalIn, + address _curator + ) public view override returns (uint256) { CurationPool memory curationPool = pools[_subgraphDeploymentID]; - uint256 curationPoolSignal = getCurationPoolSignal(_subgraphDeploymentID); require( curationPool.tokens > 0, "Subgraph deployment must be curated to perform calculations" ); + uint256 curationPoolSignal = getCurationPoolSignal(_subgraphDeploymentID); require( curationPoolSignal >= _signalIn, "Signal must be above or equal to signal issued in the curation pool" ); + uint256 curatorSignal = getCuratorSignal(_curator, _subgraphDeploymentID); + uint256 poolTokens = curationPool.tokens; - return - BancorFormula(bondingCurve).calculateSaleReturn( - curationPoolSignal, - curationPool.tokens, - curationPool.reserveRatio, - _signalIn - ); + uint256 royaltiesOf = poolTokens + .sub(curationPool.gcs.totalDeposited()) + .mul(curatorSignal) + .div(curationPoolSignal); + uint256 grtValueOf = curationPool.gcs.grtValueOf(_curator, _signalIn); + + return grtValueOf.add(royaltiesOf); } /** diff --git a/contracts/curation/GraphCurationToken.sol b/contracts/curation/GraphCurationToken.sol index 78b721e1b..0fe96c273 100644 --- a/contracts/curation/GraphCurationToken.sol +++ b/contracts/curation/GraphCurationToken.sol @@ -2,6 +2,7 @@ pragma solidity ^0.7.6; +import "@openzeppelin/contracts/math/SafeMath.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; import "../governance/Governed.sol"; @@ -19,6 +20,13 @@ import "../governance/Governed.sol"; * gas-saving purposes. */ contract GraphCurationToken is ERC20Upgradeable, Governed { + using SafeMath for uint256; + + // Bookkeeping for GRT deposits + mapping(address => uint256) public deposits; + + uint256 public totalDeposited; + /** * @dev Graph Curation Token Contract initializer. * @param _owner Address of the contract issuing this token @@ -32,9 +40,16 @@ contract GraphCurationToken is ERC20Upgradeable, Governed { * @dev Mint new tokens. * @param _to Address to send the newly minted tokens * @param _amount Amount of tokens to mint + * @param _grtDeposit Amount of GRT deposited to mint the GCS */ - function mint(address _to, uint256 _amount) public onlyGovernor { + function mint( + address _to, + uint256 _amount, + uint256 _grtDeposit + ) public onlyGovernor { _mint(_to, _amount); + deposits[_to] = deposits[_to].add(_grtDeposit); + totalDeposited = totalDeposited.add(_grtDeposit); } /** @@ -43,6 +58,27 @@ contract GraphCurationToken is ERC20Upgradeable, Governed { * @param _amount Amount of tokens to burn */ function burnFrom(address _account, uint256 _amount) public onlyGovernor { + uint256 delta = grtValueOf(_account, _amount); _burn(_account, _amount); + deposits[_account] = deposits[_account].sub(delta); + totalDeposited = totalDeposited.sub(delta); + } + + /** + * @dev Transfer tokens from the sender to an address. + * @param _recipient Address to where tokens will be sent + * @param _amount Amount of tokens to burn + */ + function transfer(address _recipient, uint256 _amount) public virtual override returns (bool) { + uint256 depositDelta = grtValueOf(msg.sender, _amount); + _transfer(msg.sender, _recipient, _amount); + deposits[msg.sender] = deposits[msg.sender].sub(depositDelta); + deposits[_recipient] = deposits[_recipient].add(depositDelta); + return true; + } + + function grtValueOf(address _account, uint256 _amount) public view returns (uint256) { + return + balanceOf(_account) == 0 ? 0 : deposits[_account].mul(_amount).div(balanceOf(_account)); } } diff --git a/contracts/curation/ICuration.sol b/contracts/curation/ICuration.sol index 9e1701aaf..bdbfacaca 100644 --- a/contracts/curation/ICuration.sol +++ b/contracts/curation/ICuration.sol @@ -25,8 +25,7 @@ interface ICuration { function burn( bytes32 _subgraphDeploymentID, - uint256 _signalIn, - uint256 _tokensOutMin + uint256 _signalIn ) external returns (uint256); function collect(bytes32 _subgraphDeploymentID, uint256 _tokens) external; @@ -49,7 +48,7 @@ interface ICuration { view returns (uint256, uint256); - function signalToTokens(bytes32 _subgraphDeploymentID, uint256 _signalIn) + function signalToTokens(bytes32 _subgraphDeploymentID, uint256 _signalIn, address _curator) external view returns (uint256); diff --git a/contracts/curation/IGraphCurationToken.sol b/contracts/curation/IGraphCurationToken.sol index 43679aba6..dbbf072ee 100644 --- a/contracts/curation/IGraphCurationToken.sol +++ b/contracts/curation/IGraphCurationToken.sol @@ -9,5 +9,9 @@ interface IGraphCurationToken is IERC20Upgradeable { function burnFrom(address _account, uint256 _amount) external; - function mint(address _to, uint256 _amount) external; + function mint(address _to, uint256 _amount, uint256 _grtDeposit) external; + + function grtValueOf(address _account, uint256 _amount) external view returns (uint256); + + function totalDeposited() external view returns (uint256); } diff --git a/contracts/discovery/GNS.sol b/contracts/discovery/GNS.sol index 3f0e16f6d..1f282de11 100644 --- a/contracts/discovery/GNS.sol +++ b/contracts/discovery/GNS.sol @@ -313,12 +313,11 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { // Move all signal from previous version to new version // NOTE: We will only do this as long as there is signal on the subgraph if (subgraphData.nSignal > 0) { - // Burn all version signal in the name pool for tokens (w/no slippage protection) + // Burn all version signal in the name pool for tokens // Sell all signal from the old deployment uint256 tokens = curation.burn( subgraphData.subgraphDeploymentID, - subgraphData.vSignal, - 0 + subgraphData.vSignal ); // Take the owner cut of the curation tax, add it to the total @@ -367,8 +366,7 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { if (subgraphData.nSignal > 0) { subgraphData.withdrawableGRT = curation().burn( subgraphData.subgraphDeploymentID, - subgraphData.vSignal, - 0 + subgraphData.vSignal ); } @@ -422,12 +420,10 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { * @dev Burn signal for a subgraph and return the GRT. * @param _subgraphID Subgraph ID * @param _nSignal The amount of nSignal the nameCurator wants to burn - * @param _tokensOutMin Expected minimum amount of tokens to receive */ function burnSignal( uint256 _subgraphID, - uint256 _nSignal, - uint256 _tokensOutMin + uint256 _nSignal ) external override notPartialPaused { // Subgraph checks SubgraphData storage subgraphData = _getSubgraphOrRevert(_subgraphID); @@ -442,7 +438,7 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { // Get tokens for name signal amount to burn uint256 vSignal = nSignalToVSignal(_subgraphID, _nSignal); - uint256 tokens = curation().burn(subgraphData.subgraphDeploymentID, vSignal, _tokensOutMin); + uint256 tokens = curation().burn(subgraphData.subgraphDeploymentID, vSignal); // Update pools subgraphData.vSignal = subgraphData.vSignal.sub(vSignal); @@ -569,7 +565,7 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { // It does not make sense to convert signal from a disabled or non-existing one SubgraphData storage subgraphData = _getSubgraphOrRevert(_subgraphID); uint256 vSignal = nSignalToVSignal(_subgraphID, _nSignalIn); - uint256 tokensOut = curation().signalToTokens(subgraphData.subgraphDeploymentID, vSignal); + uint256 tokensOut = curation().signalToTokens(subgraphData.subgraphDeploymentID, vSignal, address(this)); return (vSignal, tokensOut); } diff --git a/contracts/discovery/IGNS.sol b/contracts/discovery/IGNS.sol index 92300627e..3903fe640 100644 --- a/contracts/discovery/IGNS.sol +++ b/contracts/discovery/IGNS.sol @@ -61,8 +61,7 @@ interface IGNS { function burnSignal( uint256 _subgraphID, - uint256 _nSignal, - uint256 _tokensOutMin + uint256 _nSignal ) external; function withdraw(uint256 _subgraphID) external; diff --git a/test/curation/curation.test.ts b/test/curation/curation.test.ts index 897ef8da3..4068d858b 100644 --- a/test/curation/curation.test.ts +++ b/test/curation/curation.test.ts @@ -137,7 +137,7 @@ describe('Curation', () => { const beforeTotalTokens = await grt.balanceOf(curation.address) // Redeem - const tx = curation.connect(curator.signer).burn(subgraphDeploymentID, signalToRedeem, 0) + const tx = curation.connect(curator.signer).burn(subgraphDeploymentID, signalToRedeem) await expect(tx) .emit(curation, 'Burned') .withArgs(curator.address, subgraphDeploymentID, expectedTokens, signalToRedeem) @@ -212,7 +212,7 @@ describe('Curation', () => { const tokensToDeposit = curatorTokens it('reject convert signal to tokens if subgraph deployment not initted', async function () { - const tx = curation.signalToTokens(subgraphDeploymentID, toGRT('100')) + const tx = curation.signalToTokens(subgraphDeploymentID, toGRT('100'), curator.address) await expect(tx).revertedWith('Subgraph deployment must be curated to perform calculations') }) @@ -222,7 +222,7 @@ describe('Curation', () => { // Conversion const signal = await curation.getCurationPoolSignal(subgraphDeploymentID) - const expectedTokens = await curation.signalToTokens(subgraphDeploymentID, signal) + const expectedTokens = await curation.signalToTokens(subgraphDeploymentID, signal, curator.address) expect(expectedTokens).eq(tokensToDeposit) }) @@ -241,7 +241,7 @@ describe('Curation', () => { // Conversion const signal = await curation.getCurationPoolSignal(subgraphDeploymentID) - const tokens = await curation.signalToTokens(subgraphDeploymentID, signal) + const tokens = await curation.signalToTokens(subgraphDeploymentID, signal, curator.address) expect(tokens).eq(tokensToDeposit.sub(expectedCurationTax)) expect(expectedCurationTax).eq(curationTax) }) @@ -377,7 +377,7 @@ describe('Curation', () => { ).sub(signalOutRemainder) const tx1 = await curation .connect(curator.signer) - .burn(subgraphDeploymentID, signalOutPartial, 0) + .burn(subgraphDeploymentID, signalOutPartial) const r1 = await tx1.wait() const event1 = curation.interface.parseLog(r1.events[2]).args const tokensOut1 = event1.tokens @@ -388,7 +388,7 @@ describe('Curation', () => { // Unsignal the rest const tx2 = await curation .connect(curator.signer) - .burn(subgraphDeploymentID, signalOutRemainder, 0) + .burn(subgraphDeploymentID, signalOutRemainder) const r2 = await tx2.wait() const event2 = curation.interface.parseLog(r2.events[2]).args const tokensOut2 = event2.tokens @@ -404,19 +404,19 @@ describe('Curation', () => { }) it('reject redeem more than a curator owns', async function () { - const tx = curation.connect(me.signer).burn(subgraphDeploymentID, toGRT('1'), 0) + const tx = curation.connect(me.signer).burn(subgraphDeploymentID, toGRT('1')) await expect(tx).revertedWith('Cannot burn more signal than you own') }) it('reject redeem zero signal', async function () { - const tx = curation.connect(me.signer).burn(subgraphDeploymentID, toGRT('0'), 0) + const tx = curation.connect(me.signer).burn(subgraphDeploymentID, toGRT('0')) await expect(tx).revertedWith('Cannot burn zero signal') }) it('should allow to redeem *partially*', async function () { // Redeem just one signal const signalToRedeem = toGRT('1') - const expectedTokens = toGRT('532.455532033675866536') + const expectedTokens = toGRT('316.227766016837933299') await shouldBurn(signalToRedeem, expectedTokens) }) @@ -431,7 +431,7 @@ describe('Curation', () => { // Redeem "almost" all signal const signal = await curation.getCuratorSignal(curator.address, subgraphDeploymentID) const signalToRedeem = signal.sub(toGRT('0.000001')) - const expectedTokens = await curation.signalToTokens(subgraphDeploymentID, signalToRedeem) + const expectedTokens = await curation.signalToTokens(subgraphDeploymentID, signalToRedeem, curator.address) await shouldBurn(signalToRedeem, expectedTokens) // The pool should have less tokens that required by minimumCurationDeposit @@ -447,16 +447,6 @@ describe('Curation', () => { await shouldMint(tokensToDeposit, expectedSignal) }) - it('should revert redeem if over slippage', async function () { - const signalToRedeem = await curation.getCuratorSignal(curator.address, subgraphDeploymentID) - const expectedTokens = tokensToDeposit - - const tx = curation - .connect(curator.signer) - .burn(subgraphDeploymentID, signalToRedeem, expectedTokens.add(1)) - await expect(tx).revertedWith('Slippage protection') - }) - it('should not re-deploy the curation token when signal is reset', async function () { const beforeSubgraphPool = await curation.pools(subgraphDeploymentID) @@ -497,7 +487,7 @@ describe('Curation', () => { for (const signalToRedeem of chunkify(totalSignal, 10)) { const tx = await curation .connect(curator.signer) - .burn(subgraphDeploymentID, signalToRedeem, 0) + .burn(subgraphDeploymentID, signalToRedeem) const receipt = await tx.wait() const event: Event = receipt.events.pop() const tokens = event.args['tokens'] diff --git a/test/gns.test.ts b/test/gns.test.ts index 6494e665b..6f7a6b795 100644 --- a/test/gns.test.ts +++ b/test/gns.test.ts @@ -425,7 +425,7 @@ describe('GNS', () => { ) // Send tx - const tx = gns.connect(account.signer).burnSignal(subgraphID, beforeUsersNSignal, 0) + const tx = gns.connect(account.signer).burnSignal(subgraphID, beforeUsersNSignal) await expect(tx) .emit(gns, 'SignalBurned') .withArgs(subgraphID, account.address, beforeUsersNSignal, vSignalExpected, tokensExpected) @@ -795,7 +795,7 @@ describe('GNS', () => { it('should fail when name signal is disabled', async function () { await deprecateSubgraph(me, subgraph.id) // just test 1 since it will fail - const tx = gns.connect(me.signer).burnSignal(subgraph.id, 1, 0) + const tx = gns.connect(me.signer).burnSignal(subgraph.id, 1) await expect(tx).revertedWith('GNS: Must be active') }) @@ -803,25 +803,10 @@ describe('GNS', () => { const tx = gns.connect(me.signer).burnSignal( subgraph.id, // 1000000 * 10^18 nSignal is a lot, and will cause fail - toBN('1000000000000000000000000'), - 0, + toBN('1000000000000000000000000') ) await expect(tx).revertedWith('GNS: Curator cannot withdraw more nSignal than they have') }) - - it('reject burning if under slippage', async function () { - // Get current curator name signal - const curatorNSignal = await gns.getCuratorSignal(subgraph.id, other.address) - - // Withdraw - const { 1: expectedTokens } = await gns.nSignalToTokens(subgraph.id, curatorNSignal) - - // Force a revert by asking 1 more token than the function will return - const tx = gns - .connect(other.signer) - .burnSignal(subgraph.id, curatorNSignal, expectedTokens.add(1)) - await expect(tx).revertedWith('Slippage protection') - }) }) describe('withdraw()', async function () { diff --git a/test/graphCurationToken.test.ts b/test/graphCurationToken.test.ts new file mode 100644 index 000000000..3ccc433e5 --- /dev/null +++ b/test/graphCurationToken.test.ts @@ -0,0 +1,215 @@ +import { expect } from 'chai' +import { constants, utils, BytesLike, BigNumber, Signature } from 'ethers' +import { eip712 } from '@graphprotocol/common-ts/dist/attestations' + +import { GraphToken } from '../build/types/GraphToken' +import { GraphCurationToken } from '../build/types/GraphCurationToken' + +import * as deployment from './lib/deployment' +import { getAccounts, getChainID, toBN, toGRT, Account } from './lib/testHelpers' + +const { AddressZero, MaxUint256 } = constants +const { keccak256, SigningKey } = utils + +describe('GraphCurationToken', () => { + let me: Account + let other: Account + let governor: Account + + let grt: GraphToken + let gcs: GraphCurationToken + + before(async function () { + ;[me, other, governor] = await getAccounts() + }) + + beforeEach(async function () { + // Deploy graph token and GCS + grt = await deployment.deployGRT(governor.signer) + gcs = await deployment.deployGCS(governor.signer) + await gcs.connect(governor.signer).initialize(governor.address) + + // Mint some tokens + const tokens = toGRT('10000') + await grt.connect(governor.signer).mint(me.address, tokens) + await grt.connect(governor.signer).mint(other.address, tokens) + }) + + describe('grtValueOf', async function () { + it('should return the linearly proportional amount of GRT, 1/10th', async function () { + const tokensToMint = toGRT('100') + const tokensToDeposit = toGRT('10') + + const tx = gcs.connect(governor.signer).mint(me.address, tokensToMint, tokensToDeposit) + await expect(tx).emit(gcs, 'Transfer').withArgs(AddressZero, me.address, tokensToMint) + + const tx1 = await gcs.connect(me.signer).grtValueOf(me.address, toGRT('33')) + const tx2 = await gcs.connect(me.signer).grtValueOf(me.address, toGRT('50')) + const tx3 = await gcs.connect(me.signer).grtValueOf(me.address, toGRT('99')) + const tx4 = await gcs.connect(me.signer).grtValueOf(me.address, toGRT('10')) + const tx5 = await gcs.connect(me.signer).grtValueOf(me.address, toGRT('0.0001')) + const tx6 = await gcs.connect(me.signer).grtValueOf(me.address, toGRT('0.0123323231')) + + await expect(tx1).eq(toGRT('3.3')) + await expect(tx2).eq(toGRT('5')) + await expect(tx3).eq(toGRT('9.9')) + await expect(tx4).eq(toGRT('1')) + await expect(tx5).eq(toGRT('0.00001')) + await expect(tx6).eq(toGRT('0.00123323231')) + }) + + it('should return the linearly proportional amount of GRT, 1/3rd', async function () { + const tokensToMint = toGRT('30') + const tokensToDeposit = toGRT('10') + + const tx = gcs.connect(governor.signer).mint(me.address, tokensToMint, tokensToDeposit) + await expect(tx).emit(gcs, 'Transfer').withArgs(AddressZero, me.address, tokensToMint) + + const tx1 = await gcs.connect(me.signer).grtValueOf(me.address, toGRT('10')) + const tx2 = await gcs.connect(me.signer).grtValueOf(me.address, toGRT('5')) + const tx3 = await gcs.connect(me.signer).grtValueOf(me.address, toGRT('15')) + const tx4 = await gcs.connect(me.signer).grtValueOf(me.address, toGRT('1')) + const tx5 = await gcs.connect(me.signer).grtValueOf(me.address, toGRT('0.0001')) + const tx6 = await gcs.connect(me.signer).grtValueOf(me.address, toGRT('0.0123323231')) + + await expect(tx1).eq(toGRT('3.333333333333333333')) + await expect(tx2).eq(toGRT('1.666666666666666666')) + await expect(tx3).eq(toGRT('5')) + await expect(tx4).eq(toGRT('0.333333333333333333')) + await expect(tx5).eq(toGRT('0.000033333333333333')) + await expect(tx6).eq(toGRT('0.004110774366666666')) + }) + }) + + describe('mint', async function () { + context('if NOT governor', function () { + it('should revert on mint', async function () { + const tokensToMint = toGRT('100') + const tokensToDeposit = toGRT('10') + const tx = gcs.connect(me.signer).mint(me.address, tokensToMint, tokensToDeposit) + await expect(tx).revertedWith('Only Governor can call') + }) + }) + + context('if governor', function () { + it('should mint saving the correct deposit', async function () { + const tokensToMint = toGRT('100') + const tokensToDeposit = toGRT('10') + + const beforeTokens = await gcs.balanceOf(me.address) + const beforeDeposits = await gcs.deposits(me.address) + + const tx = gcs.connect(governor.signer).mint(me.address, tokensToMint, tokensToDeposit) + await expect(tx).emit(gcs, 'Transfer').withArgs(AddressZero, me.address, tokensToMint) + + const afterTokens = await gcs.balanceOf(me.address) + const afterDeposits = await gcs.deposits(me.address) + expect(afterTokens).eq(beforeTokens.add(tokensToMint)) + expect(afterDeposits).eq(beforeDeposits.add(tokensToDeposit)) + }) + }) + }) + + describe('burn', async function () { + const tokensPreMinted = toGRT('100') + const tokensPreDeposited = toGRT('10') + + beforeEach(async function () { + await gcs.connect(governor.signer).mint(me.address, tokensPreMinted, tokensPreDeposited) + }) + + context('if NOT governor', function () { + it('should revert on burn', async function () { + const tokensToBurn = toGRT('100') + const tx = gcs.connect(me.signer).burnFrom(me.address, tokensToBurn) + await expect(tx).revertedWith('Only Governor can call') + }) + }) + + context('if governor', function () { + it('should burn fully removing all deposits', async function () { + const beforeTokens = await gcs.balanceOf(me.address) + const beforeDeposits = await gcs.deposits(me.address) + + const tx = gcs.connect(governor.signer).burnFrom(me.address, tokensPreMinted) + await expect(tx).emit(gcs, 'Transfer').withArgs(me.address, AddressZero, tokensPreMinted) + + const afterTokens = await gcs.balanceOf(me.address) + const afterDeposits = await gcs.deposits(me.address) + expect(afterTokens).eq(beforeTokens.sub(tokensPreMinted)) + expect(afterDeposits).eq(beforeDeposits.sub(tokensPreDeposited)) + }) + + it('should burn partially removing the proportional deposit', async function () { + const beforeTokens = await gcs.balanceOf(me.address) + const beforeDeposits = await gcs.deposits(me.address) + + const tokensToBurn = toGRT('50') + const depositDeltaForBurning = await gcs.grtValueOf(me.address, tokensToBurn) + + const tx = gcs.connect(governor.signer).burnFrom(me.address, tokensToBurn) + await expect(tx).emit(gcs, 'Transfer').withArgs(me.address, AddressZero, tokensToBurn) + + const afterTokens = await gcs.balanceOf(me.address) + const afterDeposits = await gcs.deposits(me.address) + expect(afterTokens).eq(beforeTokens.sub(tokensToBurn)) + expect(afterDeposits).eq(beforeDeposits.sub(depositDeltaForBurning)) + expect(afterDeposits).gt(0) + }) + }) + }) + + describe('transfer', async function () { + const tokensPreMinted = toGRT('100') + const tokensPreDeposited = toGRT('10') + + beforeEach(async function () { + await gcs.connect(governor.signer).mint(me.address, tokensPreMinted, tokensPreDeposited) + await gcs.connect(governor.signer).mint(other.address, tokensPreMinted, tokensPreDeposited) + }) + + it('should transfer fully correctly also moving the deposit', async function () { + const beforeTokensMe = await gcs.balanceOf(me.address) + const beforeDepositsMe = await gcs.deposits(me.address) + const beforeTokensOther = await gcs.balanceOf(other.address) + const beforeDepositsOther = await gcs.deposits(other.address) + + const tx = gcs.connect(me.signer).transfer(other.address, tokensPreMinted) + await expect(tx).emit(gcs, 'Transfer').withArgs(me.address, other.address, tokensPreMinted) + + const afterTokensMe = await gcs.balanceOf(me.address) + const afterDepositsMe = await gcs.deposits(me.address) + const afterTokensOther = await gcs.balanceOf(other.address) + const afterDepositsOther = await gcs.deposits(other.address) + + expect(afterTokensMe).eq(beforeTokensMe.sub(tokensPreMinted)) + expect(afterTokensOther).eq(beforeTokensOther.add(tokensPreMinted)) + + expect(afterDepositsMe).eq(beforeDepositsMe.sub(tokensPreDeposited)) + expect(afterDepositsOther).eq(beforeDepositsOther.add(tokensPreDeposited)) + }) + + it('should transfer partially moving the proportional deposit', async function () { + const beforeTokensMe = await gcs.balanceOf(me.address) + const beforeDepositsMe = await gcs.deposits(me.address) + const beforeTokensOther = await gcs.balanceOf(other.address) + const beforeDepositsOther = await gcs.deposits(other.address) + + const tokensToTransfer = toGRT('50') + const depositDeltaForTransfer = await gcs.grtValueOf(me.address, tokensToTransfer) + const tx = gcs.connect(me.signer).transfer(other.address, tokensToTransfer) + await expect(tx).emit(gcs, 'Transfer').withArgs(me.address, other.address, tokensToTransfer) + + const afterTokensMe = await gcs.balanceOf(me.address) + const afterDepositsMe = await gcs.deposits(me.address) + const afterTokensOther = await gcs.balanceOf(other.address) + const afterDepositsOther = await gcs.deposits(other.address) + + expect(afterTokensMe).eq(beforeTokensMe.sub(tokensToTransfer)) + expect(afterTokensOther).eq(beforeTokensOther.add(tokensToTransfer)) + + expect(afterDepositsMe).eq(beforeDepositsMe.sub(depositDeltaForTransfer)) + expect(afterDepositsOther).eq(beforeDepositsOther.add(depositDeltaForTransfer)) + }) + }) +}) diff --git a/test/lib/deployment.ts b/test/lib/deployment.ts index 9dac30b7d..686ebf04e 100644 --- a/test/lib/deployment.ts +++ b/test/lib/deployment.ts @@ -13,6 +13,7 @@ import { DisputeManager } from '../../build/types/DisputeManager' import { EpochManager } from '../../build/types/EpochManager' import { GNS } from '../../build/types/GNS' import { GraphToken } from '../../build/types/GraphToken' +import { GraphCurationToken } from '../../build/types/GraphCurationToken' import { ServiceRegistry } from '../../build/types/ServiceRegistry' import { Staking } from '../../build/types/Staking' import { RewardsManager } from '../../build/types/RewardsManager' @@ -98,6 +99,10 @@ export async function deployGRT(deployer: Signer): Promise { ) as unknown as Promise } +export async function deployGCS(deployer: Signer): Promise { + return deployContract('GraphCurationToken', deployer) as unknown as Promise +} + export async function deployGDAI(deployer: Signer): Promise { return deployContract('GDAI', deployer) as unknown as Promise } diff --git a/test/rewards/rewards.test.ts b/test/rewards/rewards.test.ts index ff3b9a240..c4379b0bb 100644 --- a/test/rewards/rewards.test.ts +++ b/test/rewards/rewards.test.ts @@ -767,7 +767,7 @@ describe('Rewards', () => { // Remove all signal from the subgraph const curatorShares = await curation.getCuratorSignal(curator1.address, subgraphDeploymentID1) - await curation.connect(curator1.signer).burn(subgraphDeploymentID1, curatorShares, 0) + await curation.connect(curator1.signer).burn(subgraphDeploymentID1, curatorShares) // Close allocation. At this point rewards should be collected for that indexer await staking.connect(indexer1.signer).closeAllocation(allocationID, randomHexBytes())