diff --git a/contracts/discovery/GNS.sol b/contracts/discovery/GNS.sol index 3f0e16f6d..fc8802df2 100644 --- a/contracts/discovery/GNS.sol +++ b/contracts/discovery/GNS.sol @@ -86,6 +86,16 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { uint256 tokensReceived ); + /** + * @dev Emitted when a curator transfers signal. + */ + event SignalTransferred( + uint256 indexed subgraphID, + address indexed from, + address indexed to, + uint256 nSignalTransferred + ); + /** * @dev Emitted when a subgraph is created. */ @@ -202,10 +212,9 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { * @param _subgraphNFT Address of the ERC721 contract */ function _setSubgraphNFT(address _subgraphNFT) private { - require( - _subgraphNFT != address(0) && Address.isContract(_subgraphNFT), - "NFT must be valid" - ); + require(_subgraphNFT != address(0), "NFT address cant be zero"); + require(Address.isContract(_subgraphNFT), "NFT must be valid"); + subgraphNFT = ISubgraphNFT(_subgraphNFT); emit SubgraphNFTUpdated(_subgraphNFT); } @@ -455,6 +464,36 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { emit SignalBurned(_subgraphID, curator, _nSignal, vSignal, tokens); } + /** + * @dev Move subgraph signal from sender to `_recipient` + * @param _subgraphID Subgraph ID + * @param _recipient Address to send the signal to + * @param _amount The amount of nSignal to transfer + */ + function transferSignal( + uint256 _subgraphID, + address _recipient, + uint256 _amount + ) external override notPartialPaused { + require(_recipient != address(0), "GNS: Curator cannot transfer to the zero address"); + + // Subgraph checks + SubgraphData storage subgraphData = _getSubgraphOrRevert(_subgraphID); + + // Balance checks + address curator = msg.sender; + uint256 curatorBalance = subgraphData.curatorNSignal[curator]; + require(curatorBalance >= _amount, "GNS: Curator transfer amount exceeds balance"); + + // Move the signal + subgraphData.curatorNSignal[curator] = subgraphData.curatorNSignal[curator].sub(_amount); + subgraphData.curatorNSignal[_recipient] = subgraphData.curatorNSignal[_recipient].add( + _amount + ); + + emit SignalTransferred(_subgraphID, curator, _recipient, _amount); + } + /** * @dev Withdraw tokens from a deprecated subgraph. * When the subgraph is deprecated, any curator can call this function and diff --git a/contracts/discovery/IGNS.sol b/contracts/discovery/IGNS.sol index 92300627e..13efa1b9d 100644 --- a/contracts/discovery/IGNS.sol +++ b/contracts/discovery/IGNS.sol @@ -65,6 +65,12 @@ interface IGNS { uint256 _tokensOutMin ) external; + function transferSignal( + uint256 _subgraphID, + address _recipient, + uint256 _amount + ) external; + function withdraw(uint256 _subgraphID) external; // -- Getters -- diff --git a/test/gns.test.ts b/test/gns.test.ts index 6494e665b..298d95ab0 100644 --- a/test/gns.test.ts +++ b/test/gns.test.ts @@ -48,6 +48,7 @@ const buildSubgraphID = (account: string, seqID: BigNumber): string => describe('GNS', () => { let me: Account let other: Account + let another: Account let governor: Account let fixture: NetworkFixture @@ -444,6 +445,34 @@ describe('GNS', () => { return tx } + const transferSignal = async ( + subgraphID: string, + owner: Account, + recipient: Account, + amount: BigNumber, + ): Promise => { + // Before state + const beforeOwnerNSignal = await gns.getCuratorSignal(subgraphID, owner.address) + const beforeRecipientNSignal = await gns.getCuratorSignal(subgraphID, recipient.address) + + // Transfer + const tx = gns.connect(owner.signer).transferSignal(subgraphID, recipient.address, amount) + + await expect(tx) + .emit(gns, 'SignalTransferred') + .withArgs(subgraphID, owner.address, recipient.address, amount) + + // After state + const afterOwnerNSignal = await gns.getCuratorSignal(subgraphID, owner.address) + const afterRecipientNSignal = await gns.getCuratorSignal(subgraphID, recipient.address) + + // Check state + expect(afterOwnerNSignal).eq(beforeOwnerNSignal.sub(amount)) + expect(afterRecipientNSignal).eq(beforeRecipientNSignal.add(amount)) + + return tx + } + const withdraw = async (account: Account, subgraphID: string): Promise => { // Before state const beforeCuratorNSignal = await gns.getCuratorSignal(subgraphID, account.address) @@ -475,7 +504,7 @@ describe('GNS', () => { } before(async function () { - ;[me, other, governor] = await getAccounts() + ;[me, other, governor, another] = await getAccounts() fixture = new NetworkFixture() ;({ grt, curation, gns } = await fixture.load(governor.signer)) newSubgraph0 = buildSubgraph() @@ -531,7 +560,7 @@ describe('GNS', () => { it('revert set to empty address', async function () { const tx = gns.connect(governor.signer).setSubgraphNFT(AddressZero) - await expect(tx).revertedWith('NFT must be valid') + await expect(tx).revertedWith('NFT address cant be zero') }) it('revert set to non-contract', async function () { @@ -824,6 +853,46 @@ describe('GNS', () => { }) }) + describe('transferSignal()', async function () { + let subgraph: Subgraph + let otherNSignal: BigNumber + + beforeEach(async () => { + subgraph = await publishNewSubgraph(me, newSubgraph0) + await mintSignal(other, subgraph.id, tokens10000) + otherNSignal = await gns.getCuratorSignal(subgraph.id, other.address) + }) + + it('should transfer signal from one curator to another', async function () { + await transferSignal(subgraph.id, other, another, otherNSignal) + }) + it('should fail when transfering to zero address', async function () { + const tx = gns + .connect(other.signer) + .transferSignal(subgraph.id, ethers.constants.AddressZero, otherNSignal) + await expect(tx).revertedWith('GNS: Curator cannot transfer to the zero address') + }) + it('should fail when name signal is disabled', async function () { + await deprecateSubgraph(me, subgraph.id) + const tx = gns + .connect(other.signer) + .transferSignal(subgraph.id, another.address, otherNSignal) + await expect(tx).revertedWith('GNS: Must be active') + }) + it('should fail if you try to transfer on a non existing name', async function () { + const subgraphID = randomHexBytes(32) + const tx = gns + .connect(other.signer) + .transferSignal(subgraphID, another.address, otherNSignal) + await expect(tx).revertedWith('GNS: Must be active') + }) + it('should fail when the curator tries to transfer more signal than they have', async function () { + const tx = gns + .connect(other.signer) + .transferSignal(subgraph.id, another.address, otherNSignal.add(otherNSignal)) + await expect(tx).revertedWith('GNS: Curator transfer amount exceeds balance') + }) + }) describe('withdraw()', async function () { let subgraph: Subgraph