Skip to content

Commit 639f9cd

Browse files
authored
Merge pull request #568 from graphprotocol/tmigone/gns-transfer
feat: make GNS signal transferable
2 parents e1bd11f + 09f40d1 commit 639f9cd

File tree

3 files changed

+120
-6
lines changed

3 files changed

+120
-6
lines changed

contracts/discovery/GNS.sol

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,16 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall {
8686
uint256 tokensReceived
8787
);
8888

89+
/**
90+
* @dev Emitted when a curator transfers signal.
91+
*/
92+
event SignalTransferred(
93+
uint256 indexed subgraphID,
94+
address indexed from,
95+
address indexed to,
96+
uint256 nSignalTransferred
97+
);
98+
8999
/**
90100
* @dev Emitted when a subgraph is created.
91101
*/
@@ -202,10 +212,9 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall {
202212
* @param _subgraphNFT Address of the ERC721 contract
203213
*/
204214
function _setSubgraphNFT(address _subgraphNFT) private {
205-
require(
206-
_subgraphNFT != address(0) && Address.isContract(_subgraphNFT),
207-
"NFT must be valid"
208-
);
215+
require(_subgraphNFT != address(0), "NFT address cant be zero");
216+
require(Address.isContract(_subgraphNFT), "NFT must be valid");
217+
209218
subgraphNFT = ISubgraphNFT(_subgraphNFT);
210219
emit SubgraphNFTUpdated(_subgraphNFT);
211220
}
@@ -455,6 +464,36 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall {
455464
emit SignalBurned(_subgraphID, curator, _nSignal, vSignal, tokens);
456465
}
457466

467+
/**
468+
* @dev Move subgraph signal from sender to `_recipient`
469+
* @param _subgraphID Subgraph ID
470+
* @param _recipient Address to send the signal to
471+
* @param _amount The amount of nSignal to transfer
472+
*/
473+
function transferSignal(
474+
uint256 _subgraphID,
475+
address _recipient,
476+
uint256 _amount
477+
) external override notPartialPaused {
478+
require(_recipient != address(0), "GNS: Curator cannot transfer to the zero address");
479+
480+
// Subgraph checks
481+
SubgraphData storage subgraphData = _getSubgraphOrRevert(_subgraphID);
482+
483+
// Balance checks
484+
address curator = msg.sender;
485+
uint256 curatorBalance = subgraphData.curatorNSignal[curator];
486+
require(curatorBalance >= _amount, "GNS: Curator transfer amount exceeds balance");
487+
488+
// Move the signal
489+
subgraphData.curatorNSignal[curator] = subgraphData.curatorNSignal[curator].sub(_amount);
490+
subgraphData.curatorNSignal[_recipient] = subgraphData.curatorNSignal[_recipient].add(
491+
_amount
492+
);
493+
494+
emit SignalTransferred(_subgraphID, curator, _recipient, _amount);
495+
}
496+
458497
/**
459498
* @dev Withdraw tokens from a deprecated subgraph.
460499
* When the subgraph is deprecated, any curator can call this function and

contracts/discovery/IGNS.sol

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ interface IGNS {
6565
uint256 _tokensOutMin
6666
) external;
6767

68+
function transferSignal(
69+
uint256 _subgraphID,
70+
address _recipient,
71+
uint256 _amount
72+
) external;
73+
6874
function withdraw(uint256 _subgraphID) external;
6975

7076
// -- Getters --

test/gns.test.ts

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const buildSubgraphID = (account: string, seqID: BigNumber): string =>
4848
describe('GNS', () => {
4949
let me: Account
5050
let other: Account
51+
let another: Account
5152
let governor: Account
5253

5354
let fixture: NetworkFixture
@@ -444,6 +445,34 @@ describe('GNS', () => {
444445
return tx
445446
}
446447

448+
const transferSignal = async (
449+
subgraphID: string,
450+
owner: Account,
451+
recipient: Account,
452+
amount: BigNumber,
453+
): Promise<ContractTransaction> => {
454+
// Before state
455+
const beforeOwnerNSignal = await gns.getCuratorSignal(subgraphID, owner.address)
456+
const beforeRecipientNSignal = await gns.getCuratorSignal(subgraphID, recipient.address)
457+
458+
// Transfer
459+
const tx = gns.connect(owner.signer).transferSignal(subgraphID, recipient.address, amount)
460+
461+
await expect(tx)
462+
.emit(gns, 'SignalTransferred')
463+
.withArgs(subgraphID, owner.address, recipient.address, amount)
464+
465+
// After state
466+
const afterOwnerNSignal = await gns.getCuratorSignal(subgraphID, owner.address)
467+
const afterRecipientNSignal = await gns.getCuratorSignal(subgraphID, recipient.address)
468+
469+
// Check state
470+
expect(afterOwnerNSignal).eq(beforeOwnerNSignal.sub(amount))
471+
expect(afterRecipientNSignal).eq(beforeRecipientNSignal.add(amount))
472+
473+
return tx
474+
}
475+
447476
const withdraw = async (account: Account, subgraphID: string): Promise<ContractTransaction> => {
448477
// Before state
449478
const beforeCuratorNSignal = await gns.getCuratorSignal(subgraphID, account.address)
@@ -475,7 +504,7 @@ describe('GNS', () => {
475504
}
476505

477506
before(async function () {
478-
;[me, other, governor] = await getAccounts()
507+
;[me, other, governor, another] = await getAccounts()
479508
fixture = new NetworkFixture()
480509
;({ grt, curation, gns } = await fixture.load(governor.signer))
481510
newSubgraph0 = buildSubgraph()
@@ -531,7 +560,7 @@ describe('GNS', () => {
531560

532561
it('revert set to empty address', async function () {
533562
const tx = gns.connect(governor.signer).setSubgraphNFT(AddressZero)
534-
await expect(tx).revertedWith('NFT must be valid')
563+
await expect(tx).revertedWith('NFT address cant be zero')
535564
})
536565

537566
it('revert set to non-contract', async function () {
@@ -824,6 +853,46 @@ describe('GNS', () => {
824853
})
825854
})
826855

856+
describe('transferSignal()', async function () {
857+
let subgraph: Subgraph
858+
let otherNSignal: BigNumber
859+
860+
beforeEach(async () => {
861+
subgraph = await publishNewSubgraph(me, newSubgraph0)
862+
await mintSignal(other, subgraph.id, tokens10000)
863+
otherNSignal = await gns.getCuratorSignal(subgraph.id, other.address)
864+
})
865+
866+
it('should transfer signal from one curator to another', async function () {
867+
await transferSignal(subgraph.id, other, another, otherNSignal)
868+
})
869+
it('should fail when transfering to zero address', async function () {
870+
const tx = gns
871+
.connect(other.signer)
872+
.transferSignal(subgraph.id, ethers.constants.AddressZero, otherNSignal)
873+
await expect(tx).revertedWith('GNS: Curator cannot transfer to the zero address')
874+
})
875+
it('should fail when name signal is disabled', async function () {
876+
await deprecateSubgraph(me, subgraph.id)
877+
const tx = gns
878+
.connect(other.signer)
879+
.transferSignal(subgraph.id, another.address, otherNSignal)
880+
await expect(tx).revertedWith('GNS: Must be active')
881+
})
882+
it('should fail if you try to transfer on a non existing name', async function () {
883+
const subgraphID = randomHexBytes(32)
884+
const tx = gns
885+
.connect(other.signer)
886+
.transferSignal(subgraphID, another.address, otherNSignal)
887+
await expect(tx).revertedWith('GNS: Must be active')
888+
})
889+
it('should fail when the curator tries to transfer more signal than they have', async function () {
890+
const tx = gns
891+
.connect(other.signer)
892+
.transferSignal(subgraph.id, another.address, otherNSignal.add(otherNSignal))
893+
await expect(tx).revertedWith('GNS: Curator transfer amount exceeds balance')
894+
})
895+
})
827896
describe('withdraw()', async function () {
828897
let subgraph: Subgraph
829898

0 commit comments

Comments
 (0)