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

Feature: Add isTokenGovernanceCompatible function #269

Merged
merged 25 commits into from
Aug 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion modules/client-common/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@aragon/sdk-client-common",
"author": "Aragon Association",
"version": "1.4.0-rc0",
"version": "1.5.0-rc0",
"license": "MIT",
"main": "dist/index.js",
"module": "dist/sdk-client-common.esm.js",
Expand Down
6 changes: 3 additions & 3 deletions modules/client-common/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ export const UNAVAILABLE_PROPOSAL_METADATA: ProposalMetadata = {
resources: [],
};

const getGraphqlNode = (netowrk: SupportedNetwork): string => {
const getGraphqlNode = (network: SupportedNetwork): string => {
return `https://subgraph.satsuma-prod.com/qHR2wGfc5RLi6/aragon/osx-${
SupportedNetworksToGraphqlNetworks[netowrk]
}/version/v1.2.2/api`;
SupportedNetworksToGraphqlNetworks[network]
}/version/v1.3.0/api`;
};

export const GRAPHQL_NODES: { [K in SupportedNetwork]: { url: string }[] } = {
Expand Down
5 changes: 3 additions & 2 deletions modules/client/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ TEMPLATE:
- Block param on `getVotingSettings` and `getMembers` functions to allow for historical data
- Support for local chains
- Support for ERC1155 deposits and withdrawals
## 1.12.0-rc1
- `isTokenVotingCompatibleToken` function
## [1.12.0-rc1]
### Added
- Support for baseMainnet network
## 1.11.0-rc1
## [1.11.0-rc1]
### Added
- Added `initializeFrom` encoders and decoders
- Support for ERC721 deposits and withdrawals
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/* MARKDOWN
---
title: Is TokenVoting compatible token
---

## Check if a token is compatible with the TokenVoting Plugin as an underlying token

Check if a token is compatible with the TokenVoting Plugin as an underlying token. This means that the token is ERC20 and ERC165 compliant and has the required methods for the TokenVoting Plugin to work.
*/

import { TokenVotingClient } from "@aragon/sdk-client";
import { context } from "../index";

// Instantiate the general purpose client from the Aragon OSx SDK context.

// Create a TokenVoting client.
const tokenVotingClient: TokenVotingClient = new TokenVotingClient(
context,
);

const tokenAddress = "0x1234567890123456789012345678901234567890"; // token contract adddress

const compatibility = tokenVotingClient.methods.isTokenVotingCompatibleToken(tokenAddress);

console.log(compatibility);

/* MARKDOWN
Returns:
```ts
// "compatible" if is erc20 and erc165
// "needsWrap" if is erc20 and not erc165 or compatible with voting
// "incompatible" if is not erc20
compatible
```
*/
9 changes: 5 additions & 4 deletions modules/client/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@aragon/sdk-client",
"author": "Aragon Association",
"version": "1.12.0-rc1",
"version": "1.13.0-rc1",
"license": "MIT",
"main": "dist/index.js",
"module": "dist/sdk-client.esm.js",
Expand Down Expand Up @@ -62,16 +62,17 @@
},
"dependencies": {
"@aragon/osx-ethers": "1.3.0-rc0.2",
"@aragon/sdk-client-common": "^1.4.0-rc0",
"@aragon/sdk-common": "^1.5.0",
"@aragon/sdk-client-common": "^1.5.0-rc0",
"@aragon/sdk-common": "^1.6.0",
"@aragon/sdk-ipfs": "^1.1.0",
"@ethersproject/abstract-signer": "^5.5.0",
"@ethersproject/bignumber": "^5.6.0",
"@ethersproject/constants": "^5.6.0",
"@ethersproject/contracts": "^5.5.0",
"@ethersproject/providers": "^5.5.0",
"@ethersproject/wallet": "^5.6.0",
"@openzeppelin/contracts": "^4.9.2",
"@openzeppelin/contracts": "^4.8.1",
"@openzeppelin/contracts-upgradeable": "^4.8.1",
"graphql": "^16.5.0",
"graphql-request": "^4.3.0"
},
Expand Down
58 changes: 57 additions & 1 deletion modules/client/src/tokenVoting/internal/client/methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
IpfsPinError,
isProposalId,
NoProviderError,
NotAContractError,
promiseWithTimeout,
ProposalCreationError,
resolveIpfsCid,
Expand Down Expand Up @@ -60,6 +61,7 @@ import {
SubgraphTokenVotingMember,
SubgraphTokenVotingProposal,
SubgraphTokenVotingProposalListItem,
TokenVotingTokenCompatibility,
} from "../types";
import {
QueryTokenVotingMembers,
Expand All @@ -70,6 +72,7 @@ import {
} from "../graphql-queries";
import {
computeProposalStatusFilter,
isERC20Token,
tokenVotingInitParamsToContract,
toTokenVotingMember,
toTokenVotingProposal,
Expand Down Expand Up @@ -97,7 +100,15 @@ import {
UNAVAILABLE_PROPOSAL_METADATA,
UNSUPPORTED_PROPOSAL_METADATA_LINK,
} from "@aragon/sdk-client-common";
import { INSTALLATION_ABI } from "../constants";
import {
ERC165_INTERFACE_ID,
GOVERNANCE_SUPPORTED_INTERFACE_IDS,
INSTALLATION_ABI,
} from "../constants";
import { abi as ERC165_ABI } from "@openzeppelin/contracts/build/contracts/ERC165.json";
import { Contract } from "@ethersproject/contracts";
import { AddressZero } from "@ethersproject/constants";

/**
* Methods module the SDK TokenVoting Client
*/
Expand Down Expand Up @@ -740,4 +751,49 @@ export class TokenVotingClientMethods extends ClientCore
}
return null;
}

/**
* Checks if the given token is compatible with the TokenVoting plugin
*
* @param {string} tokenAddress
* @return {*} {Promise<TokenVotingTokenCompatibility>}
* @memberof TokenVotingClientMethods
*/
public async isTokenVotingCompatibleToken(
tokenAddress: string,
): Promise<TokenVotingTokenCompatibility> {
const signer = this.web3.getConnectedSigner();
// check if is address
if (!isAddress(tokenAddress) || tokenAddress === AddressZero) {
throw new InvalidAddressError();
}
const provider = this.web3.getProvider();
// check if is a contract
if (await provider.getCode(tokenAddress) === "0x") {
throw new NotAContractError();
}
const contract = new Contract(
tokenAddress,
ERC165_ABI,
signer,
);

if (!await isERC20Token(tokenAddress, signer)) {
return TokenVotingTokenCompatibility.INCOMPATIBLE;
}
try {
if (!await contract.supportsInterface(ERC165_INTERFACE_ID)) {
return TokenVotingTokenCompatibility.NEEDS_WRAPPING;
}
for (const interfaceId of GOVERNANCE_SUPPORTED_INTERFACE_IDS) {
const isSupported = await contract.supportsInterface(interfaceId);
if (isSupported) {
return TokenVotingTokenCompatibility.COMPATIBLE;
}
}
return TokenVotingTokenCompatibility.NEEDS_WRAPPING;
} catch (e) {
return TokenVotingTokenCompatibility.NEEDS_WRAPPING;
}
}
}
18 changes: 18 additions & 0 deletions modules/client/src/tokenVoting/internal/constants.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import {
IERC20MintableUpgradeable__factory,
IGovernanceWrappedERC20__factory,
MajorityVotingBase__factory,
} from "@aragon/osx-ethers";
import { getInterfaceId } from "@aragon/sdk-common";
import { MetadataAbiInput } from "@aragon/sdk-client-common";
import { abi as IVOTES_ABI } from "@openzeppelin/contracts/build/contracts/IVotes.json";
import { abi as IVOTES_UPGRADEABLE_ABI } from "@openzeppelin/contracts-upgradeable/build/contracts/IVotesUpgradeable.json";
import { abi as ERC165_ABI } from "@openzeppelin/contracts/build/contracts/ERC165.json";
import { Interface } from "@ethersproject/abi";

export const AVAILABLE_FUNCTION_SIGNATURES: string[] = [
MajorityVotingBase__factory.createInterface().getFunction(
"updateVotingSettings",
Expand Down Expand Up @@ -107,3 +114,14 @@ export const INSTALLATION_ABI: MetadataAbiInput[] = [
"The token mint settings struct containing the `receivers` and `amounts`.",
},
];


export const ERC165_INTERFACE_ID = getInterfaceId(
new Interface(ERC165_ABI),
);

export const GOVERNANCE_SUPPORTED_INTERFACE_IDS = [
getInterfaceId(new Interface(IVOTES_UPGRADEABLE_ABI)),
getInterfaceId(new Interface(IVOTES_ABI)),
getInterfaceId(IGovernanceWrappedERC20__factory.createInterface()),
];
2 changes: 2 additions & 0 deletions modules/client/src/tokenVoting/internal/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
WrapTokensParams,
WrapTokensStepValue,
} from "../types";
import { TokenVotingTokenCompatibility } from "./types";

// TokenVoting

Expand Down Expand Up @@ -80,6 +81,7 @@ export interface ITokenVotingClientMethods {
tokenAddress: string,
) => AsyncGenerator<UndelegateTokensStepValue>;
getDelegatee: (tokenAddress: string) => Promise<string | null>;
isTokenVotingCompatibleToken: (tokenAddress: string) => Promise<TokenVotingTokenCompatibility>;
}

export interface ITokenVotingClientEncoding {
Expand Down
6 changes: 6 additions & 0 deletions modules/client/src/tokenVoting/internal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,9 @@ export type SubgraphTokenVotingMember = {
balance: string;
}[];
};

export enum TokenVotingTokenCompatibility {
COMPATIBLE = "compatible",
NEEDS_WRAPPING = "needsWrapping",
INCOMPATIBLE = "incompatible",
}
36 changes: 34 additions & 2 deletions modules/client/src/tokenVoting/internal/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ import {
ProposalStatus,
TokenType,
} from "@aragon/sdk-client-common";
import { Signer } from "@ethersproject/abstract-signer";
import { Contract } from "@ethersproject/contracts";
import { abi as ERC_20_ABI } from "@openzeppelin/contracts/build/contracts/ERC20.json";

export function toTokenVotingProposal(
proposal: SubgraphTokenVotingProposal,
Expand Down Expand Up @@ -282,8 +285,6 @@ export function toTokenVotingMember(
};
}



export function computeProposalStatus(
proposal: SubgraphTokenVotingProposal | SubgraphTokenVotingProposalListItem,
): ProposalStatus {
Expand Down Expand Up @@ -335,3 +336,34 @@ export function computeProposalStatusFilter(status: ProposalStatus) {
}
return where;
}

/**
* Checks if the given address is an ERC20 token
* This function isn not 100% accurate.
* It just checks if the token has a balanceOf
* function and a decimals function
*
* @export
* @param {string} tokenAddress
* @param {Signer} signer
* @return {*} {Promise<boolean>}
*/
export async function isERC20Token(
josemarinas marked this conversation as resolved.
Show resolved Hide resolved
tokenAddress: string,
signer: Signer,
): Promise<boolean> {
const contract = new Contract(
tokenAddress,
ERC_20_ABI,
signer,
);
try {
await Promise.all([
contract.balanceOf(await signer.getAddress()),
contract.decimals(),
]);
return true;
} catch {
return false;
}
}
52 changes: 52 additions & 0 deletions modules/client/test/integration/tokenVoting-client/methods.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ import * as deployContracts from "../../helpers/deployContracts";

import {
getExtendedProposalId,
InvalidAddressError,
InvalidAddressOrEnsError,
NotAContractError,
} from "@aragon/sdk-common";
import {
ADDRESS_FOUR,
Expand Down Expand Up @@ -72,8 +74,10 @@ import {
SubgraphTokenVotingMember,
SubgraphTokenVotingProposal,
SubgraphTokenVotingProposalListItem,
TokenVotingTokenCompatibility,
} from "../../../src/tokenVoting/internal/types";
import { deployErc20 } from "../../helpers/deploy-erc20";
import { deployErc721 } from "../../helpers/deploy-erc721";
import {
GovernanceERC20__factory,
GovernanceWrappedERC20__factory,
Expand Down Expand Up @@ -1426,6 +1430,54 @@ describe("Token Voting Client", () => {
const token = await client.methods.getToken(pluginAddress);
expect(token).toBe(null);
});

it("Should check if a ERC20 is compatible with the TokenVotingSetup and return needs wrapping", async () => {
const ctx = new Context(contextParamsLocalChain);
const client = new TokenVotingClient(ctx);
const erc20Token = await deployErc20();
const compatibility = await client.methods.isTokenVotingCompatibleToken(
erc20Token.address,
);
expect(compatibility).toBe(
TokenVotingTokenCompatibility.NEEDS_WRAPPING,
);
});
it("Should check if ERC721 is compatible with governance and return incompatible", async () => {
const ctx = new Context(contextParamsLocalChain);
const client = new TokenVotingClient(ctx);
const erc721Token = await deployErc721();
const compatibility = await client.methods.isTokenVotingCompatibleToken(
erc721Token.address,
);
expect(compatibility).toBe(TokenVotingTokenCompatibility.INCOMPATIBLE);
});
it("Should be called with an invalid address and throw InvalidAddressError", async () => {
const ctx = new Context(contextParamsLocalChain);
const client = new TokenVotingClient(ctx);
await expect(() =>
client.methods.isTokenVotingCompatibleToken(
"0x123",
)
).rejects.toThrow(InvalidAddressError);
});
it("Should be called with wallet invalid address and throw NotAContractError", async () => {
const ctx = new Context(contextParamsLocalChain);
const client = new TokenVotingClient(ctx);
await expect(() =>
client.methods.isTokenVotingCompatibleToken(
TEST_WALLET_ADDRESS,
)
).rejects.toThrow(NotAContractError);
});
it("Should check if GovernanceERC20 is compatible with governance and return compatible", async () => {
const ctx = new Context(contextParamsLocalChain);
const client = new TokenVotingClient(ctx);
const dao = await buildTokenVotingDAO(repoAddr, VotingMode.STANDARD);
const isCompatible = await client.methods.isTokenVotingCompatibleToken(
dao.tokenAddress,
);
expect(isCompatible).toBe(TokenVotingTokenCompatibility.COMPATIBLE);
});
});
});
});
4 changes: 3 additions & 1 deletion modules/common/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ TEMPLATE:
-->

## [UPCOMING]

### Added
- New error classes
- `getInterfaceId` function
## 1.5.0
### Added
- New error classes `SizeMismatchError`, `InvalidProposalStatusError`, `NotImplementedError`, `InvalidActionError`, `InvalidSubdomainError`, `InvalidGasEstimationFactorError` and `UseTransferError`
Expand Down
Loading