From 20a53636beda5fc045e94c891fc4b80442cf99eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Manuel=20Mari=C3=B1as=20Bascoy?= Date: Wed, 15 Nov 2023 16:55:46 +0100 Subject: [PATCH] refactor checks --- .../client/src/internal/client/decoding.ts | 9 +- modules/client/src/internal/client/methods.ts | 436 +++------- .../src/internal/graphql-queries/plugin.ts | 7 + modules/client/src/internal/interfaces.ts | 15 +- modules/client/src/internal/types.ts | 8 +- modules/client/src/internal/utils.ts | 401 ++++++++- modules/client/src/types.ts | 13 +- .../test/integration/client/methods.test.ts | 737 +++-------------- modules/client/test/unit/client/utils.test.ts | 773 ++++++++++++++++-- 9 files changed, 1348 insertions(+), 1051 deletions(-) diff --git a/modules/client/src/internal/client/decoding.ts b/modules/client/src/internal/client/decoding.ts index 2d6917e89..d4f43887f 100644 --- a/modules/client/src/internal/client/decoding.ts +++ b/modules/client/src/internal/client/decoding.ts @@ -20,6 +20,7 @@ import { decodeApplyUpdateAction, decodeGrantAction, decodeInitializeFromAction, + decodeUpgradeToAction, decodeUpgradeToAndCallAction, findInterface, permissionParamsFromContract, @@ -308,13 +309,7 @@ export class ClientDecoding extends ClientCore implements IClientDecoding { return result[0]; } public upgradeToAction(data: Uint8Array): string { - const daoInterface = DAO__factory.createInterface(); - const hexBytes = bytesToHex(data); - const expectedFunction = daoInterface.getFunction( - "upgradeTo", - ); - const result = daoInterface.decodeFunctionData(expectedFunction, hexBytes); - return result[0]; + return decodeUpgradeToAction(data); } /** * Decodes upgradeToAndCallback params from an upgradeToAndCallAction diff --git a/modules/client/src/internal/client/methods.ts b/modules/client/src/internal/client/methods.ts index fa7856926..6d2e6ae2b 100644 --- a/modules/client/src/internal/client/methods.ts +++ b/modules/client/src/internal/client/methods.ts @@ -22,6 +22,7 @@ import { QueryDao, QueryDaos, QueryIPlugin, + QueryIProposal, QueryPlugin, QueryPluginPreparationsExtended, QueryPlugins, @@ -44,15 +45,12 @@ import { DaoSortBy, DaoUpdateProposalInvalidityCause, DaoUpdateProposalValidity, - DecodedInitializeFromParams, DepositErc1155Params, DepositErc20Params, DepositErc721Params, DepositEthParams, DepositParams, HasPermissionParams, - IsDaoUpdateValidParams, - IsPluginUpdateValidParams, PluginPreparationListItem, PluginPreparationQueryParams, PluginPreparationSortBy, @@ -72,10 +70,10 @@ import { TransferSortBy, } from "../../types"; import { - ProposalActionTypes, SubgraphBalance, SubgraphDao, SubgraphDaoListItem, + SubgraphIProposal, SubgraphPluginInstallation, SubgraphPluginPreparationListItem, SubgraphPluginRepo, @@ -85,22 +83,18 @@ import { } from "../types"; import { classifyProposalActions, - decodeInitializeFromAction, - decodeUpgradeToAndCallAction, - findActionIndex, + isDaoUpdateAction, isPluginUpdateAction, isPluginUpdateActionWithRootPermission, toAssetBalance, + toDaoActions, toDaoDetails, toDaoListItem, toPluginPreparationListItem, toPluginRepo, toTokenTransfer, - validateApplyUpdateFunction, - validateGrantRootPermissionAction, - validateGrantUpdatePluginPermissionAction, - validateRevokeRootPermissionAction, - validateRevokeUpdatePluginPermissionAction, + validateUpdateDaoProposalActions, + validateUpdatePluginProposalActions, } from "../utils"; import { isAddress } from "@ethersproject/address"; import { toUtf8Bytes } from "@ethersproject/strings"; @@ -121,7 +115,6 @@ import { AddressOrEnsSchema, AmountMismatchError, ClientCore, - DaoAction, DaoCreationError, EmptyMultiUriError, FailedDepositError, @@ -166,8 +159,6 @@ import { DepositErc721Schema, DepositEthSchema, HasPermissionSchema, - IsDaoUpdateValidSchema, - IsPluginUpdateValidSchema, PluginPreparationQuerySchema, PluginQuerySchema, } from "../schemas"; @@ -1110,334 +1101,163 @@ export class ClientMethods extends ClientCore implements IClientMethods { return version; } - public isDaoUpdate( - actions: DaoAction[], - ): boolean { - const initializeFromInterface = DAO__factory.createInterface() - .getFunction("initializeFrom").format("minimal"); - return findActionIndex(actions, initializeFromInterface) !== -1; - } /** - * Check if the specified actions try to update a plugin + * Given a proposal id returns if that proposal is a dao update proposal * - * @param {DaoAction[]} actions - * @return {*} {boolean} + * @param {string} proposalId + * @return {*} {Promise} * @memberof ClientMethods */ - public isPluginUpdate( - actions: DaoAction[], - ): boolean { - const applyUpdateInterface = PluginSetupProcessor__factory.createInterface() - .getFunction("applyUpdate").format("minimal"); - return findActionIndex(actions, applyUpdateInterface) !== -1; + public async isDaoUpdate( + proposalId: string, + ): Promise { + const name = "iproposal"; + const query = QueryIProposal; + type T = { iproposal: SubgraphIProposal }; + const { iproposal } = await this.graphql.request({ + query, + params: { id: proposalId.toLowerCase() }, + name, + }); + if (!iproposal) { + return false; + } + const subgraphActions = iproposal.actions; + let actions = toDaoActions(subgraphActions); + const classifiedActions = classifyProposalActions(actions); + return isDaoUpdateAction(classifiedActions); } - /** - * Check if the specified proposal id is valid for updating a plugin - * The failure map should be checked before calling this method + * Given a proposal id returns if that proposal is a plugin update proposal * - * @param {DaoAction[]} actions - * @return {*} {Promise} + * @param {string} proposalId + * @return {*} {Promise} * @memberof ClientMethods */ - + public async isPluginUpdate( + proposalId: string, + ): Promise { + const name = "iproposal"; + const query = QueryIProposal; + type T = { iproposal: SubgraphIProposal }; + const { iproposal } = await this.graphql.request({ + query, + params: { id: proposalId.toLowerCase() }, + name, + }); + if (!iproposal) { + return false; + } + const subgraphActions = iproposal.actions; + let actions = toDaoActions(subgraphActions); + const classifiedActions = classifyProposalActions(actions); + return isPluginUpdateAction(classifiedActions) || + isPluginUpdateActionWithRootPermission(classifiedActions); + } /** - * Check that the failure map is all false -> cannot be checked because we receive tha actions, not the proposal id - * Divide the proposal into blocks of actions, and then check them separately. - * - UpgradeTo and upgradeToAndCall should only be in the first position of the list, this means that we can assume - * that the rest of the actions will be related to o one or multiple plugin updates. - * Michael: - * - * This is required in case - * - there is a dependency (the new plugin version requires a specific DAO version, although the PluginSetup could theoretically check this) - * - the DAO update fixes a bug / exploit that could be triggered by a plugin setup being applied afterwards. - * - * - In the rest of cases we must follow a pattern checking. - * Since we are doing an update we should have at least 3 actions and up to 5 actions: - * - First action is always grant UPGRADE_PLUGIN_PERMISSION - * - Check third action, if its revoke UPGRADE_PLUGIN_PERMISSION, the middle one is an applyUpdate, if its an applyUpdate the middle one is a grant ROOT_PERMISSION - * - If third action is a revoke, check that the fourth and fifth are a revoke of ROOT_PERMISSION and a revoke of UPGRADE_PLUGIN_PERMISSION + * Check if the specified proposal id is valid for updating a plugin * - * Check that the to in all actions are expected contracts (PSP, dao) anything else is invalid - * Check that the value is 0 in all cases, this may change in the future, but for now is always 0 - * Filter all the actions and make sure that we only have the calls to expected methods (grant, revoke, upgradeToAndCall...) - * ands that there is not any other action like withdraw or transfer - * Pattern matching is needed grant/applyupdate/revoke => valid - * Then do the proper checks for each group of actions depending on what you want to do, - * for example checking a plugin upgrade should only receive the subset of actions related to that plugin upgrade - * Checking that the ROOT_PERMISSION is granted if the permissions in the applyUpdate action are not empty + * @param {string} proposalId + * @return {*} {Promise} + * @memberof ClientMethods */ - - private updateIndex = 0; public async isPluginUpdateValid( - params: IsPluginUpdateValidParams, + proposalId: string, ): Promise { - IsPluginUpdateValidSchema.strict().validate(params); + // not validating the proposalId because multiple proposal id formats can be used let causes: PluginUpdateProposalInValidityCause[][] = []; - const { daoAddress, pluginAddress } = params; - let actions = params.actions; - const classifiedActions = classifyProposalActions(params.actions); - - if (isPluginUpdateAction(classifiedActions)) { - const pspAddress = this.web3.getAddress("pluginSetupProcessorAddress"); - for (const [index, action] of classifiedActions.entries()) { - switch (action) { - case ProposalActionTypes.GRANT_PLUGIN_UPDATE_PERMISSION: - causes[this.updateIndex].concat( - validateGrantUpdatePluginPermissionAction( - actions[index], - pluginAddress, - pspAddress, - ), - ); - break; - case ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION: - causes[this.updateIndex].concat( - validateRevokeUpdatePluginPermissionAction( - actions[index], - pluginAddress, - pspAddress, - ), - ); - break; - case ProposalActionTypes.APPLY_UPDATE: - causes[this.updateIndex].concat( - await validateApplyUpdateFunction( - actions[index], - daoAddress, - this.graphql, - this.ipfs, - ), - ); - break; - } - if (causes.length !== 0) { - actions = actions.slice(2); - this.updateIndex++; - const recCauses = await this.isPluginUpdateValid({ - actions, - daoAddress, - pluginAddress, - }); - causes = [...causes, ...recCauses.causes]; - } - } - } else if (isPluginUpdateActionWithRootPermission(classifiedActions)) { - const pspAddress = this.web3.getAddress("pluginSetupProcessorAddress"); - for (const [index, action] of classifiedActions.entries()) { - switch (action) { - case ProposalActionTypes.GRANT_PLUGIN_UPDATE_PERMISSION: - causes[this.updateIndex].concat( - validateGrantUpdatePluginPermissionAction( - actions[index], - pluginAddress, - pspAddress, - ), - ); - break; - case ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION: - causes[this.updateIndex].concat( - validateRevokeUpdatePluginPermissionAction( - actions[index], - pluginAddress, - pspAddress, - ), - ); - break; - case ProposalActionTypes.GRANT_ROOT_PERMISSION: - causes[this.updateIndex].concat( - validateGrantRootPermissionAction( - actions[index], - daoAddress, - pspAddress, - ), - ); - break; - case ProposalActionTypes.REVOKE_ROOT_PERMISSION: - causes[this.updateIndex].concat( - validateRevokeRootPermissionAction( - actions[index], - daoAddress, - pspAddress, - ), - ); - break; - case ProposalActionTypes.APPLY_UPDATE: - causes[this.updateIndex].concat( - await validateApplyUpdateFunction( - actions[index], - daoAddress, - this.graphql, - this.ipfs, - ), - ); - break; - } - } - if (causes.length !== 0) { - actions = actions.slice(4); - this.updateIndex++; - const recCauses = await this.isPluginUpdateValid({ - actions, - daoAddress, - pluginAddress, - }); - causes = [...causes, ...recCauses.causes]; - } - } else { - causes[this.updateIndex].push( - PluginUpdateProposalInValidityCause.INVALID_ACTIONS, + // get the iproposal given the proposal id + const name = "iproposal"; + const query = QueryIProposal; + type T = { iproposal: SubgraphIProposal }; + const { iproposal } = await this.graphql.request({ + query, + params: { id: proposalId.toLowerCase() }, + name, + }); + if (!iproposal) { + // if the proposal does not exist return proposal not found + causes[0] = []; + causes[0].push( + PluginUpdateProposalInValidityCause.PROPOSAL_NOT_FOUND, ); return { isValid: false, causes, }; } - this.updateIndex = 0; - return { - isValid: true, - causes, - }; + // check failure map + if (iproposal.allowFailureMap !== "0") { + causes[0] = []; + // if the failure map is not 0 return invalid failure map + causes[0].push( + PluginUpdateProposalInValidityCause.INVALID_ALLOW_FAILURE_MAP, + ); + return { + isValid: false, + causes, + }; + } + // validate actions + return validateUpdatePluginProposalActions( + toDaoActions(iproposal.actions), + iproposal.dao.id, + this.web3.getAddress("pluginSetupProcessorAddress"), + this.graphql, + this.ipfs, + ); } /** - * Check if the specified actions are valid for updating a dao - * The failure map should be checked before calling this method + * Check if the specified proposalId actions are valid for updating a dao * - * @param {IsDaoUpdateValidParams} params + * @param {string} proposalId + * @param {SupportedVersion} [version] * @return {*} {Promise} * @memberof ClientMethods */ public async isDaoUpdateValid( - params: IsDaoUpdateValidParams, + proposalId: string, + version?: SupportedVersion, ): Promise { - await IsDaoUpdateValidSchema.strict().validate(params); - const causes: DaoUpdateProposalInvalidityCause[] = []; - // get initialize from signature - const upgradeToAndCallSignature = DAO__factory.createInterface() - .getFunction( - "upgradeToAndCall", - ).format("minimal"); - const upgradeToAndCallIndex = findActionIndex( - params.actions, - upgradeToAndCallSignature, - ); - // check that initialize from action is present - if (upgradeToAndCallIndex === -1) { - causes.push(DaoUpdateProposalInvalidityCause.INVALID_ACTIONS); + // omit input validation because we are receiving the proposal id + + // get the iproposal given the proposal id + const name = "iproposal"; + const query = QueryIProposal; + type T = { iproposal: SubgraphIProposal }; + const res = await this.graphql.request({ + query, + params: { id: proposalId.toLowerCase() }, + name, + }); + const { iproposal } = res; + // if the proposal does not exist return invalid + if (!iproposal) { return { - isValid: causes.length === 0, - causes, + isValid: false, + causes: [ + DaoUpdateProposalInvalidityCause.PROPOSAL_NOT_FOUND, + ], }; } - const decodedUpgradeToAndCallParams = decodeUpgradeToAndCallAction( - params.actions[upgradeToAndCallIndex].data, - ); - let decodedInitializeFromParams: DecodedInitializeFromParams; - try { - decodedInitializeFromParams = decodeInitializeFromAction( - decodedUpgradeToAndCallParams.data, - ); - } catch { - causes.push(DaoUpdateProposalInvalidityCause.INVALID_ACTIONS); - return { isValid: causes.length === 0, causes }; - } - - // check version - if ( - !await this.isDaoUpdateVersionValid( - params.daoAddress, - decodedInitializeFromParams.previousVersion, - ) - ) { - causes.push(DaoUpdateProposalInvalidityCause.INVALID_VERSION); - } - // get version if not specified use the one from the dao factory address - // in the context - let upgradeToVersion = params.version; - if (!upgradeToVersion) { - upgradeToVersion = await this.getProtocolVersion( - this.web3.getAddress("daoFactoryAddress"), - ); - } - // check implementation - if ( - !await this.isDaoUpdateImplementationValid( - upgradeToVersion.join(".") as SupportedVersion, - decodedUpgradeToAndCallParams.implementationAddress, - ) - ) { - causes.push(DaoUpdateProposalInvalidityCause.INVALID_IMPLEMENTATION); - } - // check data - if (!this.isDaoUpdateInitDataValid(decodedInitializeFromParams.initData)) { - causes.push(DaoUpdateProposalInvalidityCause.INVALID_INIT_DATA); + // get implementation address, use latest version as default + let daoFactoryAddress = this.web3.getAddress("daoFactoryAddress"); + if (version) { + // if version is specified get the dao factory address from the live contracts + daoFactoryAddress = LIVE_CONTRACTS[version][ + this.web3.getNetworkName() + ].daoFactoryAddress; } - return { isValid: causes.length === 0, causes }; - } - /** - * Check if the current version of the dao is the same as the specified version - * - * @private - * @param {string} daoAddress - * @param {[number, number, number]} specifiedVersion - * @return {*} {Promise} - * @memberof ClientMethods - */ - private async isDaoUpdateVersionValid( - daoAddress: string, - specifiedVersion: [number, number, number], - ): Promise { - // get the current version of the dao, so the result should not be the upgraded value - const currentDaoVersion = await this.getProtocolVersion(daoAddress); - // currentDao version should be equal to the previous version - // because it references the version that the dao will be upgraded from - // ex: if we want to upgrade from version 1.0.0 to 1.3.0 - // the previous version should be 1.0.0 and so should be the current dao version - return JSON.stringify(currentDaoVersion) === - JSON.stringify(specifiedVersion); - } - - /** - * Check if the implementation address is the same as the one from the dao factory - * - * @private - * @param {SupportedVersion} version - * @param {string} implementationAddress - * @return {*} {Promise} - * @memberof ClientMethods - */ - private async isDaoUpdateImplementationValid( - version: SupportedVersion, - implementationAddress: string, - ): Promise { - const networkName = this.web3.getNetworkName(); - // The dao factory address holds the implementation address for each version - // so we can check that the specified implementation address is the same - // as the one from the dao factory - const daoFactoryAddress = - LIVE_CONTRACTS[version][networkName].daoFactoryAddress; - const daoBase = await this.getDaoImplementation(daoFactoryAddress); - return daoBase === implementationAddress; - } - - /** - * Check if the init data is valid for the specified version of the dao - * - * @param {IsDaoUpdateInitDataValidParams} params - * @return {*} {Promise} - * @memberof ClientMethods - */ - private isDaoUpdateInitDataValid( - data: Uint8Array, - _version?: SupportedVersion, - ): boolean { - // TODO: decode the data using the abi from the the prepare update - // for now the init data must be empty but this can change in the future - // atm we cannot know the parameters for each version of the dao - return data.length === 0; + return validateUpdateDaoProposalActions( + toDaoActions(iproposal.actions), + iproposal.dao.id, + await this.getDaoImplementation(daoFactoryAddress), + await this.getProtocolVersion( + iproposal.dao.id, + ), + ); } - /** * Return the implementation address for the specified dao factory * diff --git a/modules/client/src/internal/graphql-queries/plugin.ts b/modules/client/src/internal/graphql-queries/plugin.ts index 7d58ebcb9..e18b9d680 100644 --- a/modules/client/src/internal/graphql-queries/plugin.ts +++ b/modules/client/src/internal/graphql-queries/plugin.ts @@ -96,3 +96,10 @@ query PluginPreparations($where: PluginPreparation_filter!, $limit: Int!, $skip: } } `; + +export const QueryPluginInstallations = gql` + query PluginInstallations($where: PluginInstallation_filter!) { + pluginInstallations(where: $where) { + id + } + }` diff --git a/modules/client/src/internal/interfaces.ts b/modules/client/src/internal/interfaces.ts index f5ca75cc7..04684c8c3 100644 --- a/modules/client/src/internal/interfaces.ts +++ b/modules/client/src/internal/interfaces.ts @@ -15,6 +15,7 @@ import { PrepareUninstallationStepValue, PrepareUpdateParams, PrepareUpdateStepValue, + SupportedVersion, } from "@aragon/sdk-client-common"; import { AssetBalance, @@ -35,7 +36,6 @@ import { GrantPermissionWithConditionParams, HasPermissionParams, InitializeFromParams, - IsDaoUpdateValidParams, PluginPreparationListItem, PluginPreparationQueryParams, PluginQueryParams, @@ -98,19 +98,20 @@ export interface IClientMethods { ) => Promise<[number, number, number]>; isPluginUpdate: ( - actions: DaoAction[], - ) => boolean; + proposalId: string, + ) => Promise; isPluginUpdateValid: ( - params: IsDaoUpdateValidParams, + proposalId: string, ) => Promise; isDaoUpdate: ( - actions: DaoAction[], - ) => boolean; + proposalId: string, + ) => Promise; isDaoUpdateValid: ( - params: IsDaoUpdateValidParams, + proposalId: string, + version?: SupportedVersion, ) => Promise; getDaoImplementation: ( diff --git a/modules/client/src/internal/types.ts b/modules/client/src/internal/types.ts index 9c568815b..df6bce33e 100644 --- a/modules/client/src/internal/types.ts +++ b/modules/client/src/internal/types.ts @@ -242,10 +242,14 @@ export enum ProposalActionTypes { UPGRADE_TO = "upgradeTo", UPGRADE_TO_AND_CALL = "upgradeToAndCall", APPLY_UPDATE = "applyUpdate", - GRANT_PLUGIN_UPDATE_PERMISSION = "grant", - REVOKE_PLUGIN_UPGRADE_PERMISSION = "revoke", + GRANT_PLUGIN_UPDATE_PERMISSION = "grantUpdatePluginPermission", + REVOKE_PLUGIN_UPGRADE_PERMISSION = "revokeUpdatePluginPeermission", GRANT_ROOT_PERMISSION = "grantRootPermission", REVOKE_ROOT_PERMISSION = "revokeRootPermission", ACTION_NOT_ALLOWED = "actionNotAllowed", UNKNOWN = "unknown", } + +export type SubgraphPluginInstallationListItem = { + id: string; +}; diff --git a/modules/client/src/internal/utils.ts b/modules/client/src/internal/utils.ts index adeac4b22..1e41125a4 100644 --- a/modules/client/src/internal/utils.ts +++ b/modules/client/src/internal/utils.ts @@ -3,6 +3,8 @@ import { DaoDetails, DaoListItem, DaoMetadata, + DaoUpdateProposalInvalidityCause, + DaoUpdateProposalValidity, DecodedInitializeFromParams, DepositErc1155Params, DepositErc20Params, @@ -40,6 +42,7 @@ import { SubgraphErc721TransferListItem, SubgraphNativeBalance, SubgraphNativeTransferListItem, + SubgraphPluginInstallationListItem, SubgraphPluginListItem, SubgraphPluginPermissionOperation, SubgraphPluginPreparationListItem, @@ -107,6 +110,7 @@ import { import { QueryDao, QueryPlugin, + QueryPluginInstallations, QueryPluginPreparations, } from "./graphql-queries"; @@ -791,6 +795,18 @@ export function decodeUpgradeToAndCallAction( }; } +export function decodeUpgradeToAction( + data: Uint8Array, +) { + const daoInterface = DAO__factory.createInterface(); + const hexBytes = bytesToHex(data); + const expectedFunction = daoInterface.getFunction( + "upgradeTo", + ); + const result = daoInterface.decodeFunctionData(expectedFunction, hexBytes); + return result[0]; +} + export function decodeInitializeFromAction( data: Uint8Array, ): DecodedInitializeFromParams { @@ -862,23 +878,51 @@ export function compareArrays(array1: T[], array2: T[]): boolean { } return true; } -export function validateGrantUpdatePluginPermissionAction( - action: DaoAction, +async function getPluginInstallations( + daoAddress: string, pluginAddress: string, + graphql: IClientGraphQLCore, +): Promise { + const name = "pluginInstallations"; + type U = { pluginInstallations: SubgraphPluginInstallationListItem[] }; + const query = QueryPluginInstallations; + const params = { + where: { + plugin: pluginAddress.toLowerCase(), + dao: daoAddress.toLowerCase(), + }, + }; + const res = await graphql.request({ + query, + params, + name, + }); + const { pluginInstallations } = res; + return pluginInstallations; +} +export async function validateGrantUpdatePluginPermissionAction( + action: DaoAction, pspAddress: string, -): PluginUpdateProposalInValidityCause[] { + daoAddress: string, + graphql: IClientGraphQLCore, +): Promise { const causes: PluginUpdateProposalInValidityCause[] = []; const decodedPermission = decodeGrantAction(action.data); - if (action.value.toString() !== "0") { + const pluginInstallations = await getPluginInstallations( + daoAddress, + decodedPermission.where, + graphql, + ); + if (pluginInstallations.length === 0) { causes.push( PluginUpdateProposalInValidityCause - .INVALID_GRANT_UPDATE_PERMISSION_VALUE, + .INVALID_GRANT_UPDATE_PERMISSION_WHERE_ADDRESS, ); } - if (decodedPermission.where !== pluginAddress) { + if (action.value.toString() !== "0") { causes.push( PluginUpdateProposalInValidityCause - .INVALID_GRANT_UPDATE_PERMISSION_WHERE_ADDRESS, + .INVALID_GRANT_UPDATE_PERMISSION_VALUE, ); } if (decodedPermission.who !== pspAddress) { @@ -906,23 +950,30 @@ export function validateGrantUpdatePluginPermissionAction( } return causes; } -export function validateRevokeUpdatePluginPermissionAction( + +export async function validateRevokeUpdatePluginPermissionAction( action: DaoAction, - pluginAddress: string, pspAddress: string, -): PluginUpdateProposalInValidityCause[] { + daoAddress: string, + graphql: IClientGraphQLCore, +): Promise { const causes: PluginUpdateProposalInValidityCause[] = []; const decodedPermission = decodeRevokeAction(action.data); - if (action.value.toString() !== "0") { + const pluginInstallations = await getPluginInstallations( + daoAddress, + decodedPermission.where, + graphql, + ); + if (pluginInstallations.length === 0) { causes.push( PluginUpdateProposalInValidityCause - .INVALID_REVOKE_UPDATE_PERMISSION_VALUE, + .INVALID_REVOKE_UPDATE_PERMISSION_WHERE_ADDRESS, ); } - if (decodedPermission.where !== pluginAddress) { + if (action.value.toString() !== "0") { causes.push( PluginUpdateProposalInValidityCause - .INVALID_REVOKE_UPDATE_PERMISSION_WHERE_ADDRESS, + .INVALID_REVOKE_UPDATE_PERMISSION_VALUE, ); } if (decodedPermission.who !== pspAddress) { @@ -1038,6 +1089,16 @@ export function validateRevokeRootPermissionAction( } return causes; } +/** + * Validate a plugin update proposal + * + * @export + * @param {DaoAction} action + * @param {string} daoAddress + * @param {IClientGraphQLCore} graphql + * @param {IClientIpfsCore} ipfs + * @return {*} {Promise} + */ export async function validateApplyUpdateFunction( action: DaoAction, daoAddress: string, @@ -1133,7 +1194,10 @@ export async function validateApplyUpdateFunction( const metadata = await ipfs.fetchString(metadataCid!); const metadataJson = JSON.parse(metadata) as PluginRepoBuildMetadata; // get the update abi for the specified build - if (metadataJson?.pluginSetup?.prepareUpdate[decodedParams.versionTag.build]?.inputs) { + if ( + metadataJson?.pluginSetup?.prepareUpdate[decodedParams.versionTag.build] + ?.inputs + ) { // if the abi exists try to decode the data const updateAbi = metadataJson.pluginSetup.prepareUpdate[ decodedParams.versionTag.build @@ -1169,6 +1233,14 @@ export async function validateApplyUpdateFunction( return causes; } +/** + * Given a list of actions, it decodes the actions and returns the + * type of action + * + * @export + * @param {DaoAction[]} actions + * @return {*} {ProposalActionTypes[]} + */ export function classifyProposalActions( actions: DaoAction[], ): ProposalActionTypes[] { @@ -1236,31 +1308,318 @@ export function classifyProposalActions( } return classifiedActions; } + +/** + * Returns true if the actions are valid for a plugin update proposal with root permission + * + * @export + * @param {ProposalActionTypes[]} actions + * @return {*} {boolean} + */ export function isPluginUpdateActionWithRootPermission( actions: ProposalActionTypes[], ): boolean { - // get the first 4 actions - const receivedPattern = actions.slice(0, 4); + // get the first 5 actions + // if the first is a dao upgrade skip it + let receivedPattern; + if ( + actions[0] === ProposalActionTypes.UPGRADE_TO || + actions[0] === ProposalActionTypes.UPGRADE_TO_AND_CALL + ) { + receivedPattern = actions.slice(1, 6); + } else { + receivedPattern = actions.slice(0, 5); + } // check if it matches the expected pattern // length should be 5 if ( - actions.length !== 5 || + receivedPattern.length !== 5 || !compareArrays(receivedPattern, PLUGIN_UPDATE_WITH_ROOT_ACTION_PATTERN) ) { return false; } return true; } + +/** + * Returns true if the actions are valid for a plugin update proposal without root permission + * + * @export + * @param {ProposalActionTypes[]} actions + * @return {*} {boolean} + */ export function isPluginUpdateAction(actions: ProposalActionTypes[]): boolean { - // get the first 3 actions - const receivedPattern = actions.slice(0, 2); + // get the first 3 action + // if the first is a dao upgrade skip it + let receivedPattern; + if ( + actions[0] === ProposalActionTypes.UPGRADE_TO || + actions[0] === ProposalActionTypes.UPGRADE_TO_AND_CALL + ) { + receivedPattern = actions.slice(1, 4); + } else { + receivedPattern = actions.slice(0, 3); + } // check if it matches the expected pattern // length should be 3 if ( - actions.length !== 3 || + receivedPattern.length !== 3 || !compareArrays(receivedPattern, PLUGIN_UPDATE_ACTION_PATTERN) ) { return false; } return true; } +/** + * Returns true if the actions are valid for a plugin update proposal + * + * @export + * @param {ProposalActionTypes[]} actions + * @return {*} {boolean} + */ +export function isDaoUpdateAction(actions: ProposalActionTypes[]): boolean { + // UpgradeTo or UpgradeToAndCall should be the first action + return actions[0] === ProposalActionTypes.UPGRADE_TO || + actions[0] === ProposalActionTypes.UPGRADE_TO_AND_CALL; +} + +export function validateUpdateDaoProposalActions( + actions: DaoAction[], + daoAddress: string, + implementationAddress: string, + currentDaoVersion: [number, number, number], +): DaoUpdateProposalValidity { + const classifiedActions = classifyProposalActions(actions); + const causes: DaoUpdateProposalInvalidityCause[] = []; + // check if the actions are valid + // if they are not valid return add + // invalid actions to the causes and return + if (isDaoUpdateAction(classifiedActions)) { + // if they are valid, the upgrade action must + // be the first one + const upgradeActionType = classifiedActions[0]; + const upgradeAction = actions[0]; + // if the to address is not the dao address + // add the cause to the causes array + if (upgradeAction.to !== daoAddress) { + causes.push( + DaoUpdateProposalInvalidityCause.INVALID_TO_ADDRESS, + ); + } + // if the value is different from 0 + // add the cause to the causes array + if (upgradeAction.value.toString() !== "0") { + causes.push( + DaoUpdateProposalInvalidityCause.INVALID_VALUE, + ); + } + // if the upgrade action is upgradeTo + if (upgradeActionType === ProposalActionTypes.UPGRADE_TO) { + // decode the upgradeTo action + const decodedImplementationAddress = decodeUpgradeToAction( + actions[0].data, + ); + // check that the implementation address is the same + if (implementationAddress !== decodedImplementationAddress) { + causes.push( + DaoUpdateProposalInvalidityCause + .INVALID_UPGRADE_TO_IMPLEMENTATION_ADDRESS, + ); + } + // if the upgrade action is upgradeToAndCall + } else if (upgradeActionType === ProposalActionTypes.UPGRADE_TO_AND_CALL) { + // decode the action + const upgradeToAndCallDecodedParams = decodeUpgradeToAndCallAction( + actions[0].data, + ); + // the call data should be the initializeFrom function encoded + // so we decode the initialize from function + const initializeFromDecodedParams = decodeInitializeFromAction( + upgradeToAndCallDecodedParams.data, + ); + // check that the implementation address is the same as specified + // in the upgradeToAndCall action + if ( + implementationAddress !== + upgradeToAndCallDecodedParams.implementationAddress + ) { + causes.push( + DaoUpdateProposalInvalidityCause + .INVALID_UPGRADE_TO_AND_CALL_IMPLEMENTATION_ADDRESS, + ); + } + // check that the current version version of the dao is the same + // as the one specified as previous version in the initializeFrom function + if ( + JSON.stringify(initializeFromDecodedParams.previousVersion) !== + JSON.stringify(currentDaoVersion) + ) { + causes.push( + DaoUpdateProposalInvalidityCause.INVALID_UPGRADE_TO_AND_CALL_VERSION, + ); + } + // TODO + // check that the data can be decoded with the abi in the metadata of the version we are + // upgrading to. For now is not possible so we check that the data is empty + if (initializeFromDecodedParams.initData.length !== 0) { + causes.push( + DaoUpdateProposalInvalidityCause.INVALID_UPGRADE_TO_AND_CALL_DATA, + ); + } + } + } else { + // add invalid actions to the causes array + causes.push( + DaoUpdateProposalInvalidityCause.INVALID_ACTIONS, + ); + } + // return the validity of the proposal + return { isValid: causes.length === 0, causes }; +} + +export async function validateUpdatePluginProposalActions( + actions: DaoAction[], + daoAddress: string, + pspAddress: string, + graphql: IClientGraphQLCore, + ipfs: IClientIpfsCore, +) { + let causes: PluginUpdateProposalInValidityCause[][] = []; + const classifiedActions = classifyProposalActions(actions); + if (isPluginUpdateAction(classifiedActions)) { + causes[0] = []; + for (const [index, action] of classifiedActions.entries()) { + switch (action) { + case ProposalActionTypes.GRANT_PLUGIN_UPDATE_PERMISSION: + causes[0].concat( + await validateGrantUpdatePluginPermissionAction( + actions[index], + pspAddress, + daoAddress, + graphql, + ), + ); + break; + case ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION: + causes[0].concat( + await validateRevokeUpdatePluginPermissionAction( + actions[index], + pspAddress, + daoAddress, + graphql, + ), + ); + break; + case ProposalActionTypes.APPLY_UPDATE: + const resCauses = await validateApplyUpdateFunction( + actions[index], + daoAddress, + graphql, + ipfs, + ); + causes[0].concat( + resCauses, + ); + break; + } + } + actions = actions.slice(3); + if (actions.length !== 0) { + const recCauses = await validateUpdatePluginProposalActions( + actions, + daoAddress, + pspAddress, + graphql, + ipfs, + ); + causes = causes.concat(recCauses.causes); + } + } else if (isPluginUpdateActionWithRootPermission(classifiedActions)) { + causes[0] = []; + for (const [index, action] of classifiedActions.entries()) { + switch (action) { + case ProposalActionTypes.GRANT_PLUGIN_UPDATE_PERMISSION: + causes[0].concat( + await validateGrantUpdatePluginPermissionAction( + actions[index], + pspAddress, + daoAddress, + graphql, + ), + ); + break; + case ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION: + causes[0].concat( + await validateRevokeUpdatePluginPermissionAction( + actions[index], + pspAddress, + daoAddress, + graphql, + ), + ); + break; + case ProposalActionTypes.GRANT_ROOT_PERMISSION: + causes[0].concat( + validateGrantRootPermissionAction( + actions[index], + daoAddress, + pspAddress, + ), + ); + break; + case ProposalActionTypes.REVOKE_ROOT_PERMISSION: + causes[0].concat( + validateRevokeRootPermissionAction( + actions[index], + daoAddress, + pspAddress, + ), + ); + break; + case ProposalActionTypes.APPLY_UPDATE: + causes[0].concat( + await validateApplyUpdateFunction( + actions[index], + daoAddress, + graphql, + ipfs, + ), + ); + break; + } + } + actions = actions.slice(5); + if (actions.length !== 0) { + const recCauses = await validateUpdatePluginProposalActions( + actions, + daoAddress, + pspAddress, + graphql, + ipfs, + ); + causes = [...causes, ...recCauses.causes]; + } + } else { + causes[0] = []; + causes[0].push( + PluginUpdateProposalInValidityCause.INVALID_ACTIONS, + ); + return { + isValid: false, + causes, + }; + } + return { + // every item in the array should be empty + isValid: causes.every((cause) => cause.length === 0), + causes, + }; +} + +export function toSubgraphActions(actions: DaoAction[]): SubgraphAction[] { + return actions.map((action) => ({ + to: action.to, + value: action.value.toString(), + data: bytesToHex(action.data), + })); +} diff --git a/modules/client/src/types.ts b/modules/client/src/types.ts index 60a303553..1357bc5dc 100644 --- a/modules/client/src/types.ts +++ b/modules/client/src/types.ts @@ -424,6 +424,7 @@ export type PluginUpdateProposalValidity = { }; export enum PluginUpdateProposalInValidityCause { + PROPOSAL_NOT_FOUND = "proposalNotFound", // Grant UPDATE_PLUGIN_PERMISSION action INVALID_GRANT_UPDATE_PERMISSION_WHO_ADDRESS = "invalidGrantUpdatePermissionWhoAddress", @@ -488,10 +489,16 @@ export type IsPluginUpdateProposalValidParams = { }; export enum DaoUpdateProposalInvalidityCause { + PROPOSAL_NOT_FOUND = "proposalNotFound", INVALID_ACTIONS = "invalidActions", - INVALID_IMPLEMENTATION = "invalidImplementation", - INVALID_VERSION = "invalidVersion", - INVALID_INIT_DATA = "invalidInitData", + INVALID_TO_ADDRESS = "invalidToAddress", + INVALID_VALUE = "invalidValue", + INVALID_UPGRADE_TO_IMPLEMENTATION_ADDRESS = + "invalidUpgradeToImplementationAddress", + INVALID_UPGRADE_TO_AND_CALL_DATA = "invalidUpgradeToAndCallData", + INVALID_UPGRADE_TO_AND_CALL_IMPLEMENTATION_ADDRESS = + "invalidUpgradeToAndCallImplementationAddress", + INVALID_UPGRADE_TO_AND_CALL_VERSION = "invalidUpgradeToAndCallVersion", } export type DaoUpdateProposalValidity = { diff --git a/modules/client/test/integration/client/methods.test.ts b/modules/client/test/integration/client/methods.test.ts index b1178c7e6..f32aeaad1 100644 --- a/modules/client/test/integration/client/methods.test.ts +++ b/modules/client/test/integration/client/methods.test.ts @@ -18,7 +18,6 @@ import { TEST_NO_BALANCES_DAO_ADDRESS, TEST_NON_EXISTING_ADDRESS, TEST_TX_HASH, - TOKEN_VOTING_BUILD_METADATA, // TEST_WALLET, } from "../constants"; import { @@ -40,6 +39,7 @@ import { PluginRepoBuildMetadata, PluginRepoReleaseMetadata, PluginSortBy, + PluginUpdateProposalInValidityCause, SetAllowanceParams, SetAllowanceSteps, TransferQueryParams, @@ -51,6 +51,7 @@ import { Server } from "ganache"; import { SubgraphBalance, SubgraphDao, + SubgraphIProposal, SubgraphPluginInstallation, SubgraphPluginPermissionOperation, SubgraphPluginPreparationListItem, @@ -95,7 +96,10 @@ import { import { JsonRpcProvider } from "@ethersproject/providers"; import { SupportedPluginRepo } from "../../../src/internal/constants"; import { ValidationError } from "yup"; -import { toPluginPermissionOperationType } from "../../../src/internal/utils"; +import { + toPluginPermissionOperationType, + toSubgraphActions, +} from "../../../src/internal/utils"; describe("Client", () => { let daoAddress: string; @@ -1866,30 +1870,47 @@ describe("Client", () => { describe("isPluginUpdateValid", () => { const context = new Context(); const client = new Client(context); - const mockedClient = mockedGraphqlRequest.getMockedInstance( - client.graphql.getClient(), - ); let updateActions: DaoAction[]; let applyUpdateParams: ApplyUpdateParams; let subgraphDao: SubgraphDao; let subgraphPluginRepo: SubgraphPluginRepo; + const mockedClient = mockedGraphqlRequest.getMockedInstance( + client.graphql.getClient(), + ); let subgraphPluginPreparation: SubgraphPluginUpdatePreparation; + let subgraphIProposal: SubgraphIProposal; + beforeEach(() => { + mockedClient.request.mockReset(); + }); beforeAll(() => { + subgraphPluginPreparation = { + data: "0x", + }; + applyUpdateParams = { - helpers: [], - pluginAddress, - pluginRepo: ADDRESS_ONE, - initData: new Uint8Array(), - permissions: [], versionTag: { - release: 1, build: 2, + release: 1, }, + initData: new Uint8Array(), + pluginRepo: ADDRESS_ONE, + pluginAddress: ADDRESS_ONE, + permissions: [], + helpers: [], }; + updateActions = client.encoding.applyUpdateAction( daoAddress, applyUpdateParams, ); + subgraphIProposal = { + dao: { + id: daoAddress, + }, + allowFailureMap: "0", + actions: toSubgraphActions(updateActions), + }; + subgraphDao = { id: daoAddress, subdomain: "test-tokenvoting-dao", @@ -1910,8 +1931,9 @@ describe("Client", () => { }, }], }; + subgraphPluginRepo = { - id: deployment.tokenVotingRepo.address, + id: ADDRESS_ONE, subdomain: SupportedPluginRepo.TOKEN_VOTING, releases: [ { @@ -1930,629 +1952,98 @@ describe("Client", () => { }, ], }; - subgraphPluginPreparation = { - data: "0x", - }; }); - it("should return an empty array when the actions are valid", async () => { - const ctx = new Context(contextParamsLocalChain); - const client = new Client(ctx); - - mockedClient.request.mockResolvedValueOnce({ + it("Should return true if the update is valid", async () => { + mockedClient.request.mockResolvedValue({ + iproposal: subgraphIProposal, + pluginInstallations: [{ + id: ADDRESS_ONE, + }], dao: subgraphDao, - }); - mockedClient.request.mockResolvedValueOnce({ + subgraphPreparation: subgraphPluginPreparation, pluginRepo: subgraphPluginRepo, }); - mockedClient.request.mockResolvedValueOnce({ - pluginPreparation: subgraphPluginPreparation, + const res = await client.methods.isPluginUpdateValid( + TEST_MULTISIG_PROPOSAL_ID, + ); + expect(res.isValid).toBe(true); + expect(res.causes).toMatchObject([[]]); + }); + it("Should throw if the proposal does not exist", async () => { + mockedClient.request.mockResolvedValue({ + iproposal: null, + pluginInstallations: [{ + id: ADDRESS_ONE, + }], + dao: subgraphDao, + subgraphPreparation: subgraphPluginPreparation, + pluginRepo: subgraphPluginRepo, }); - mockedIPFSClient.cat.mockResolvedValueOnce(Buffer.from( - JSON.stringify(TOKEN_VOTING_BUILD_METADATA), - )); - - const result = await client.methods.isPluginUpdateValid({ - daoAddress, - pluginAddress, - actions: updateActions, + const res = await client.methods.isPluginUpdateValid( + TEST_MULTISIG_PROPOSAL_ID, + ); + expect(res.isValid).toBe(false); + expect(res.causes).toMatchObject([[ + PluginUpdateProposalInValidityCause.PROPOSAL_NOT_FOUND, + ]]); + }); + it("Should throw if the failure map is not 0", async () => { + mockedClient.request.mockResolvedValue({ + iproposal: { ...subgraphIProposal, allowFailureMap: "1" }, + pluginInstallations: [{ + id: ADDRESS_ONE, + }], + dao: subgraphDao, + subgraphPreparation: subgraphPluginPreparation, + pluginRepo: subgraphPluginRepo, }); - - expect(result.isValid).toBe(true); - expect(result.causes.length).toBe(0); + const res = await client.methods.isPluginUpdateValid( + TEST_MULTISIG_PROPOSAL_ID, + ); + expect(res.isValid).toBe(false); + expect(res.causes).toMatchObject([[ + PluginUpdateProposalInValidityCause.INVALID_ALLOW_FAILURE_MAP, + ]]); }); - // it("should throw a `ProposalNotFoundError` for a proposal that does not exist", async () => { - // const ctx = new Context(contextParamsLocalChain); - // const client = new Client(ctx); - // expect( - // () => - // client.methods.isPluginUpdateValid({ - // actions: [], - // daoAddress, - // }), - // ).rejects.toThrow( - // new Error("actions field must have at least 1 items"), - // ); - // }); - // it("should return `INVALID_ACTIONS` when any of the required actions is not present", async () => { - // const ctx = new Context(contextParamsLocalChain); - // const client = new Client(ctx); - - // const validationResult = await client.methods - // .isPluginUpdateValid({ - // daoAddress, - // actions: [updateActions[0], updateActions[1]], - // }); - // expect(validationResult.isValid).toBe(false); - // expect(validationResult.causes.length).toBe(1); - // expect( - // validationResult.causes.includes( - // PluginUpdateProposalInValidityCause.INVALID_ACTIONS, - // ), - // ).toBe(true); - // }); - // it("should return `INVALID_GRANT_PERMISSION` when the grant permission is invalid", async () => { - // const ctx = new Context(contextParamsLocalChain); - // const client = new Client(ctx); - - // const mockedClient = mockedGraphqlRequest.getMockedInstance( - // client.graphql.getClient(), - // ); - // const invalidGrantAction = client.encoding.grantAction( - // daoAddress, - // { - // permission: Permissions.ROOT_PERMISSION, - // where: pluginAddress, - // who: daoAddress, - // }, - // ); - // mockedClient.request.mockResolvedValueOnce({ - // dao: subgraphDao, - // }); - // mockedClient.request.mockResolvedValueOnce({ - // pluginRepo: subgraphPluginRepo, - // }); - // mockedClient.request.mockResolvedValueOnce({ - // pluginPreparation: subgraphPluginPreparation, - // }); - // mockedIPFSClient.cat.mockResolvedValueOnce(Buffer.from( - // JSON.stringify(TOKEN_VOTING_BUILD_METADATA), - // )); - // const validationResult = await client.methods - // .isPluginUpdateValid({ - // actions: [ - // invalidGrantAction, - // updateActions[1], - // updateActions[2], - // ], - // daoAddress, - // }); - // expect(validationResult.isValid).toBe(false); - // expect(validationResult.causes.length).toBe(1); - // expect( - // validationResult.causes.includes( - // PluginUpdateProposalInValidityCause.INVALID_GRANT_PERMISSION, - // ), - // ).toBe(true); - // }); - // it("should return `INVALID_REVOKE_PERMISSION` when the grant permission is invalid", async () => { - // const ctx = new Context(contextParamsLocalChain); - // const client = new Client(ctx); - - // const mockedClient = mockedGraphqlRequest.getMockedInstance( - // client.graphql.getClient(), - // ); - // const invalidRevokeAction = client.encoding.revokeAction( - // daoAddress, - // { - // permission: Permissions.ROOT_PERMISSION, - // where: pluginAddress, - // who: daoAddress, - // }, - // ); - // mockedClient.request.mockResolvedValueOnce({ - // dao: subgraphDao, - // }); - // mockedClient.request.mockResolvedValueOnce({ - // pluginRepo: subgraphPluginRepo, - // }); - // mockedClient.request.mockResolvedValueOnce({ - // pluginPreparation: subgraphPluginPreparation, - // }); - // mockedIPFSClient.cat.mockResolvedValueOnce(Buffer.from( - // JSON.stringify(TOKEN_VOTING_BUILD_METADATA), - // )); - // const validationResult = await client.methods - // .isPluginUpdateValid({ - // daoAddress, - // actions: [ - // updateActions[0], - // updateActions[1], - // invalidRevokeAction, - // ], - // }); - // expect(validationResult.isValid).toBe(false); - // expect(validationResult.causes.length).toBe(1); - // expect( - // validationResult.causes.includes( - // PluginUpdateProposalInValidityCause.INVALID_REVOKE_PERMISSION, - // ), - // ).toBe(true); - // }); - // it("should return `INVALID_PLUGIN_RELEASE` when the release of the update is different", async () => { - // const ctx = new Context(contextParamsLocalChain); - // const client = new Client(ctx); - - // const mockedClient = mockedGraphqlRequest.getMockedInstance( - // client.graphql.getClient(), - // ); - // const invalidApplyUpdateActions = client.encoding.applyUpdateAction( - // daoAddress, - // { ...applyUpdateParams, versionTag: { release: 2, build: 2 } }, - // ); - // mockedClient.request.mockResolvedValueOnce({ - // dao: subgraphDao, - // }); - // mockedClient.request.mockResolvedValueOnce({ - // pluginRepo: subgraphPluginRepo, - // }); - // mockedClient.request.mockResolvedValueOnce({ - // pluginPreparation: subgraphPluginPreparation, - // }); - // mockedIPFSClient.cat.mockResolvedValueOnce(Buffer.from( - // JSON.stringify(TOKEN_VOTING_BUILD_METADATA), - // )); - // const validationResult = await client.methods - // .isPluginUpdateValid({ - // actions: invalidApplyUpdateActions, - // daoAddress, - // }); - // expect(validationResult.isValid).toBe(false); - // expect(validationResult.causes.length).toBe(1); - // expect( - // validationResult.causes.includes( - // PluginUpdateProposalInValidityCause.INVALID_PLUGIN_RELEASE, - // ), - // ).toBe(true); - // }); - // it("should return `INVALID_PLUGIN_BUILD` when the build of the update is equal or lower to the one installed", async () => { - // const ctx = new Context(contextParamsLocalChain); - // const client = new Client(ctx); - - // const mockedClient = mockedGraphqlRequest.getMockedInstance( - // client.graphql.getClient(), - // ); - // const invalidApplyUpdateActions = client.encoding.applyUpdateAction( - // daoAddress, - // { ...applyUpdateParams, versionTag: { release: 1, build: 1 } }, - // ); - // mockedClient.request.mockResolvedValueOnce({ - // dao: subgraphDao, - // }); - // mockedClient.request.mockResolvedValueOnce({ - // pluginRepo: subgraphPluginRepo, - // }); - // mockedClient.request.mockResolvedValueOnce({ - // pluginPreparation: subgraphPluginPreparation, - // }); - // mockedIPFSClient.cat.mockResolvedValueOnce(Buffer.from( - // JSON.stringify(TOKEN_VOTING_BUILD_METADATA), - // )); - // const validationResult = await client.methods - // .isPluginUpdateValid({ - // daoAddress, - // actions: invalidApplyUpdateActions, - // }); - // expect(validationResult.isValid).toBe(false); - // expect(validationResult.causes.length).toBe(1); - // expect( - // validationResult.causes.includes( - // PluginUpdateProposalInValidityCause.INVALID_PLUGIN_BUILD, - // ), - // ).toBe(true); - // }); - - // it("should return `PLUGIN_NOT_INSTALLED` when the plugin is not installed in the dao", async () => { - // const ctx = new Context(contextParamsLocalChain); - // const client = new Client(ctx); - - // const mockedClient = mockedGraphqlRequest.getMockedInstance( - // client.graphql.getClient(), - // ); - // mockedClient.request.mockResolvedValueOnce({ - // dao: { - // ...subgraphDao, - // plugins: [{ - // subdomain: SupportedPluginRepo.TOKEN_VOTING, - // appliedVersion: { build: 2, release: { release: 1 } }, - // }], - // }, - // }); - // const validationResult = await client.methods - // .isPluginUpdateValid({ - // daoAddress, - // actions: updateActions, - // }); - // expect(validationResult.isValid).toBe(false); - // expect(validationResult.causes.length).toBe(1); - // expect( - // validationResult.causes.includes( - // PluginUpdateProposalInValidityCause.PLUGIN_NOT_INSTALLED, - // ), - // ).toBe(true); - // }); - - // it("should return `NOT_ARAGON_PLUGIN_REPO` when the plugin is not an aragon plugin", async () => { - // const ctx = new Context(contextParamsLocalChain); - // const client = new Client(ctx); - - // const mockedClient = mockedGraphqlRequest.getMockedInstance( - // client.graphql.getClient(), - // ); - // mockedClient.request.mockResolvedValueOnce({ - // dao: subgraphDao, - // }); - // mockedClient.request.mockResolvedValueOnce({ - // pluginRepo: { ...subgraphPluginRepo, subdomain: "test" }, - // }); - // mockedClient.request.mockResolvedValueOnce({ - // pluginPreparation: subgraphPluginPreparation, - // }); - // mockedIPFSClient.cat.mockResolvedValueOnce(Buffer.from( - // JSON.stringify(TOKEN_VOTING_BUILD_METADATA), - // )); - // const validationResult = await client.methods - // .isPluginUpdateValid( - // { - // daoAddress, - // actions: updateActions, - // }, - // ); - // expect(validationResult.isValid).toBe(false); - // expect(validationResult.causes.length).toBe(1); - // expect( - // validationResult.causes.includes( - // PluginUpdateProposalInValidityCause.NOT_ARAGON_PLUGIN_REPO, - // ), - // ).toBe(true); - // }); - - // it("should return `MISSING_PLUGIN_REPO` when the plugin repo does not exist", async () => { - // const ctx = new Context(contextParamsLocalChain); - // const client = new Client(ctx); - - // const mockedClient = mockedGraphqlRequest.getMockedInstance( - // client.graphql.getClient(), - // ); - // mockedClient.request.mockResolvedValueOnce({ - // dao: subgraphDao, - // }); - // mockedClient.request.mockResolvedValueOnce({ - // pluginRepo: null, - // }); - // const validationResult = await client.methods - // .isPluginUpdateValid({ - // daoAddress, - // actions: updateActions, - // }); - // expect(validationResult.isValid).toBe(false); - // expect(validationResult.causes.length).toBe(1); - // expect( - // validationResult.causes.includes( - // PluginUpdateProposalInValidityCause.MISSING_PLUGIN_REPO, - // ), - // ).toBe(true); - // }); - - // it("should return `INVALID_DATA` when the initData does not match the abi in metadata", async () => { - // const ctx = new Context(contextParamsLocalChain); - // const client = new Client(ctx); - - // const mockedClient = mockedGraphqlRequest.getMockedInstance( - // client.graphql.getClient(), - // ); - // const invalidApplyUpdateActions = client.encoding.applyUpdateAction( - // daoAddress, - // { ...applyUpdateParams, initData: updateActions[0].data }, - // ); - // mockedClient.request.mockResolvedValueOnce({ - // dao: subgraphDao, - // }); - // mockedClient.request.mockResolvedValueOnce({ - // pluginRepo: subgraphPluginRepo, - // }); - // mockedClient.request.mockResolvedValueOnce({ - // pluginPreparation: subgraphPluginPreparation, - // }); - // mockedIPFSClient.cat.mockResolvedValueOnce(Buffer.from( - // JSON.stringify(TOKEN_VOTING_BUILD_METADATA), - // )); - // const validationResult = await client.methods - // .isPluginUpdateValid({ - // actions: invalidApplyUpdateActions, - // daoAddress, - // }); - // expect(validationResult.isValid).toBe(false); - // expect(validationResult.causes.length).toBe(1); - // expect( - // validationResult.causes.includes( - // PluginUpdateProposalInValidityCause.INVALID_DATA, - // ), - // ).toBe(true); - // }); - - // it("should return `INVALID_PLUGIN_REPO_METADATA` if the abi of the metadata is not available", async () => { - // const ctx = new Context(contextParamsLocalChain); - // const client = new Client(ctx); - - // const mockedClient = mockedGraphqlRequest.getMockedInstance( - // client.graphql.getClient(), - // ); - // mockedClient.request.mockResolvedValueOnce({ - // dao: subgraphDao, - // }); - // mockedClient.request.mockResolvedValueOnce({ - // pluginRepo: subgraphPluginRepo, - // }); - // mockedClient.request.mockResolvedValueOnce({ - // pluginPreparation: subgraphPluginPreparation, - // }); - - // mockedIPFSClient.cat.mockResolvedValueOnce(Buffer.from( - // JSON.stringify({ - // ...TOKEN_VOTING_BUILD_METADATA, - // pluginSetup: { - // ...TOKEN_VOTING_BUILD_METADATA.pluginSetup, - // prepareUpdate: {}, - // }, - // }), - // )); - // const validationResult = await client.methods - // .isPluginUpdateValid({ - // daoAddress, - // actions: updateActions, - // }); - // expect(validationResult.isValid).toBe(false); - // expect(validationResult.causes.length).toBe(1); - // expect( - // validationResult.causes.includes( - // PluginUpdateProposalInValidityCause.INVALID_PLUGIN_REPO_METADATA, - // ), - // ).toBe(true); - // }); - // it("should return `MISSING_PLUGIN_PREPARATION` if the preparation does not exist", async () => { - // const ctx = new Context(contextParamsLocalChain); - // const client = new Client(ctx); - - // const mockedClient = mockedGraphqlRequest.getMockedInstance( - // client.graphql.getClient(), - // ); - // mockedClient.request.mockResolvedValueOnce({ - // dao: subgraphDao, - // }); - // mockedClient.request.mockResolvedValueOnce({ - // pluginRepo: subgraphPluginRepo, - // }); - // mockedClient.request.mockResolvedValueOnce({ - // pluginPreparation: null, - // }); - - // const validationResult = await client.methods - // .isPluginUpdateValid({ - // actions: updateActions, - // daoAddress, - // }); - // expect(validationResult.isValid).toBe(false); - // expect(validationResult.causes.length).toBe(1); - // expect( - // validationResult.causes.includes( - // PluginUpdateProposalInValidityCause.MISSING_PLUGIN_PREPARATION, - // ), - // ).toBe(true); - // }); - // it("should pass and the `cause` array be empty", async () => { - // const ctx = new Context(contextParamsLocalChain); - // const client = new Client(ctx); - - // const mockedClient = mockedGraphqlRequest.getMockedInstance( - // client.graphql.getClient(), - // ); - // mockedClient.request.mockResolvedValueOnce({ - // dao: subgraphDao, - // }); - // mockedClient.request.mockResolvedValueOnce({ - // pluginRepo: subgraphPluginRepo, - // }); - // mockedClient.request.mockResolvedValueOnce({ - // pluginPreparation: subgraphPluginPreparation, - // }); - - // mockedIPFSClient.cat.mockResolvedValueOnce(Buffer.from( - // JSON.stringify(TOKEN_VOTING_BUILD_METADATA), - // )); - - // const validationResult = await client.methods - // .isPluginUpdateValid({ - // daoAddress, - // actions: updateActions, - // }); - // expect(validationResult.isValid).toBe(true); - // expect(validationResult.causes.length).toBe(0); - // }); - // }); - // describe("isDaoUpdateValid", () => { - // let upgradeToAndCallAction: DaoAction; - // let upgradeToAndCallParams: UpgradeToAndCallParams; - // let initializeFromParams: InitializeFromParams; + }); + describe("isDaoUpdateValid", () => { + // it("Should return true for a valid update", async () => { + // const context = new Context(); + // const client = new Client(context); // let initializeFromAction: DaoAction; - // let implementationAddress: string; - // beforeAll(async () => { - // const ctx = new Context(contextParamsLocalChain); - // const client = new Client(ctx); - // initializeFromParams = { + // let upgradeToAndCallAction: DaoAction; + // initializeFromAction = client.encoding.initializeFromAction( + // daoAddress, + // { + // initData: new Uint8Array(), // previousVersion: [1, 0, 0], - // }; - // initializeFromAction = client.encoding.initializeFromAction( - // daoAddressV1, - // initializeFromParams, - // ); - // implementationAddress = await client.methods.getDaoImplementation( - // deployment.daoFactory.address, - // ); - // upgradeToAndCallParams = { - // implementationAddress, + // }, + // ); + // upgradeToAndCallAction = client.encoding.upgradeToAndCallAction( + // daoAddressV1, + // { // data: initializeFromAction.data, - // }; - // upgradeToAndCallAction = client.encoding.upgradeToAndCallAction( - // daoAddressV1, - // upgradeToAndCallParams, - // ); - // }); - // it("should return `INVALID_ACTIONS` when the action is not an upgradeToAndCall", async () => { - // const ctx = new Context(contextParamsLocalChain); - // const client = new Client(ctx); - - // const invalidAction = client.encoding.upgradeToAction( - // daoAddressV1, - // ADDRESS_ONE, - // ); - - // const validationResult = await client.methods - // .isDaoUpdateValid({ - // actions: [invalidAction], - // daoAddress: daoAddressV1, - // }); - // expect(validationResult.isValid).toBe(false); - // expect(validationResult.causes.length).toBe(1); - // expect( - // validationResult.causes.includes( - // DaoUpdateProposalInvalidityCause.INVALID_ACTIONS, - // ), - // ).toBe(true); - // }); - // it("should return `INVALID_ACTIONS` when the call data is not an encoded initializeFrom", async () => { - // const ctx = new Context(contextParamsLocalChain); - // const client = new Client(ctx); - // const invalidAction = client.encoding.upgradeToAction( - // daoAddressV1, - // ADDRESS_ONE, - // ); - // const upgradeToAndCallAction = client.encoding.upgradeToAndCallAction( - // daoAddressV1, - // { - // implementationAddress, - // data: invalidAction.data, - // }, - // ); - // const validationResult = await client.methods - // .isDaoUpdateValid({ - // actions: [upgradeToAndCallAction], - // daoAddress: daoAddressV1, - // }); - // expect(validationResult.isValid).toBe(false); - // expect(validationResult.causes.length).toBe(1); - // expect( - // validationResult.causes.includes( - // DaoUpdateProposalInvalidityCause.INVALID_ACTIONS, - // ), - // ).toBe(true); - // }); - // it("should return `INVALID_VERSION` when the specified previous version is different from the real currentVersion", async () => { - // const ctx = new Context(contextParamsLocalChain); - // const client = new Client(ctx); - - // const invalidAction = client.encoding.initializeFromAction( - // daoAddressV1, - // { - // previousVersion: [1, 3, 0], - // }, - // ); - // const upgradeToAndCallAction = client.encoding.upgradeToAndCallAction( - // daoAddressV1, - // { - // implementationAddress, - // data: invalidAction.data, - // }, - // ); - - // const validationResult = await client.methods - // .isDaoUpdateValid({ - // actions: [upgradeToAndCallAction], - // daoAddress: daoAddressV1, - // }); - // expect(validationResult.isValid).toBe(false); - // expect(validationResult.causes.length).toBe(1); - // expect( - // validationResult.causes.includes( - // DaoUpdateProposalInvalidityCause.INVALID_VERSION, - // ), - // ).toBe(true); - // }); - // it("should return `INVALID_IMPLEMENTATION` when the implementation address is not correct", async () => { - // const ctx = new Context(contextParamsLocalChain); - // const client = new Client(ctx); - - // const upgradeToAndCallAction = client.encoding.upgradeToAndCallAction( - // daoAddressV1, - // { - // implementationAddress: "0x1234567890123456789012345678901234567890", - // data: initializeFromAction.data, - // }, - // ); - // const validationResult = await client.methods - // .isDaoUpdateValid({ - // actions: [upgradeToAndCallAction], - // daoAddress: daoAddressV1, - // }); - // expect(validationResult.isValid).toBe(false); - // expect(validationResult.causes.length).toBe(1); - // expect( - // validationResult.causes.includes( - // DaoUpdateProposalInvalidityCause.INVALID_IMPLEMENTATION, - // ), - // ).toBe(true); - // }); - // it("should return `INVALID_INIT_DATA` when the init data is not empty", async () => { - // const ctx = new Context(contextParamsLocalChain); - // const client = new Client(ctx); - // const invalidAction = client.encoding.initializeFromAction( - // daoAddressV1, - // { - // previousVersion: [1, 0, 0], - // initData: new Uint8Array([10, 20, 30, 40, 50]), - // }, - // ); - // const upgradeToAndCallAction = client.encoding.upgradeToAndCallAction( - // daoAddressV1, - // { - // implementationAddress, - // data: invalidAction.data, + // implementationAddress: "0x1234567890123456789012345678901234567890", + // }, + // ); + // const mockedClient = mockedGraphqlRequest.getMockedInstance( + // client.graphql.getClient(), + // ); + // mockedClient.request.mockResolvedValueOnce({ + // iproposal: { + // dao: { + // id: daoAddress, // }, - // ); - // const validationResult = await client.methods - // .isDaoUpdateValid({ - // actions: [upgradeToAndCallAction], - // daoAddress: daoAddressV1, - // }); - // expect(validationResult.isValid).toBe(false); - // expect(validationResult.causes.length).toBe(1); - // expect( - // validationResult.causes.includes( - // DaoUpdateProposalInvalidityCause.INVALID_INIT_DATA, - // ), - // ).toBe(true); - // }); - // it("should valid and not return anything in the cause", async () => { - // const ctx = new Context(contextParamsLocalChain); - // const client = new Client(ctx); - - // const validationResult = await client.methods - // .isDaoUpdateValid({ - // actions: [upgradeToAndCallAction], - // daoAddress: daoAddressV1, - // }); - // expect(validationResult.isValid).toBe(true); - // expect(validationResult.causes.length).toBe(0); + // allowFailureMap: "0", + // actions: toSubgraphActions([upgradeToAndCallAction]), + // }, // }); + // const res = await client.methods.isDaoUpdateValid( + // TEST_MULTISIG_PROPOSAL_ID, + // ); + // expect(res.isValid).toBe(true); + // expect(res.causes).toMatchObject([]); + // }); }); }); }); diff --git a/modules/client/test/unit/client/utils.test.ts b/modules/client/test/unit/client/utils.test.ts index 0ef26a8d7..9f839a339 100644 --- a/modules/client/test/unit/client/utils.test.ts +++ b/modules/client/test/unit/client/utils.test.ts @@ -4,9 +4,17 @@ import { mockedIPFSClient } from "../../mocks/aragon-sdk-ipfs"; import { ApplyUpdateParams, Context, + DaoAction, + MultiTargetPermission, + PermissionIds, Permissions, + TokenType, } from "@aragon/sdk-client-common"; -import { Client, PluginUpdateProposalInValidityCause } from "../../../src"; +import { + Client, + DaoUpdateProposalInvalidityCause, + PluginUpdateProposalInValidityCause, +} from "../../../src"; // import * as deployV1Contracts from "../../helpers/deploy-v1-contracts"; import { @@ -18,13 +26,19 @@ import { TOKEN_VOTING_BUILD_METADATA, } from "../../integration/constants"; import { + isDaoUpdateAction, + isPluginUpdateAction, + isPluginUpdateActionWithRootPermission, validateApplyUpdateFunction, validateGrantRootPermissionAction, validateGrantUpdatePluginPermissionAction, validateRevokeRootPermissionAction, validateRevokeUpdatePluginPermissionAction, + validateUpdateDaoProposalActions, + validateUpdatePluginProposalActions, } from "../../../src/internal/utils"; import { + ProposalActionTypes, SubgraphDao, SubgraphPluginRepo, SubgraphPluginUpdatePreparation, @@ -32,26 +46,45 @@ import { import { SupportedPluginRepo } from "../../../src/internal/constants"; describe("Test client utils", () => { + const pspAddress = ADDRESS_TWO; + const context = new Context({ pluginSetupProcessorAddress: pspAddress }); + const client = new Client(context); + const pluginAddress = ADDRESS_ONE; + const daoAddress = ADDRESS_THREE; + const pluginRepo = ADDRESS_FOUR; + const tokenVotingRepoAddress = ADDRESS_ONE; + let applyUpdateParams: ApplyUpdateParams; + let subgraphDao: SubgraphDao; + let subgraphPluginRepo: SubgraphPluginRepo; + let subgraphPluginPreparation: SubgraphPluginUpdatePreparation; + let permission: MultiTargetPermission; + const mockedClient = mockedGraphqlRequest.getMockedInstance( + client.graphql.getClient(), + ); describe("validateGrantUpdatePluginPermissionAction", () => { - const context = new Context(); - const client = new Client(context); - const pluginAddress = ADDRESS_ONE; - const pspAddress = ADDRESS_TWO; - const daoAddress = ADDRESS_THREE; - it("should return an empty array for a valid action", () => { + beforeEach(() => { + mockedClient.request.mockReset(); + mockedClient.request.mockResolvedValue({ + pluginInstallations: [{ + id: ADDRESS_ONE, + }], + }); + }); + it("should return an empty array for a valid action", async () => { const grantAction = client.encoding.grantAction(daoAddress, { where: pluginAddress, who: pspAddress, permission: Permissions.UPGRADE_PLUGIN_PERMISSION, }); - const result = validateGrantUpdatePluginPermissionAction( + const result = await validateGrantUpdatePluginPermissionAction( grantAction, - pluginAddress, pspAddress, + daoAddress, + client.graphql, ); expect(result).toEqual([]); }); - it("should return an error if the action is not a grant action", () => { + it("should return an error if the action is not a grant action", async () => { expect(() => validateGrantUpdatePluginPermissionAction( { @@ -59,70 +92,78 @@ describe("Test client utils", () => { value: BigInt(0), data: new Uint8Array(), }, - pluginAddress, pspAddress, + daoAddress, + client.graphql, ) - ).toThrow(); + ).rejects.toThrow(); }); - it("should return an error value of the action is not 0", () => { + it("should return an error value of the action is not 0", async () => { const grantAction = client.encoding.grantAction(daoAddress, { where: pluginAddress, who: pspAddress, permission: Permissions.UPGRADE_PLUGIN_PERMISSION, }); grantAction.value = BigInt(10); - const result = validateGrantUpdatePluginPermissionAction( + const result = await validateGrantUpdatePluginPermissionAction( grantAction, - pluginAddress, pspAddress, + daoAddress, + client.graphql, ); expect(result).toEqual([ PluginUpdateProposalInValidityCause .INVALID_GRANT_UPDATE_PERMISSION_VALUE, ]); }); - it("should return an error if the action permission is note granted in the plugin", () => { + it("should return an error if the plugin does not exist", async () => { const grantAction = client.encoding.grantAction(daoAddress, { where: daoAddress, who: pspAddress, permission: Permissions.UPGRADE_PLUGIN_PERMISSION, }); - const result = validateGrantUpdatePluginPermissionAction( + mockedClient.request.mockResolvedValueOnce({ + pluginInstallations: [], + }); + const result = await validateGrantUpdatePluginPermissionAction( grantAction, - pluginAddress, pspAddress, + daoAddress, + client.graphql, ); expect(result).toEqual([ PluginUpdateProposalInValidityCause .INVALID_GRANT_UPDATE_PERMISSION_WHERE_ADDRESS, ]); }); - it("should return an error if the permission is not granted to the psp", () => { + it("should return an error if the permission is not granted to the psp", async () => { const grantAction = client.encoding.grantAction(daoAddress, { where: pluginAddress, who: daoAddress, permission: Permissions.UPGRADE_PLUGIN_PERMISSION, }); - const result = validateGrantUpdatePluginPermissionAction( + const result = await validateGrantUpdatePluginPermissionAction( grantAction, - pluginAddress, pspAddress, + daoAddress, + client.graphql, ); expect(result).toEqual([ PluginUpdateProposalInValidityCause .INVALID_GRANT_UPDATE_PERMISSION_WHO_ADDRESS, ]); }); - it("should return an error if the permission is not correct", () => { + it("should return an error if the permission is not correct", async () => { const grantAction = client.encoding.grantAction(daoAddress, { where: pluginAddress, who: pspAddress, permission: Permissions.MINT_PERMISSION, }); - const result = validateGrantUpdatePluginPermissionAction( + const result = await validateGrantUpdatePluginPermissionAction( grantAction, - pluginAddress, pspAddress, + daoAddress, + client.graphql, ); expect(result).toEqual([ PluginUpdateProposalInValidityCause @@ -131,16 +172,20 @@ describe("Test client utils", () => { .INVALID_GRANT_UPDATE_PERMISSION_PERMISSION_ID, ]); }); - it("should return two causes if the permission is not granted to the psp and is not granted in the plugin", () => { + it("should return two causes if the permission is not granted to the psp and the plugin does not exist", async () => { const grantAction = client.encoding.grantAction(daoAddress, { - where: daoAddress, + where: pluginAddress, who: daoAddress, permission: Permissions.UPGRADE_PLUGIN_PERMISSION, }); - const result = validateGrantUpdatePluginPermissionAction( + mockedClient.request.mockResolvedValueOnce({ + pluginInstallations: [], + }); + const result = await validateGrantUpdatePluginPermissionAction( grantAction, - pluginAddress, pspAddress, + daoAddress, + client.graphql, ); expect(result).toEqual([ PluginUpdateProposalInValidityCause @@ -151,25 +196,29 @@ describe("Test client utils", () => { }); }); describe("validateRevokeUpdatePluginPermissionAction", () => { - const context = new Context(); - const client = new Client(context); - const pluginAddress = ADDRESS_ONE; - const pspAddress = ADDRESS_TWO; - const daoAddress = ADDRESS_THREE; - it("should return an empty array for a valid action", () => { + beforeEach(() => { + mockedClient.request.mockReset(); + mockedClient.request.mockResolvedValue({ + pluginInstallations: [{ + id: ADDRESS_ONE, + }], + }); + }); + it("should return an empty array for a valid action", async () => { const revokeAction = client.encoding.revokeAction(daoAddress, { where: pluginAddress, who: pspAddress, permission: Permissions.UPGRADE_PLUGIN_PERMISSION, }); - const result = validateRevokeUpdatePluginPermissionAction( + const result = await validateRevokeUpdatePluginPermissionAction( revokeAction, - pluginAddress, pspAddress, + daoAddress, + client.graphql, ); expect(result).toEqual([]); }); - it("should return an error if the action is not a revoke action", () => { + it("should return an error if the action is not a revoke action", async () => { expect(() => validateRevokeUpdatePluginPermissionAction( { @@ -177,70 +226,78 @@ describe("Test client utils", () => { value: BigInt(0), data: new Uint8Array(), }, - pluginAddress, pspAddress, + daoAddress, + client.graphql, ) - ).toThrow(); + ).rejects.toThrow(); }); - it("should return an error value of the action is not 0", () => { + it("should return an error value of the action is not 0", async () => { const revokeAction = client.encoding.revokeAction(daoAddress, { where: pluginAddress, who: pspAddress, permission: Permissions.UPGRADE_PLUGIN_PERMISSION, }); revokeAction.value = BigInt(10); - const result = validateRevokeUpdatePluginPermissionAction( + const result = await validateRevokeUpdatePluginPermissionAction( revokeAction, - pluginAddress, pspAddress, + daoAddress, + client.graphql, ); expect(result).toEqual([ PluginUpdateProposalInValidityCause .INVALID_REVOKE_UPDATE_PERMISSION_VALUE, ]); }); - it("should return an error if the permission is not revoked in the plugin", () => { + it("should return an error if the installation doees not exist", async () => { const revokeAction = client.encoding.revokeAction(daoAddress, { where: daoAddress, who: pspAddress, permission: Permissions.UPGRADE_PLUGIN_PERMISSION, }); - const result = validateRevokeUpdatePluginPermissionAction( + mockedClient.request.mockResolvedValueOnce({ + pluginInstallations: [], + }); + const result = await validateRevokeUpdatePluginPermissionAction( revokeAction, - pluginAddress, pspAddress, + daoAddress, + client.graphql, ); expect(result).toEqual([ PluginUpdateProposalInValidityCause .INVALID_REVOKE_UPDATE_PERMISSION_WHERE_ADDRESS, ]); }); - it("should return an error if the is not revoked from the psp", () => { + it("should return an error if the is not revoked from the psp", async () => { const revokeAction = client.encoding.revokeAction(daoAddress, { where: pluginAddress, who: daoAddress, permission: Permissions.UPGRADE_PLUGIN_PERMISSION, }); - const result = validateRevokeUpdatePluginPermissionAction( + const result = await validateRevokeUpdatePluginPermissionAction( revokeAction, - pluginAddress, pspAddress, + daoAddress, + client.graphql, ); expect(result).toEqual([ PluginUpdateProposalInValidityCause .INVALID_REVOKE_UPDATE_PERMISSION_WHO_ADDRESS, ]); }); - it("should return an error if the permission is not correct", () => { + it("should return an error if the permission is not correct", async () => { const revokeAction = client.encoding.revokeAction(daoAddress, { where: pluginAddress, who: pspAddress, permission: Permissions.MINT_PERMISSION, }); - const result = validateRevokeUpdatePluginPermissionAction( + const result = await validateRevokeUpdatePluginPermissionAction( revokeAction, - pluginAddress, pspAddress, + daoAddress, + client.graphql, ); expect(result).toEqual([ PluginUpdateProposalInValidityCause @@ -249,16 +306,20 @@ describe("Test client utils", () => { .INVALID_REVOKE_UPDATE_PERMISSION_PERMISSION_ID, ]); }); - it("should return two causes if the permission is not granted to the psp and is not granted in the plugin", () => { + it("should return two causes if the permission is not granted to the psp and the plugin installation does not exist", async () => { const revokeAction = client.encoding.revokeAction(daoAddress, { where: daoAddress, who: daoAddress, permission: Permissions.UPGRADE_PLUGIN_PERMISSION, }); - const result = validateRevokeUpdatePluginPermissionAction( + mockedClient.request.mockResolvedValueOnce({ + pluginInstallations: [], + }); + const result = await validateRevokeUpdatePluginPermissionAction( revokeAction, - pluginAddress, pspAddress, + daoAddress, + client.graphql, ); expect(result).toEqual([ PluginUpdateProposalInValidityCause @@ -269,11 +330,9 @@ describe("Test client utils", () => { }); }); describe("validateGrantRootPermissionAction", () => { - const context = new Context(); - const client = new Client(context); - const pluginAddress = ADDRESS_ONE; - const pspAddress = ADDRESS_TWO; - const daoAddress = ADDRESS_THREE; + beforeEach(() => { + mockedClient.request.mockReset(); + }); it("should return an empty array for a valid action", () => { const revokeAction = client.encoding.grantAction(daoAddress, { where: daoAddress, @@ -295,8 +354,8 @@ describe("Test client utils", () => { value: BigInt(0), data: new Uint8Array(), }, - daoAddress, pspAddress, + daoAddress, ) ).toThrow(); }); @@ -387,11 +446,9 @@ describe("Test client utils", () => { }); }); describe("validateRevokeRootPermissionAction", () => { - const context = new Context(); - const client = new Client(context); - const pluginAddress = ADDRESS_ONE; - const pspAddress = ADDRESS_TWO; - const daoAddress = ADDRESS_THREE; + beforeEach(() => { + mockedClient.request.mockReset(); + }); it("should return an empty array for a valid action", () => { const revokeAction = client.encoding.revokeAction(daoAddress, { where: pluginAddress, @@ -413,8 +470,8 @@ describe("Test client utils", () => { value: BigInt(0), data: new Uint8Array(), }, - pluginAddress, pspAddress, + daoAddress, ) ).toThrow(); }); @@ -505,20 +562,9 @@ describe("Test client utils", () => { }); }); describe("validateApplyUpdateFunction", () => { - const context = new Context(); - const client = new Client(context); - const mockedClient = mockedGraphqlRequest.getMockedInstance( - client.graphql.getClient(), - ); - const pluginAddress = ADDRESS_ONE; - // const pspAddress = ADDRESS_TWO; - const daoAddress = ADDRESS_THREE; - const pluginRepo = ADDRESS_FOUR; - const tokenVotingRepoAddress = ADDRESS_ONE; - let applyUpdateParams: ApplyUpdateParams; - let subgraphDao: SubgraphDao; - let subgraphPluginRepo: SubgraphPluginRepo; - let subgraphPluginPreparation: SubgraphPluginUpdatePreparation; + beforeEach(() => { + mockedClient.request.mockReset(); + }); beforeAll(() => { applyUpdateParams = { versionTag: { @@ -863,4 +909,571 @@ describe("Test client utils", () => { ]); }); }); + describe("validateUpdatePluginProposalActions", () => { + beforeEach(() => { + mockedClient.request.mockReset(); + }); + beforeAll(() => { + subgraphPluginPreparation = { + data: "0x", + }; + + applyUpdateParams = { + versionTag: { + build: 2, + release: 1, + }, + initData: new Uint8Array(), + pluginRepo: ADDRESS_ONE, + pluginAddress: ADDRESS_ONE, + permissions: [], + helpers: [], + }; + + subgraphDao = { + id: daoAddress, + subdomain: "test-tokenvoting-dao", + metadata: `ipfs://${IPFS_CID}`, + createdAt: "1234567890", + plugins: [{ + appliedPreparation: { + pluginAddress: pluginAddress, + }, + appliedPluginRepo: { + subdomain: SupportedPluginRepo.TOKEN_VOTING, + }, + appliedVersion: { + build: 1, + release: { + release: 1, + }, + }, + }], + }; + + subgraphPluginRepo = { + id: ADDRESS_ONE, + subdomain: SupportedPluginRepo.TOKEN_VOTING, + releases: [ + { + release: 1, + metadata: `ipfs://${IPFS_CID}`, + builds: [ + { + build: 1, + metadata: `ipfs://${IPFS_CID}`, + }, + { + build: 2, + metadata: `ipfs://${IPFS_CID}`, + }, + ], + }, + ], + }; + permission = { + who: ADDRESS_ONE, + where: ADDRESS_TWO, + permissionId: PermissionIds.MINT_PERMISSION_ID, + operation: 1, + }; + }); + it("should return an empty array for a valid actions", async () => { + const actions = client.encoding.applyUpdateAction( + daoAddress, + applyUpdateParams, + ); + mockedClient.request.mockResolvedValueOnce({ + pluginInstallations: [{ + id: ADDRESS_ONE, + }], + }); + mockedClient.request.mockResolvedValueOnce({ + dao: subgraphDao, + }); + mockedClient.request.mockResolvedValueOnce({ + pluginRepo: subgraphPluginRepo, + }); + mockedClient.request.mockResolvedValueOnce({ + pluginPreparation: subgraphPluginPreparation, + }); + mockedClient.request.mockResolvedValueOnce({ + pluginInstallations: [{ + id: ADDRESS_ONE, + }], + }); + const result = await validateUpdatePluginProposalActions( + actions, + daoAddress, + pspAddress, + client.graphql, + client.ipfs, + ); + expect(result.isValid).toEqual(true); + expect(result.causes).toMatchObject([[]]); + }); + it("should return an empty array for a valid actions where root is granted", async () => { + const actions = client.encoding.applyUpdateAction( + daoAddress, + { + ...applyUpdateParams, + permissions: [permission], + }, + ); + mockedClient.request.mockResolvedValueOnce({ + pluginInstallations: [{ + id: ADDRESS_ONE, + }], + }); + mockedClient.request.mockResolvedValueOnce({ + dao: subgraphDao, + }); + mockedClient.request.mockResolvedValueOnce({ + pluginRepo: subgraphPluginRepo, + }); + mockedClient.request.mockResolvedValueOnce({ + pluginPreparation: subgraphPluginPreparation, + }); + mockedClient.request.mockResolvedValueOnce({ + pluginInstallations: [{ + id: ADDRESS_ONE, + }], + }); + const result = await validateUpdatePluginProposalActions( + actions, + daoAddress, + pspAddress, + client.graphql, + client.ipfs, + ); + expect(result.isValid).toEqual(true); + expect(result.causes).toMatchObject([[]]); + }); + it("should return an empty for two groups of apply update", async () => { + const actionsGroupOne = client.encoding.applyUpdateAction( + daoAddress, + applyUpdateParams, + ); + const actionsGroupTwo = client.encoding.applyUpdateAction( + daoAddress, + { + ...applyUpdateParams, + permissions: [permission], + }, + ); + // setup mocks + for (let i = 0; i < 3; i++) { + mockedClient.request.mockResolvedValueOnce({ + pluginInstallations: [{ + id: ADDRESS_ONE, + }], + }); + mockedClient.request.mockResolvedValueOnce({ + dao: subgraphDao, + }); + mockedClient.request.mockResolvedValueOnce({ + pluginRepo: subgraphPluginRepo, + }); + mockedClient.request.mockResolvedValueOnce({ + pluginPreparation: subgraphPluginPreparation, + }); + mockedClient.request.mockResolvedValueOnce({ + pluginInstallations: [{ + id: ADDRESS_ONE, + }], + }); + } + + const result = await validateUpdatePluginProposalActions( + [...actionsGroupOne, ...actionsGroupTwo], + daoAddress, + pspAddress, + client.graphql, + client.ipfs, + ); + expect(result.isValid).toEqual(true); + expect(result.causes).toMatchObject([[], []]); + }); + it("should return an INVALID_ACTIONS when the actions don't match the expected pattern", async () => { + const actions = await client.encoding.withdrawAction({ + amount: BigInt(0), + type: TokenType.NATIVE, + recipientAddressOrEns: ADDRESS_ONE, + }); + + const result = await validateUpdatePluginProposalActions( + [actions], + daoAddress, + pspAddress, + client.graphql, + client.ipfs, + ); + expect(result.isValid).toEqual(false); + expect(result.causes).toMatchObject([[ + PluginUpdateProposalInValidityCause.INVALID_ACTIONS, + ]]); + }); + }); + describe("validateUpdateDaoProposalActions", () => { + let currentDaoVersion: [number, number, number]; + let upgradeToAndCallAction: DaoAction; + let upgradeToAction: DaoAction; + let initializeFromAction: DaoAction; + const implementationAddress = ADDRESS_TWO; + beforeEach(() => { + mockedClient.request.mockReset(); + currentDaoVersion = [1, 0, 0]; + initializeFromAction = client.encoding.initializeFromAction(daoAddress, { + previousVersion: currentDaoVersion, + initData: new Uint8Array(), + }); + upgradeToAndCallAction = client.encoding.upgradeToAndCallAction( + daoAddress, + { + implementationAddress, + data: initializeFromAction.data, + }, + ); + upgradeToAction = client.encoding.upgradeToAction( + daoAddress, + implementationAddress, + ); + }); + it("should return an empty array for a valid upgradeToAndCall action", () => { + const result = validateUpdateDaoProposalActions( + [upgradeToAndCallAction], + daoAddress, + pspAddress, + currentDaoVersion, + ); + expect(result.isValid).toEqual(true); + expect(result.causes).toMatchObject([]); + }); + it("should return an empty array for a valid upgradeTo action", () => { + const result = validateUpdateDaoProposalActions( + [upgradeToAction], + daoAddress, + pspAddress, + currentDaoVersion, + ); + expect(result.isValid).toEqual(true); + expect(result.causes).toMatchObject([]); + }); + it("should return INVALID_ACTIONS when the actions are not valid for updating a dao", async () => { + const withdrawAction = await client.encoding.withdrawAction({ + amount: BigInt(10), + type: TokenType.NATIVE, + recipientAddressOrEns: ADDRESS_ONE, + }); + const result = validateUpdateDaoProposalActions( + [withdrawAction], + daoAddress, + pspAddress, + currentDaoVersion, + ); + expect(result.isValid).toEqual(false); + expect(result.causes).toMatchObject([ + DaoUpdateProposalInvalidityCause.INVALID_ACTIONS, + ]); + }); + it("should return INVALID_TO_ADDRESS when the to address is not the dao address", () => { + upgradeToAndCallAction.to = ADDRESS_FOUR; + const result = validateUpdateDaoProposalActions( + [upgradeToAndCallAction], + daoAddress, + pspAddress, + currentDaoVersion, + ); + expect(result.isValid).toEqual(false); + expect(result.causes).toMatchObject([ + DaoUpdateProposalInvalidityCause.INVALID_TO_ADDRESS, + ]); + }); + it("should return INVALID_VALUE when the the value of the action is not 0", () => { + upgradeToAndCallAction.value = BigInt(10); + const result = validateUpdateDaoProposalActions( + [upgradeToAndCallAction], + daoAddress, + pspAddress, + currentDaoVersion, + ); + expect(result.isValid).toEqual(false); + expect(result.causes).toMatchObject([ + DaoUpdateProposalInvalidityCause.INVALID_VALUE, + ]); + }); + it("should return INVALID_UPGRADE_TO_IMPLEMENTATION_ADDRESS when the implementation address is not the correct one", () => { + upgradeToAction = client.encoding.upgradeToAction( + daoAddress, + daoAddress, + ); + const result = validateUpdateDaoProposalActions( + [upgradeToAction], + daoAddress, + pspAddress, + currentDaoVersion, + ); + expect(result.isValid).toEqual(false); + expect(result.causes).toMatchObject([ + DaoUpdateProposalInvalidityCause + .INVALID_UPGRADE_TO_IMPLEMENTATION_ADDRESS, + ]); + }); + it("should return INVALID_UPGRADE_TO_AND_CALL_IMPLEMENTATION_ADDRESS when the implementation address is not the correct one", () => { + upgradeToAndCallAction = client.encoding.upgradeToAndCallAction( + daoAddress, + { + implementationAddress: daoAddress, + data: initializeFromAction.data, + }, + ); + const result = validateUpdateDaoProposalActions( + [upgradeToAndCallAction], + daoAddress, + pspAddress, + currentDaoVersion, + ); + expect(result.isValid).toEqual(false); + expect(result.causes).toMatchObject([ + DaoUpdateProposalInvalidityCause + .INVALID_UPGRADE_TO_AND_CALL_IMPLEMENTATION_ADDRESS, + ]); + }); + it("should return INVALID_UPGRADE_TO_AND_CALL_VERSION when the version in the encoded initializeFrom action is not the correct one", () => { + initializeFromAction = client.encoding.initializeFromAction( + daoAddress, + { + previousVersion: [1, 3, 0], + initData: new Uint8Array(), + }, + ); + upgradeToAndCallAction = client.encoding.upgradeToAndCallAction( + daoAddress, + { + implementationAddress, + data: initializeFromAction.data, + }, + ); + const result = validateUpdateDaoProposalActions( + [upgradeToAndCallAction], + daoAddress, + pspAddress, + currentDaoVersion, + ); + expect(result.isValid).toEqual(false); + expect(result.causes).toMatchObject([ + DaoUpdateProposalInvalidityCause + .INVALID_UPGRADE_TO_AND_CALL_VERSION, + ]); + }); + it("should return INVALID_UPGRADE_TO_AND_CALL_DATA when the data in the encoded initializeFrom action is not empty", () => { + initializeFromAction = client.encoding.initializeFromAction( + daoAddress, + { + previousVersion: currentDaoVersion, + initData: new Uint8Array([0, 10, 20, 30]), + }, + ); + upgradeToAndCallAction = client.encoding.upgradeToAndCallAction( + daoAddress, + { + implementationAddress, + data: initializeFromAction.data, + }, + ); + const result = validateUpdateDaoProposalActions( + [upgradeToAndCallAction], + daoAddress, + pspAddress, + currentDaoVersion, + ); + expect(result.isValid).toEqual(false); + expect(result.causes).toMatchObject([ + DaoUpdateProposalInvalidityCause + .INVALID_UPGRADE_TO_AND_CALL_DATA, + ]); + }); + }); + describe("isDaoUpdateAction", () => { + it("should return the expected output given a specific input", () => { + const cases = [ + { input: [ProposalActionTypes.UPGRADE_TO], expected: true }, + { input: [ProposalActionTypes.UPGRADE_TO_AND_CALL], expected: true }, + { + input: [ + ProposalActionTypes.UPGRADE_TO, + ProposalActionTypes.GRANT_PLUGIN_UPDATE_PERMISSION, + ProposalActionTypes.APPLY_UPDATE, + ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION, + ], + expected: true, + }, + { + input: [ + ProposalActionTypes.UPGRADE_TO_AND_CALL, + ProposalActionTypes.GRANT_PLUGIN_UPDATE_PERMISSION, + ProposalActionTypes.APPLY_UPDATE, + ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION, + ], + expected: true, + }, + { + input: [ + ProposalActionTypes.GRANT_PLUGIN_UPDATE_PERMISSION, + ProposalActionTypes.APPLY_UPDATE, + ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION, + ], + expected: false, + }, + { + input: [ + ProposalActionTypes.GRANT_PLUGIN_UPDATE_PERMISSION, + ProposalActionTypes.APPLY_UPDATE, + ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION, + ProposalActionTypes.UPGRADE_TO, + ], + expected: false, + }, + { + input: [ + ProposalActionTypes.GRANT_PLUGIN_UPDATE_PERMISSION, + ProposalActionTypes.APPLY_UPDATE, + ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION, + ProposalActionTypes.UPGRADE_TO_AND_CALL, + ], + expected: false, + }, + ]; + for (const { input, expected } of cases) { + const result = isDaoUpdateAction(input); + expect(result).toEqual(expected); + } + }); + }); + describe("isPluginUpdateAction", () => { + it("should return the expected output given a specific input", () => { + const cases = [ + { input: [ProposalActionTypes.UPGRADE_TO], expected: false }, + { input: [ProposalActionTypes.UPGRADE_TO_AND_CALL], expected: false }, + { + input: [ + ProposalActionTypes.UPGRADE_TO, + ProposalActionTypes.GRANT_PLUGIN_UPDATE_PERMISSION, + ProposalActionTypes.APPLY_UPDATE, + ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION, + ], + expected: true, + }, + { + input: [ + ProposalActionTypes.UPGRADE_TO_AND_CALL, + ProposalActionTypes.GRANT_PLUGIN_UPDATE_PERMISSION, + ProposalActionTypes.APPLY_UPDATE, + ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION, + ], + expected: true, + }, + { + input: [ + ProposalActionTypes.GRANT_PLUGIN_UPDATE_PERMISSION, + ProposalActionTypes.APPLY_UPDATE, + ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION, + ], + expected: true, + }, + { + input: [ + ProposalActionTypes.GRANT_PLUGIN_UPDATE_PERMISSION, + ProposalActionTypes.APPLY_UPDATE, + ], + expected: false, + }, + { + input: [ + ProposalActionTypes.UPGRADE_TO_AND_CALL, + ProposalActionTypes.GRANT_PLUGIN_UPDATE_PERMISSION, + ProposalActionTypes.APPLY_UPDATE, + ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION, + ProposalActionTypes.GRANT_PLUGIN_UPDATE_PERMISSION, + ProposalActionTypes.APPLY_UPDATE, + ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION, + ], + expected: true, + }, + ]; + for (const { input, expected } of cases) { + const result = isPluginUpdateAction(input); + expect(result).toEqual(expected); + } + }); + }); + describe("isPluginUpdateWithRootAction", () => { + it("should return the expected output given a specific input", () => { + const cases = [ + { input: [ProposalActionTypes.UPGRADE_TO], expected: false }, + { input: [ProposalActionTypes.UPGRADE_TO_AND_CALL], expected: false }, + { + input: [ + ProposalActionTypes.UPGRADE_TO, + ProposalActionTypes.GRANT_PLUGIN_UPDATE_PERMISSION, + ProposalActionTypes.GRANT_ROOT_PERMISSION, + ProposalActionTypes.APPLY_UPDATE, + ProposalActionTypes.REVOKE_ROOT_PERMISSION, + ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION, + ], + expected: true, + }, + { + input: [ + ProposalActionTypes.UPGRADE_TO_AND_CALL, + ProposalActionTypes.GRANT_PLUGIN_UPDATE_PERMISSION, + ProposalActionTypes.GRANT_ROOT_PERMISSION, + ProposalActionTypes.APPLY_UPDATE, + ProposalActionTypes.REVOKE_ROOT_PERMISSION, + ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION, + ], + expected: true, + }, + { + input: [ + ProposalActionTypes.GRANT_PLUGIN_UPDATE_PERMISSION, + ProposalActionTypes.GRANT_ROOT_PERMISSION, + ProposalActionTypes.APPLY_UPDATE, + ProposalActionTypes.REVOKE_ROOT_PERMISSION, + ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION, + ], + expected: true, + }, + { + input: [ + ProposalActionTypes.GRANT_PLUGIN_UPDATE_PERMISSION, + ProposalActionTypes.GRANT_ROOT_PERMISSION, + ProposalActionTypes.APPLY_UPDATE, + ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION, + ], + expected: false, + }, + { + input: [ + ProposalActionTypes.UPGRADE_TO_AND_CALL, + ProposalActionTypes.GRANT_PLUGIN_UPDATE_PERMISSION, + ProposalActionTypes.GRANT_ROOT_PERMISSION, + ProposalActionTypes.APPLY_UPDATE, + ProposalActionTypes.REVOKE_ROOT_PERMISSION, + ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION, + ProposalActionTypes.GRANT_PLUGIN_UPDATE_PERMISSION, + ProposalActionTypes.GRANT_ROOT_PERMISSION, + ProposalActionTypes.APPLY_UPDATE, + ProposalActionTypes.REVOKE_ROOT_PERMISSION, + ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION, + ], + expected: true, + }, + ]; + for (const { input, expected } of cases) { + const result = isPluginUpdateActionWithRootPermission(input); + expect(result).toEqual(expected); + } + }); + }); });