Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/negative subgraph balances main #548

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions packages/contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,14 @@
"@ensdomains/ens-contracts": "0.0.11",
"@nomicfoundation/hardhat-chai-matchers": "^1.0.5",
"@nomicfoundation/hardhat-verify": "^1.0.4",
"@nomiclabs/hardhat-ethers": "^2.2.1",
"@nomiclabs/hardhat-ethers": "2.2.1",
"@opengsn/contracts": "2.2.6",
"@openzeppelin/contracts": "4.8.1",
"@openzeppelin/contracts-upgradeable": "4.8.1",
"@openzeppelin/hardhat-upgrades": "^1.23.1",
"@rollup/plugin-json": "^4.1.0",
"@typechain/ethers-v5": "^7.2.0",
"@typechain/hardhat": "^2.3.1",
"@typechain/ethers-v5": "7.2.0",
"@typechain/hardhat": "2.3.1",
"@types/chai": "^4.2.22",
"@types/mocha": "^9.0.0",
"@types/node": "^16.11.7",
Expand All @@ -70,7 +70,7 @@
"eslint-plugin-promise": "^5.1.1",
"ethereumjs-util": "^7.1.4",
"ethers": "^5.5.1",
"hardhat": "^2.12.7",
"hardhat": "2.12.7",
"hardhat-deploy": "^0.9.26",
"hardhat-gas-reporter": "^1.0.4",
"ipfs-http-client": "51.0.0",
Expand All @@ -83,7 +83,7 @@
"solidity-docgen": "^0.6.0-beta.35",
"tmp-promise": "^3.0.3",
"ts-node": "^8.1.0",
"typechain": "^5.2.0",
"typechain": "5.2.0",
"typescript": "^4.4.4"
}
}
5 changes: 5 additions & 0 deletions packages/subgraph/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

### Changed

- Fixed bug with negative number balances and missing delegation history for existing ERC20 tokens using `TokenVoting`
- Fixed bugs regarding inconsistent memberIds in various parts of the codebase. This primarily affects delegation.

### Removed

## [1.3.0]
Expand Down
2 changes: 1 addition & 1 deletion packages/subgraph/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ type TokenVotingMember @entity {
plugin: TokenVotingPlugin!

# delegates
delegatee: TokenVotingMember!
delegatee: TokenVotingMember
votingPower: BigInt
# we assume token owners and/or delegatees are members
delegators: [TokenVotingMember!]! @derivedFrom(field: "delegatee")
Expand Down
148 changes: 106 additions & 42 deletions packages/subgraph/src/packages/token/governance-erc20.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,90 +4,154 @@ import {TokenVotingMember} from '../../../generated/schema';
import {Transfer} from '../../../generated/templates/TokenVoting/ERC20';
import {
DelegateChanged,
DelegateVotesChanged
DelegateVotesChanged,
GovernanceERC20 as GovernanceERC20Contract
} from '../../../generated/templates/GovernanceERC20/GovernanceERC20';
import {generateMemberEntityId} from '../../utils/ids';
import {
TokenVotingMemberResult,
getDelegateeId,
getERC20Balance,
getVotingPower
} from './utils';

function getOrCreateMember(
user: Address,
pluginId: string,
tokenAddress: Address
): TokenVotingMemberResult {
let memberEntityId = generateMemberEntityId(
Address.fromString(pluginId),
user
);
let createdNew = false;
let member = TokenVotingMember.load(memberEntityId);

function getOrCreateMember(user: Address, pluginId: string): TokenVotingMember {
let id = user
.toHexString()
.concat('_')
.concat(pluginId);
let member = TokenVotingMember.load(id);
if (!member) {
member = new TokenVotingMember(id);
createdNew = true;
member = new TokenVotingMember(memberEntityId);
member.address = user;
member.balance = BigInt.zero();
member.balance = getERC20Balance(user, tokenAddress);
member.plugin = pluginId;

member.delegatee = id; // we assume by default member delegates itself
member.votingPower = BigInt.zero();
member.delegatee = getDelegateeId(user, tokenAddress, pluginId);
member.votingPower = getVotingPower(user, tokenAddress);
}

return member;
return new TokenVotingMemberResult(member, createdNew);
}

export function handleTransfer(event: Transfer): void {
let context = dataSource.context();
let pluginId = context.getString('pluginId');
let tokenAddress = event.address;

if (event.params.from != Address.zero()) {
let fromMember = getOrCreateMember(event.params.from, pluginId);
fromMember.balance = fromMember.balance.minus(event.params.value);
let result = getOrCreateMember(event.params.from, pluginId, tokenAddress);
let fromMember = result.entity;

// in the case of an existing member, update the balance
if (!result.createdNew) {
fromMember.balance = fromMember.balance.minus(event.params.value);
}
fromMember.save();
}

if (event.params.to != Address.zero()) {
let toMember = getOrCreateMember(event.params.to, pluginId);
toMember.balance = toMember.balance.plus(event.params.value);
let result = getOrCreateMember(event.params.to, pluginId, tokenAddress);
let toMember = result.entity;

// in the case of an existing member, update the balance
if (!result.createdNew) {
toMember.balance = toMember.balance.plus(event.params.value);
}
toMember.save();
}
}

export function handleDelegateChanged(event: DelegateChanged): void {
let context = dataSource.context();
let pluginId = context.getString('pluginId');
let tokenAddress = event.address;
let toDelegate = event.params.toDelegate;

// make sure `fromDelegate` & `toDelegate`are members
if (event.params.fromDelegate != Address.zero()) {
let fromMember = getOrCreateMember(event.params.fromDelegate, pluginId);
fromMember.save();
}
if (event.params.toDelegate != Address.zero()) {
let toMember = getOrCreateMember(event.params.toDelegate, pluginId);
toMember.save();
let resultFromDelegate = getOrCreateMember(
event.params.fromDelegate,
pluginId,
tokenAddress
);
resultFromDelegate.entity.save();
}

// make sure `delegator` is member and set delegatee
if (event.params.delegator != Address.zero()) {
let delegator = getOrCreateMember(event.params.delegator, pluginId);
let resultDelegator = getOrCreateMember(
event.params.delegator,
pluginId,
tokenAddress
);
let delegator = resultDelegator.entity;

// set delegatee
let delegatee = event.params.toDelegate
.toHexString()
.concat('_')
.concat(pluginId);

delegator.delegatee = delegatee;

if (toDelegate != Address.zero()) {
const resultDelegatee = getOrCreateMember(
toDelegate,
pluginId,
tokenAddress
);
const delegatee = resultDelegatee.entity;
const delegateeId = generateMemberEntityId(
Address.fromString(pluginId),
Address.fromBytes(delegatee.address)
);
delegatee.save();
delegator.delegatee = delegateeId;
}
delegator.save();
}
}

export function handleDelegateVotesChanged(event: DelegateVotesChanged): void {
let context = dataSource.context();
let pluginId = context.getString('pluginId');
const delegate = event.params.delegate;
if (delegate == Address.zero()) return;
const newVotingPower = event.params.newBalance;

const context = dataSource.context();
const pluginId = context.getString('pluginId');
const tokenAddress = event.address;

let result = getOrCreateMember(delegate, pluginId, tokenAddress);
let member = result.entity;

if (event.params.delegate != Address.zero()) {
let member = getOrCreateMember(event.params.delegate, pluginId);
if (
member.balance.equals(BigInt.zero()) &&
event.params.newBalance.equals(BigInt.zero())
) {
if (isZeroBalanceAndVotingPower(member.balance, newVotingPower)) {
if (shouldRemoveMember(event.address, delegate)) {
store.remove('TokenVotingMember', member.id);
} else {
// Assign the cumulative delegated votes to this member from all their delegators.
member.votingPower = event.params.newBalance;
member.save();
return;
}
}
member.votingPower = newVotingPower;
member.save();
}

function isZeroBalanceAndVotingPower(
memberBalance: BigInt,
votingPower: BigInt
): boolean {
return (
memberBalance.equals(BigInt.zero()) && votingPower.equals(BigInt.zero())
);
}

function shouldRemoveMember(
contractAddress: Address,
delegate: Address
): boolean {
const governanceERC20Contract = GovernanceERC20Contract.bind(contractAddress);
const delegates = governanceERC20Contract.try_delegates(delegate);
if (!delegates.reverted) {
return delegates.value == delegate || delegates.value == Address.zero();
}
return false;
}
60 changes: 60 additions & 0 deletions packages/subgraph/src/packages/token/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {Address, BigInt} from '@graphprotocol/graph-ts';
import {TokenVotingMember} from '../../../generated/schema';
import {GovernanceERC20 as GovernanceERC20Contract} from '../../../generated/templates/GovernanceERC20/GovernanceERC20';
import {ADDRESS_ZERO} from '../../utils/constants';
import {generateMemberEntityId} from '../../utils/ids';

export function getERC20Balance(user: Address, tokenAddress: Address): BigInt {
let contract = GovernanceERC20Contract.bind(tokenAddress);
let balance = contract.balanceOf(user);
return balance;
}

export function getDelegation(
user: Address,
tokenAddress: Address
): string | null {
let contract = GovernanceERC20Contract.bind(tokenAddress);
let delegate = contract.delegates(user);

return delegate === Address.fromString(ADDRESS_ZERO)
? null
: delegate.toHexString();
}

export function getDelegateeId(
user: Address,
tokenAddress: Address,
pluginId: string
): string | null {
let delegatee = getDelegation(user, tokenAddress);
return delegatee
? generateMemberEntityId(
Address.fromString(pluginId),
Address.fromString(user.toHexString())
)
: null;
}

export function getVotingPower(user: Address, tokenAddress: Address): BigInt {
let contract = GovernanceERC20Contract.bind(tokenAddress);
let votingPower = contract.getVotes(user);
return votingPower;
}

/**
* A container for the result of the `getOrCreateMember` function.
* @param entity - The `TokenVotingMember` entity.
* @param createdNew - A boolean indicating whether the entity was created new
* or if false it was previously created. If the entity was created new, it already
* has the latest balance of the user, so no need to then update the balance.
*/
export class TokenVotingMemberResult {
entity: TokenVotingMember;
createdNew: boolean;

constructor(entity: TokenVotingMember, createNew: boolean) {
this.entity = entity;
this.createdNew = createNew;
}
}
16 changes: 16 additions & 0 deletions packages/subgraph/src/utils/ids.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {Address} from '@graphprotocol/graph-ts';

export function generateEntityIdFromAddress(address: Address): string {
return address.toHexString();
}

export function generatePluginEntityId(pluginAddress: Address): string {
return generateEntityIdFromAddress(pluginAddress);
}

export function generateMemberEntityId(
pluginAddress: Address,
memberAddress: Address
): string {
return [pluginAddress.toHexString(), memberAddress.toHexString()].join('_');
}
3 changes: 3 additions & 0 deletions packages/subgraph/tests/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@ export const ADDRESS_THREE = '0x0000000000000000000000000000000000000003';
export const ADDRESS_FOUR = '0x0000000000000000000000000000000000000004';
export const ADDRESS_FIVE = '0x0000000000000000000000000000000000000005';
export const ADDRESS_SIX = '0x0000000000000000000000000000000000000006';
export const ADDRESS_SEVEN = '0x0000000000000000000000000000000000000007';
export const DAO_ADDRESS = '0x00000000000000000000000000000000000000da';
export const CONTRACT_ADDRESS = '0x00000000000000000000000000000000000000Ad';
export const DAO_TOKEN_ADDRESS = '0x6B175474E89094C44Da98b954EedeAC495271d0F';
export const DEFAULT_MOCK_EVENT_ADDRESS =
'0xA16081F360e3847006dB660bae1c6d1b2e17eC2A';

export const ZERO = '0';
export const ONE = '1';
Expand Down
16 changes: 13 additions & 3 deletions packages/subgraph/tests/helpers/method-classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ import {
createNewProposalExecutedEvent,
createNewVoteCastEvent,
createNewVotingSettingsUpdatedEvent,
delegatesCall,
getProposalCountCall
} from '../token/utils';
import {
Expand All @@ -104,6 +105,7 @@ import {
createWrappedTokenCalls,
createERC1155TokenCalls
} from '../utils';
import {generateMemberEntityId} from '../../src/utils/ids';

/* eslint-disable @typescript-eslint/no-unused-vars */
// ERC1155Contract
Expand Down Expand Up @@ -692,9 +694,9 @@ class TokenVotingMemberMethods extends TokenVotingMember {
memberAddress: string = ADDRESS_ONE,
pluginAddress: string = CONTRACT_ADDRESS
): TokenVotingMemberMethods {
const plugin = Address.fromHexString(pluginAddress);
let id = memberAddress.concat('_').concat(plugin.toHexString());

const plugin = Address.fromBytes(Bytes.fromHexString(pluginAddress));
const member = Address.fromBytes(Bytes.fromHexString(memberAddress));
let id = generateMemberEntityId(plugin, member);
this.id = id;
this.address = Address.fromHexString(memberAddress);
this.balance = BigInt.zero();
Expand All @@ -705,6 +707,14 @@ class TokenVotingMemberMethods extends TokenVotingMember {
return this;
}

mockCall_delegatesCall(
tokenContractAddress: string,
account: string,
returns: string
): void {
delegatesCall(tokenContractAddress, account, returns);
}

createEvent_DelegateChanged(
delegator: string = this.address.toHexString(),
fromDelegate: string = ADDRESS_ONE,
Expand Down
Loading
Loading