From df2697522c49d38bc95a11693c223733a1b59879 Mon Sep 17 00:00:00 2001 From: prego Date: Sat, 2 Dec 2023 23:33:07 +0100 Subject: [PATCH 1/2] fix issue with transfer events wrongly updating delegate values --- packages/nouns-subgraph/src/nouns-erc-721.ts | 6 +- .../tests/nouns-erc-721.test.ts | 198 ++++++++++++++++++ packages/nouns-subgraph/tests/utils.ts | 91 ++++++++ 3 files changed, 292 insertions(+), 3 deletions(-) create mode 100644 packages/nouns-subgraph/tests/nouns-erc-721.test.ts diff --git a/packages/nouns-subgraph/src/nouns-erc-721.ts b/packages/nouns-subgraph/src/nouns-erc-721.ts index 951688e12a..e383078d02 100644 --- a/packages/nouns-subgraph/src/nouns-erc-721.ts +++ b/packages/nouns-subgraph/src/nouns-erc-721.ts @@ -143,8 +143,6 @@ export function handleTransfer(event: Transfer): void { if (fromHolder.tokenBalanceRaw == BIGINT_ZERO && fromHolderPreviousBalance > BIGINT_ZERO) { governance.currentTokenHolders = governance.currentTokenHolders.minus(BIGINT_ONE); governance.save(); - - fromHolder.delegate = null; } else if ( fromHolder.tokenBalanceRaw > BIGINT_ZERO && fromHolderPreviousBalance == BIGINT_ZERO @@ -198,7 +196,9 @@ export function handleTransfer(event: Transfer): void { governance.currentTokenHolders = governance.currentTokenHolders.plus(BIGINT_ONE); governance.save(); - toHolder.delegate = toHolder.id; + if (!toHolder.delegate) { + toHolder.delegate = toHolder.id; + } } let noun = Noun.load(transferredNounId); diff --git a/packages/nouns-subgraph/tests/nouns-erc-721.test.ts b/packages/nouns-subgraph/tests/nouns-erc-721.test.ts new file mode 100644 index 0000000000..78ad8f3f9f --- /dev/null +++ b/packages/nouns-subgraph/tests/nouns-erc-721.test.ts @@ -0,0 +1,198 @@ +import { + assert, + clearStore, + test, + describe, + beforeAll, + afterAll, + afterEach, + beforeEach, +} from 'matchstick-as/assembly/index'; +import { Address, BigInt, Bytes } from '@graphprotocol/graph-ts'; +import { + createTransferEvent, + createDelegateChangedEvent, + createDelegateVotesChangedEvent, +} from './utils'; +import { BIGINT_ONE, BIGINT_ZERO } from '../src/utils/constants'; +import { getOrCreateDelegate, getOrCreateAccount } from '../src/utils/helpers'; +import { + handleDelegateChanged, + handleDelegateVotesChanged, + handleTransfer, +} from '../src/nouns-erc-721'; + +const someAddress = Address.fromString('0x0000000000000000000000000000000000000001'); +const popularDelegate = Address.fromString('0x0000000000000000000000000000000000000002'); +const BLACKHOLE_ADDRESS = Address.fromString('0x0000000000000000000000000000000000000000'); + +const txHash = Bytes.fromI32(11); +const logIndex = BigInt.fromI32(3); +const updateBlockTimestamp = BigInt.fromI32(946684800); +const updateBlockNumber = BigInt.fromI32(15537394); + +afterEach(() => { + clearStore(); +}); + +describe('nouns-erc-721', () => { + describe('Delegate changes', () => { + beforeEach(() => { + const delegate = getOrCreateDelegate(someAddress.toHexString()); + delegate.tokenHoldersRepresentedAmount = 1; + delegate.delegatedVotes = BIGINT_ONE; + delegate.delegatedVotesRaw = BIGINT_ONE; + delegate.save(); + + const account = getOrCreateAccount(someAddress.toHexString()); + account.tokenBalance = BIGINT_ONE; + account.tokenBalanceRaw = BIGINT_ONE; + account.nouns = ['1']; + account.save(); + }); + + afterAll(() => { + clearStore(); + }); + + test('after having delegated votes, transferring all nouns avoids nulling account delegate', () => { + const delegateChangeEvent = createDelegateChangedEvent( + txHash, + logIndex, + updateBlockTimestamp, + updateBlockNumber, + someAddress, + someAddress, + popularDelegate, + ); + + handleDelegateChanged(delegateChangeEvent); + + assert.fieldEquals( + 'Account', + someAddress.toHexString(), + 'delegate', + popularDelegate.toHexString(), + ); + assert.fieldEquals('Delegate', popularDelegate.toHexString(), 'nounsRepresented', '[1]'); + + const randomDelegateIncreateVoteEvent = createDelegateVotesChangedEvent( + txHash, + logIndex, + updateBlockTimestamp, + updateBlockNumber, + popularDelegate, + BIGINT_ZERO, + BIGINT_ONE, + ); + + const accountDelegateDecreaseVoteEvent = createDelegateVotesChangedEvent( + txHash, + logIndex, + updateBlockTimestamp, + updateBlockNumber, + someAddress, + BIGINT_ONE, + BIGINT_ZERO, + ); + + handleDelegateVotesChanged(randomDelegateIncreateVoteEvent); + handleDelegateVotesChanged(accountDelegateDecreaseVoteEvent); + + assert.fieldEquals('Delegate', popularDelegate.toHexString(), 'delegatedVotes', '1'); + assert.fieldEquals('Delegate', someAddress.toHexString(), 'delegatedVotes', '0'); + + const transferEvent = createTransferEvent( + txHash, + logIndex, + updateBlockTimestamp, + updateBlockNumber, + someAddress, + BLACKHOLE_ADDRESS, + BIGINT_ONE, + ); + + const randomDelegateDecreateVoteEvent = createDelegateVotesChangedEvent( + txHash, + logIndex, + updateBlockTimestamp, + updateBlockNumber, + popularDelegate, + BIGINT_ONE, + BIGINT_ZERO, + ); + + handleTransfer(transferEvent); + handleDelegateVotesChanged(randomDelegateDecreateVoteEvent); + + assert.fieldEquals('Account', someAddress.toHexString(), 'tokenBalance', '0'); + assert.fieldEquals( + 'Account', + someAddress.toHexString(), + 'delegate', + popularDelegate.toHexString(), + ); + assert.fieldEquals('Delegate', popularDelegate.toHexString(), 'nounsRepresented', '[]'); + assert.fieldEquals('Delegate', popularDelegate.toHexString(), 'delegatedVotes', '0'); + }); + + test('transfer events ignore delegate changes', () => { + const delegateChangeEvent = createDelegateChangedEvent( + txHash, + logIndex, + updateBlockTimestamp, + updateBlockNumber, + someAddress, + someAddress, + popularDelegate, + ); + + handleDelegateChanged(delegateChangeEvent); + + assert.fieldEquals( + 'Account', + someAddress.toHexString(), + 'delegate', + popularDelegate.toHexString(), + ); + + const selloutEvent = createTransferEvent( + txHash, + logIndex, + updateBlockTimestamp, + updateBlockNumber, + someAddress, + BLACKHOLE_ADDRESS, + BIGINT_ONE, + ); + + handleTransfer(selloutEvent); + + assert.fieldEquals( + 'Account', + someAddress.toHexString(), + 'delegate', + popularDelegate.toHexString(), + ); + + const buybackEvent = createTransferEvent( + txHash, + logIndex, + updateBlockTimestamp, + updateBlockNumber, + BLACKHOLE_ADDRESS, + someAddress, + BIGINT_ONE, + ); + + handleTransfer(buybackEvent); + + assert.fieldEquals( + 'Account', + someAddress.toHexString(), + 'delegate', + popularDelegate.toHexString(), + ); + }); + }); +}); diff --git a/packages/nouns-subgraph/tests/utils.ts b/packages/nouns-subgraph/tests/utils.ts index 7f992aa349..a230e6f2a5 100644 --- a/packages/nouns-subgraph/tests/utils.ts +++ b/packages/nouns-subgraph/tests/utils.ts @@ -25,6 +25,11 @@ import { import { Address, ethereum, Bytes, BigInt, ByteArray } from '@graphprotocol/graph-ts'; import { BIGINT_ONE, BIGINT_ZERO } from '../src/utils/constants'; import { ProposalCandidateCreated, SignatureAdded } from '../src/types/NounsDAOData/NounsDAOData'; +import { + DelegateChanged, + DelegateVotesChanged, + Transfer, +} from '../src/types/NounsToken/NounsToken'; export function createProposalCreatedWithRequirementsEventV3( input: ProposalCreatedWithRequirementsEvent, @@ -661,3 +666,89 @@ export function createProposalQueuedEvent( return newEvent; } + +export function createTransferEvent( + txHash: Bytes, + logIndex: BigInt, + blockTimestamp: BigInt, + blockNumber: BigInt, + from: Address, + to: Address, + tokenId: BigInt, +): Transfer { + let newEvent = changetype(newMockEvent()); + + newEvent.transaction.hash = txHash; + newEvent.logIndex = logIndex; + newEvent.block.timestamp = blockTimestamp; + newEvent.block.number = blockNumber; + + newEvent.parameters = new Array(); + newEvent.parameters.push(new ethereum.EventParam('from', ethereum.Value.fromAddress(from))); + newEvent.parameters.push(new ethereum.EventParam('to', ethereum.Value.fromAddress(to))); + newEvent.parameters.push( + new ethereum.EventParam('tokenId', ethereum.Value.fromUnsignedBigInt(tokenId)), + ); + + return newEvent; +} + +export function createDelegateChangedEvent( + txHash: Bytes, + logIndex: BigInt, + blockTimestamp: BigInt, + blockNumber: BigInt, + delegator: Address, + previousDelegate: Address, + newDelegate: Address, +): DelegateChanged { + let newEvent = changetype(newMockEvent()); + + newEvent.transaction.hash = txHash; + newEvent.logIndex = logIndex; + newEvent.block.timestamp = blockTimestamp; + newEvent.block.number = blockNumber; + + newEvent.parameters = new Array(); + newEvent.parameters.push( + new ethereum.EventParam('delegator', ethereum.Value.fromAddress(delegator)), + ); + newEvent.parameters.push( + new ethereum.EventParam('previousDelegate', ethereum.Value.fromAddress(previousDelegate)), + ); + newEvent.parameters.push( + new ethereum.EventParam('newDelegate', ethereum.Value.fromAddress(newDelegate)), + ); + + return newEvent; +} + +export function createDelegateVotesChangedEvent( + txHash: Bytes, + logIndex: BigInt, + blockTimestamp: BigInt, + blockNumber: BigInt, + delegate: Address, + previousBalance: BigInt, + newBalance: BigInt, +): DelegateVotesChanged { + let newEvent = changetype(newMockEvent()); + + newEvent.transaction.hash = txHash; + newEvent.logIndex = logIndex; + newEvent.block.timestamp = blockTimestamp; + newEvent.block.number = blockNumber; + + newEvent.parameters = new Array(); + newEvent.parameters.push( + new ethereum.EventParam('delegate', ethereum.Value.fromAddress(delegate)), + ); + newEvent.parameters.push( + new ethereum.EventParam('previousBalance', ethereum.Value.fromUnsignedBigInt(previousBalance)), + ); + newEvent.parameters.push( + new ethereum.EventParam('newBalance', ethereum.Value.fromUnsignedBigInt(newBalance)), + ); + + return newEvent; +} From 3235dc1cbf65f6c00c828ae72ba3bc39825a0244 Mon Sep 17 00:00:00 2001 From: prego Date: Sun, 3 Dec 2023 09:59:05 +0100 Subject: [PATCH 2/2] addtl test for new token holders delegation --- .../tests/nouns-erc-721.test.ts | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/packages/nouns-subgraph/tests/nouns-erc-721.test.ts b/packages/nouns-subgraph/tests/nouns-erc-721.test.ts index 78ad8f3f9f..7baad01a46 100644 --- a/packages/nouns-subgraph/tests/nouns-erc-721.test.ts +++ b/packages/nouns-subgraph/tests/nouns-erc-721.test.ts @@ -23,7 +23,8 @@ import { } from '../src/nouns-erc-721'; const someAddress = Address.fromString('0x0000000000000000000000000000000000000001'); -const popularDelegate = Address.fromString('0x0000000000000000000000000000000000000002'); +const freshHolderAddress = Address.fromString('0x0000000000000000000000000000000000000002'); +const popularDelegate = Address.fromString('0x0000000000000000000000000000000000000003'); const BLACKHOLE_ADDRESS = Address.fromString('0x0000000000000000000000000000000000000000'); const txHash = Bytes.fromI32(11); @@ -136,7 +137,7 @@ describe('nouns-erc-721', () => { assert.fieldEquals('Delegate', popularDelegate.toHexString(), 'delegatedVotes', '0'); }); - test('transfer events ignore delegate changes', () => { + test('transfer events from account with delegation ignore delegate changes', () => { const delegateChangeEvent = createDelegateChangedEvent( txHash, logIndex, @@ -194,5 +195,31 @@ describe('nouns-erc-721', () => { popularDelegate.toHexString(), ); }); + + test('fresh new token holder self-delegates by default', () => { + const account = getOrCreateAccount(freshHolderAddress.toHexString()); + account.save(); + + const buyNounEvent = createTransferEvent( + txHash, + logIndex, + updateBlockTimestamp, + updateBlockNumber, + BLACKHOLE_ADDRESS, + freshHolderAddress, + BIGINT_ONE, + ); + + handleTransfer(buyNounEvent); + + assert.fieldEquals('Account', freshHolderAddress.toHexString(), 'tokenBalance', '1'); + + assert.fieldEquals( + 'Account', + freshHolderAddress.toHexString(), + 'delegate', + freshHolderAddress.toHexString(), + ); + }); }); });