From 2588415bc93ec5e73f233cd877b7a9721c123a57 Mon Sep 17 00:00:00 2001 From: josemarinas <36479864+josemarinas@users.noreply.github.com> Date: Thu, 23 Nov 2023 16:22:44 +0100 Subject: [PATCH] refactor: Plugin Update Security Check (#301) * add fix for plugin upgrade execution * fix tests * fix tests * remove unused query * Update modules/client/src/internal/client/encoding.ts Co-authored-by: Michael Heuer <20623991+Michael-A-Heuer@users.noreply.github.com> * Update modules/client/CHANGELOG.md Co-authored-by: Michael Heuer <20623991+Michael-A-Heuer@users.noreply.github.com> * update changelogg * wip * refactor and update tests * add tests * add constants * refactor checks * fix tests * fix code smells and comment validation function * update changelogs * refactor update plugin output * refactor compare arrays * refactor conditions in validateApplyUpdateFunction * refactor in classifyProposalActions * refactor isPluginUpdateActionWithRootPermission and isPluginUpdateAction * refactor validateUpdateDaoProposalActions function * refactor conditions in validateUpdatePluginProposalActions * error causes renaming * add minor function and variable renaming * add tests to utils * rename functions * rename isDaoUpdateValid and isDaoUpdate functions * rename isPluginUpdateValid and isPluginUpdate functions * rename isPluginUpdateAction function * fix build * remove unused types * fix unit tests * fix integration tests * fix comments * fix tests * docs: added security check documentation * update error messages and docs * fix typo * docs: fix typos Co-authored-by: Mathias Scherer * fix naming --------- Co-authored-by: Michael Heuer <20623991+Michael-A-Heuer@users.noreply.github.com> Co-authored-by: Michael Heuer Co-authored-by: Mathias Scherer --- docs/sdk/03-update-security-check/index.md | 121 ++ modules/client-common/CHANGELOG.md | 7 + modules/client-common/package.json | 2 +- modules/client-common/src/constants.ts | 3 +- modules/client/CHANGELOG.md | 20 + .../01-client/16-is-plugin-update-valid.ts | 2 +- .../01-client/18-is-dao-update-valid.ts | 2 +- .../01-client/19-is-dao-update-proposal.ts | 2 +- .../05-encoders-decoders/15-apply-update.ts | 2 +- modules/client/package.json | 4 +- .../client/src/internal/client/decoding.ts | 9 +- .../client/src/internal/client/encoding.ts | 48 +- modules/client/src/internal/client/methods.ts | 523 ++---- modules/client/src/internal/constants.ts | 35 +- .../src/internal/graphql-queries/dao.ts | 41 - .../src/internal/graphql-queries/plugin.ts | 7 + modules/client/src/internal/interfaces.ts | 25 +- modules/client/src/internal/schemas.ts | 4 +- modules/client/src/internal/types.ts | 16 + modules/client/src/internal/utils.ts | 861 +++++++++- modules/client/src/types.ts | 97 +- .../test/integration/client/decoding.test.ts | 6 +- .../test/integration/client/encoding.test.ts | 241 ++- .../test/integration/client/methods.test.ts | 693 ++------ modules/client/test/unit/client/utils.test.ts | 1482 +++++++++++++++++ 25 files changed, 3129 insertions(+), 1124 deletions(-) create mode 100644 docs/sdk/03-update-security-check/index.md create mode 100644 modules/client/test/unit/client/utils.test.ts diff --git a/docs/sdk/03-update-security-check/index.md b/docs/sdk/03-update-security-check/index.md new file mode 100644 index 000000000..5114d6b83 --- /dev/null +++ b/docs/sdk/03-update-security-check/index.md @@ -0,0 +1,121 @@ +--- +title: Update Security Check +--- + +## General Update Proposal Checks + +The security check is composed of two functions: `isDaoUpdateProposalValid` and `isPluginUpdateProposalValid` that both receive a proposal ID as an input. +If the proposal cannot be found via the ID or the subgraph is down, we return `"proposalNotFound"` in both cases. + +A proposal contains an `Action[]` array (see [our docs](https://devs.aragon.org/docs/osx/how-it-works/core/dao/actions#actions)). + +The update proposal MUST only contain actions related to the update and no other actions. +If an unexpected action is present, we return `"invalidActions"`. + +DAO proposals contain an `_allowFailureMap` value indicating actions in the action array that are allowed to fail (see [our docs](https://devs.aragon.org/docs/osx/how-it-works/core/dao/actions#the-allowfailuremap-input-argument)). +For updates, `_allowFailureMap` MUST be zero (no failure is allowed). +If it is non-zero, we return `"nonZeroAllowFailureMapValue"`. + +## Specific Action Checks + +Every `Action` in the actions array has three parameters: + +```solidity title="@aragon/osx/core/dao/IDAO.sol" +/// @notice The action struct to be consumed by the DAO's `execute` function resulting in an external call. +/// @param to The address to call. +/// @param value The native token value to be sent with the call. +/// @param data The bytes-encoded function selector and calldata for the call. +struct Action { + address to; + uint256 value; + bytes data; +} +``` + +In the following, we distinguish between actions related to the DAO. + +### DAO Update + +For DAO updates, we expect single action at position 0 of the action array being characterized as follows: + +- `to` MUST be the DAO address. If not, we return `"invalidToAddress"`. + +- `value` MUST be zero. If not, we return `"nonZeroCallValue"`. + +- `data` MUST contain the `upgradeTo(address newImplementation)` OR `upgradeToAndCall(address newImplementation, bytes memory data)` function selector depending on the nature of the update + - The `upgradeToAndCall` call + - MUST go to the right implementation address. This can be either the latest version or any other, newer version. If not, we return `"invalidUpgradeToImplementationAddress"`. + - MUST go to the `initializeFrom` function + - the additional data passed to `initializeFrom` MUST be empty. If not, we return `"invalidUpgradeToAndCallData"`. + - the semantic version number of the previous DAO must be as expected. If not, we return `"invalidUpgradeToAndCallVersion"`. + - `upgradeTo` can be called instead, if no reinitalization of the DAO is required. The call + - MUST go to the right implementation address. This can be either the latest version or any other, newer version. If not, we return `"invalidUpgradeToAndCallImplementationAddress"`. + - MUST have empty subsequent calldata. If not, we return `"invalidUpgradeToAndCallData"`. + +### Plugin Updates + +For each plugin update, we expect a block of associated actions. There can be multiple, independent plugin updates happening in one update proposal. +We expect two types of blocks: + +``` +[ + grant({_where: plugin, _who: pluginSetupProcessor, _permissionId: UPGRADE_PLUGIN_PERMISSION_ID}), + applyUpdate({_dao: dao, _params: applyUpdateParams}), + revoke({_where: plugin, _who: pluginSetupProcessor, _permissionId: UPGRADE_PLUGIN_PERMISSION_ID}) +] +``` + +or + +``` +[ + grant({_where: plugin, _who: pluginSetupProcessor, _permissionId: UPGRADE_PLUGIN_PERMISSION_ID}), + grant({_where: dao, _who: pluginSetupProcessor, _permissionId: ROOT_PERMISSION_ID}), + applyUpdate({_dao: dao, _params: applyUpdateParams}), + revoke({_where: dao, _who: pluginSetupProcessor, _permissionId: ROOT_PERMISSION_ID}), + revoke({_where: plugin, _who: pluginSetupProcessor, _permissionId: UPGRADE_PLUGIN_PERMISSION_ID}) +] +``` + +#### Mandatory `applyUpdate` Call + +Each block being related to a plugin update MUST contain an `applyUpdate` action exactly once. This action is composed as follows: + +- `to` MUST be the `PluginSetupProcessor` address +- `value` MUST be zero. If not, we return `"nonZeroApplyUpdateCallValue"`. +- `data` MUST contain the `applyUpdate` function selector in the first 4 bytes. The following bytes MUST be encoded according to the [the `build-metadata.json` specifications](https://devs.aragon.org/docs/osx/how-to-guides/plugin-development/publication/metadata). + If we cannot decode the action, we return `"invalidData"`. + If we cannot obtain the metadata, we return `"invalidPluginRepoMetadata"`. + Furthermore, the data MUST + - update a plugin that is currently installed to the DAO. If not, we return `"pluginNotInstalled"`. + - update a plugin from an Aragon plugin repo. If it is not an Aragon repo, we return `"notAragonPluginRepo"`. If it does not exist, we return `"missingPluginRepo"`. + - reference an update preparation (resulting from an `prepareUpdate` call). If the update is not prepared, we return `"missingPluginPreparation"`. + - update to a new build and not + - to the same or an older build. If not, we return `"updateToOlderOrSameBuild"`. + - to a different release. If not, we return `"updateToIncompatibleRelease"`. + +#### Mandatory `grant`/`revoke` `UPGRADE_PLUGIN_PERMISSION` Calls + +The `applyUpdate` action MUST be wrapped by `grant` and `revoke` actions on the same DAO where + +- `to` MUST be the DAO address +- `value` MUST be zero. If not, we return `"nonZeroGrantUpgradePluginPermissionCallValue"` / `"nonZeroRevokeUpgradePluginPermissionCallValue"`. +- `data` MUST contain the `grant` / `revoke` function selector in the first 4 bytes. The subsequent bytes MUST be as follows: + - `where` MUST be the plugin proxy address. If not, we return `"invalidGrantUpgradePluginPermissionWhereAddress"` / `"invalidRevokeUpgradePluginPermissionWhereAddress"`. + - `who` MUST be the `PluginSetupProcessor` address. If not, we return `"invalidGrantUpgradePluginPermissionWhoAddress"` / `"invalidRevokeUpgradePluginPermissionWhoAddress"`. + - `permissionId` MUST be `bytes32 UPGRADE_PLUGIN_PERMISSION_ID = keccak256("UPGRADE_PLUGIN_PERMISSION")`. If not, we return `"invalidGrantUpgradePluginPermissionPermissionId"` / `"invalidRevokeUpgradePluginPermissionPermissionId"`. + - `permissionName` MUST be `UPGRADE_PLUGIN_PERMISSION`. If not, we return `"invalidGrantUpgradePluginPermissionPermissionName"` / `"invalidRevokeUpgradePluginPermissionPermissionName"` + +#### Optional `grant`/`revoke` `ROOT_PERMISSION` Calls + +The `applyUpdate` action CAN be wrapped by `grant` and `revoke` actions on the same DAO where + +- `to` MUST be the DAO address +- `value` MUST be zero. If not, we return `"nonZeroGrantRootPermissionCallValue"` / `"nonZeroRevokeRootPermissionCallValue"`. +- `data` MUST contain the `grant` / `revoke` function selector in the first 4 bytes. The subsequent bytes MUST be as follows: + - `where` MUST be the DAO proxy address. If not, we return `"invalidGrantRootPermissionWhereAddress"` / `"invalidRevokeRootPermissionWhereAddress"`. + - `who` MUST be the `PluginSetupProcessor` address. If not, we return `"invalidGrantRootPermissionWhoAddress"` / `"invalidRevokeRootPermissionWhoAddress"`. + - `permissionId` MUST be `bytes32 ROOT_PERMISSION_ID = keccak256("ROOT_PERMISSION")`. If not, we return `"invalidGrantRootPermissionPermissionId"` / `"invalidRevokeRootPermissionPermissionId"`. + - `permissionName` MUST be `ROOT_PERMISSION`. If not, we return `"invalidGrantRootPermissionPermissionName"` / `"invalidRevokeRootPermissionPermissionName"` + + diff --git a/modules/client-common/CHANGELOG.md b/modules/client-common/CHANGELOG.md index d89af3c0d..139a4f302 100644 --- a/modules/client-common/CHANGELOG.md +++ b/modules/client-common/CHANGELOG.md @@ -21,6 +21,13 @@ TEMPLATE: ### Added +- Added `UPGRADE_PLUGIN_PERMISSION` to `PermissionType` enum +- Fix chain id for sepolia network + +## [1.11.0] + +### Added + - Added `actions` to `SubgraphListItem` type ## [1.10.0] diff --git a/modules/client-common/package.json b/modules/client-common/package.json index 13330eb04..477f1cc90 100644 --- a/modules/client-common/package.json +++ b/modules/client-common/package.json @@ -1,7 +1,7 @@ { "name": "@aragon/sdk-client-common", "author": "Aragon Association", - "version": "1.11.0", + "version": "1.11.1", "license": "MIT", "main": "dist/index.js", "module": "dist/sdk-client-common.esm.js", diff --git a/modules/client-common/src/constants.ts b/modules/client-common/src/constants.ts index 47b38d24e..43351f659 100644 --- a/modules/client-common/src/constants.ts +++ b/modules/client-common/src/constants.ts @@ -456,7 +456,7 @@ export const ADDITIONAL_NETWORKS: Network[] = [ }, { name: "sepolia", - chainId: 58008, + chainId: 11155111, }, { name: "local", @@ -466,6 +466,7 @@ export const ADDITIONAL_NETWORKS: Network[] = [ const Permissions = { UPGRADE_PERMISSION: "UPGRADE_PERMISSION", + UPGRADE_PLUGIN_PERMISSION: "UPGRADE_PLUGIN_PERMISSION", SET_METADATA_PERMISSION: "SET_METADATA_PERMISSION", EXECUTE_PERMISSION: "EXECUTE_PERMISSION", WITHDRAW_PERMISSION: "WITHDRAW_PERMISSION", diff --git a/modules/client/CHANGELOG.md b/modules/client/CHANGELOG.md index 17c3cd996..e8bda1cef 100644 --- a/modules/client/CHANGELOG.md +++ b/modules/client/CHANGELOG.md @@ -19,6 +19,26 @@ TEMPLATE: ## [UPCOMING] +### Changed + +- Add grant and revoke for permission `UPGRADE_PLUGIN_PERMISSION` in `applyUpdate` encoder +- Refactor DAO update proposal validation function +- Refactor plugin update proposal validation function +- Refactor boolean check for checking if a proposal is a DAO update proposal +- Refactor boolean check for checking if a proposal is a plugin update proposal +- Rename `applyUpdateAction` to `applyUpdateActionAndPermissionsBlock` +- Rename `isDaoUpdateAction`to `containsDaoUpdateAction` +- Rename `isDaoUpdate` to `isDaoUpdateProposal` +- Rename `isDaoUpdateValid` to `isDaoUpdateProposalValid` +- Rename `isPluginUpdate` to `isPluginUpdateProposal` +- Rename `isPluginUpdateValid` to `isPluginUpdateProposalValid` +- Rename `isPluginUpdateAction` to `containsPluginUpdateAction` +- Rename `isPluginUpdateActionBlockWithRootPermission` to `containsPluginUpdateActionBlockWithRootPermission` + + + +## [1.19.0] + ### Added - `isMember` function to `TokenVotingClient` diff --git a/modules/client/examples/01-client/16-is-plugin-update-valid.ts b/modules/client/examples/01-client/16-is-plugin-update-valid.ts index bddad60d4..ad37f35a2 100644 --- a/modules/client/examples/01-client/16-is-plugin-update-valid.ts +++ b/modules/client/examples/01-client/16-is-plugin-update-valid.ts @@ -21,7 +21,7 @@ const actions: DaoAction[] = [ ]; // check if a plugin update proposal is valid -const isValid = client.methods.isPluginUpdateValid({ +const isValid = client.methods.isPluginUpdateProposalValid({ daoAddress: "0x1234567890123456789012345678901234567890", actions, }); diff --git a/modules/client/examples/01-client/18-is-dao-update-valid.ts b/modules/client/examples/01-client/18-is-dao-update-valid.ts index 20504273c..e110d62f7 100644 --- a/modules/client/examples/01-client/18-is-dao-update-valid.ts +++ b/modules/client/examples/01-client/18-is-dao-update-valid.ts @@ -21,7 +21,7 @@ const actions: DaoAction[] = [ ]; // check if a dap update proposal is valid -const isValid = client.methods.isDaoUpdateValid({ +const isValid = client.methods.isDaoUpdateProposalValid({ daoAddress: "0x1234567890123456789012345678901234567890", actions, }); diff --git a/modules/client/examples/01-client/19-is-dao-update-proposal.ts b/modules/client/examples/01-client/19-is-dao-update-proposal.ts index d466b863d..b62bc5639 100644 --- a/modules/client/examples/01-client/19-is-dao-update-proposal.ts +++ b/modules/client/examples/01-client/19-is-dao-update-proposal.ts @@ -24,7 +24,7 @@ const actions: DaoAction[] = [ ]; // check if a plugin update proposal is valid -const isValid = client.methods.isDaoUpdateProposal(actions); +const isValid = client.methods.isDaoUpdateProposalProposal(actions); console.log(isValid); diff --git a/modules/client/examples/05-encoders-decoders/15-apply-update.ts b/modules/client/examples/05-encoders-decoders/15-apply-update.ts index 7e1e3345f..513a81154 100644 --- a/modules/client/examples/05-encoders-decoders/15-apply-update.ts +++ b/modules/client/examples/05-encoders-decoders/15-apply-update.ts @@ -42,7 +42,7 @@ const applyUpdateParams: ApplyUpdateParams = { const daoAddressOrEns: string = "0x123123123123123123123123123123123123"; // "my-dao.eth" -const actions: DaoAction[] = client.encoding.applyUpdateAction( +const actions: DaoAction[] = client.encoding.applyUpdateAndPermissionsActionBlock( daoAddressOrEns, applyUpdateParams, ); diff --git a/modules/client/package.json b/modules/client/package.json index 5a3e78345..5cdb0e35d 100644 --- a/modules/client/package.json +++ b/modules/client/package.json @@ -1,7 +1,7 @@ { "name": "@aragon/sdk-client", "author": "Aragon Association", - "version": "1.19.0", + "version": "1.19.1", "license": "MIT", "main": "dist/index.js", "module": "dist/sdk-client.esm.js", @@ -62,7 +62,7 @@ }, "dependencies": { "@aragon/osx-ethers": "1.3.0-rc0.4", - "@aragon/sdk-client-common": "^1.11.0", + "@aragon/sdk-client-common": "^1.11.1", "@aragon/sdk-ipfs": "^1.1.0", "@ethersproject/abstract-signer": "^5.5.0", "@ethersproject/bignumber": "^5.6.0", 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/encoding.ts b/modules/client/src/internal/client/encoding.ts index e4a63d7b7..ab1b81148 100644 --- a/modules/client/src/internal/client/encoding.ts +++ b/modules/client/src/internal/client/encoding.ts @@ -152,7 +152,7 @@ export class ClientEncoding extends ClientCore implements IClientEncoding { * @return {*} {DaoAction[]} * @memberof ClientEncoding */ - public applyUpdateAction( + public applyUpdateAndPermissionsActionBlock( daoAddress: string, params: ApplyUpdateParams, ): DaoAction[] { @@ -164,26 +164,52 @@ export class ClientEncoding extends ClientCore implements IClientEncoding { args, ]); const pspAddress = this.web3.getAddress("pluginSetupProcessorAddress"); - // Grant ROOT_PERMISION in the DAO to the PSP - const grantAction = this.grantAction(daoAddress, { - where: daoAddress, + + // Grant UPGRADE_PLUGIN_PERMISSION in the plugin to the PSP + const grantUpgradeAction = this.grantAction(daoAddress, { + where: params.pluginAddress, who: pspAddress, - permission: Permissions.ROOT_PERMISSION, + permission: Permissions.UPGRADE_PLUGIN_PERMISSION, }); - // Revoke ROOT_PERMISION in the DAO to the PSP - const revokeAction = this.revokeAction(daoAddress, { - where: daoAddress, + // Revoke UPGRADE_PLUGIN_PERMISSION in the plugin to the PSP + const revokeUpgradeAction = this.revokeAction(daoAddress, { + where: params.pluginAddress, who: pspAddress, - permission: Permissions.ROOT_PERMISSION, + permission: Permissions.UPGRADE_PLUGIN_PERMISSION, }); + // If the update requests permissions to be granted or revoked, the PSP needs temporary `ROOT_PERMISSION_ID` permission + if (params.permissions.length > 0) { + const grantRootAction = this.grantAction(daoAddress, { + where: daoAddress, + who: pspAddress, + permission: Permissions.ROOT_PERMISSION, + }); + // Revoke ROOT_PERMISSION in the DAO to the PSP + const revokeRootAction = this.revokeAction(daoAddress, { + where: daoAddress, + who: pspAddress, + permission: Permissions.ROOT_PERMISSION, + }); + return [ + grantUpgradeAction, + grantRootAction, + { + to: pspAddress, + value: BigInt(0), + data: hexToBytes(hexBytes), + }, + revokeRootAction, + revokeUpgradeAction, + ]; + } return [ - grantAction, + grantUpgradeAction, { to: pspAddress, value: BigInt(0), data: hexToBytes(hexBytes), }, - revokeAction, + revokeUpgradeAction, ]; } diff --git a/modules/client/src/internal/client/methods.ts b/modules/client/src/internal/client/methods.ts index c24f83577..f7f64ab75 100644 --- a/modules/client/src/internal/client/methods.ts +++ b/modules/client/src/internal/client/methods.ts @@ -22,8 +22,8 @@ import { QueryDao, QueryDaos, QueryIPlugin, + QueryIProposal, QueryPlugin, - QueryPluginPreparations, QueryPluginPreparationsExtended, QueryPlugins, QueryTokenBalances, @@ -43,18 +43,13 @@ import { DaoMetadata, DaoQueryParams, DaoSortBy, - DaoUpdateProposalInvalidityCause, DaoUpdateProposalValidity, - DecodedInitializeFromParams, DepositErc1155Params, DepositErc20Params, DepositErc721Params, DepositEthParams, DepositParams, - GrantPermissionDecodedParams, HasPermissionParams, - IsDaoUpdateValidParams, - IsPluginUpdateValidParams, PluginPreparationListItem, PluginPreparationQueryParams, PluginPreparationSortBy, @@ -64,9 +59,8 @@ import { PluginRepoListItem, PluginRepoReleaseMetadata, PluginSortBy, - PluginUpdateProposalInValidityCause, PluginUpdateProposalValidity, - RevokePermissionDecodedParams, + ProposalSettingsErrorCause, SetAllowanceParams, SetAllowanceSteps, SetAllowanceStepValue, @@ -78,29 +72,28 @@ import { SubgraphBalance, SubgraphDao, SubgraphDaoListItem, + SubgraphIProposal, SubgraphPluginInstallation, SubgraphPluginPreparationListItem, SubgraphPluginRepo, SubgraphPluginRepoListItem, - SubgraphPluginRepoRelease, - SubgraphPluginUpdatePreparation, SubgraphTransferListItem, SubgraphTransferTypeMap, } from "../types"; import { - decodeApplyUpdateAction, - decodeGrantAction, - decodeInitializeFromAction, - decodeRevokeAction, - decodeUpgradeToAndCallAction, - findActionIndex, - getPreparedSetupId, + classifyProposalActions, + containsDaoUpdateAction, + containsPluginUpdateActionBlock, + containsPluginUpdateActionBlockWithRootPermission, toAssetBalance, + toDaoActions, toDaoDetails, toDaoListItem, toPluginPreparationListItem, toPluginRepo, toTokenTransfer, + validateUpdateDaoProposalActions, + validateUpdatePluginProposalActions, } from "../utils"; import { isAddress } from "@ethersproject/address"; import { toUtf8Bytes } from "@ethersproject/strings"; @@ -109,9 +102,6 @@ import { EMPTY_BUILD_METADATA_LINK, EMPTY_DAO_METADATA_LINK, EMPTY_RELEASE_METADATA_LINK, - PreparationType, - SupportedPluginRepo, - SupportedPluginRepoArray, UNAVAILABLE_BUILD_METADATA, UNAVAILABLE_DAO_METADATA, UNAVAILABLE_RELEASE_METADATA, @@ -124,13 +114,10 @@ import { AddressOrEnsSchema, AmountMismatchError, ClientCore, - DaoAction, DaoCreationError, - DecodedApplyUpdateParams, EmptyMultiUriError, FailedDepositError, findLog, - getNamedTypesFromMetadata, InstallationNotFoundError, InvalidAddressOrEnsError, InvalidCidError, @@ -142,7 +129,6 @@ import { NoProviderError, NotImplementedError, PermissionIds, - Permissions, PluginUninstallationPreparationError, prepareGenericInstallation, prepareGenericUpdate, @@ -172,8 +158,6 @@ import { DepositErc721Schema, DepositEthSchema, HasPermissionSchema, - IsDaoUpdateValidSchema, - IsPluginUpdateValidSchema, PluginPreparationQuerySchema, PluginQuerySchema, } from "../schemas"; @@ -1116,408 +1100,169 @@ 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 isDaoUpdateProposal( + 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 containsDaoUpdateAction(classifiedActions); } - /** - * check if permission is root - * check if permissionId is root - * check if where is the dao address - * check if who is the psp address + * Given a proposal id returns if that proposal is a plugin update proposal * - * @private - * @param {(GrantPermissionDecodedParams | RevokePermissionDecodedParams)} params - * @param {string} daoAddress - * @return {*} {boolean} - * @memberof ClientMethods - */ - private isPluginUpdatePermissionValid( - params: GrantPermissionDecodedParams | RevokePermissionDecodedParams, - daoAddress: string, - ): boolean { - const pspAddress = this.web3.getAddress("pluginSetupProcessorAddress"); - return ( - params.permission === Permissions.ROOT_PERMISSION && - params.permissionId === PermissionIds.ROOT_PERMISSION_ID && - params.where === daoAddress && - params.who === pspAddress - ); - } - - /** - * check if the plugin is installed - * check if the plugin release is the same as the one installed - * check if the plugin build is higher than the one installed - * check if the plugin repo (pluginSetupRepo) exist - * check if is one of the aragon plugin repos - * check if the plugin repo metadata is valid - * check if the plugin preparation exist - * check if the plugin preparation data is valid - * - * @private - * @param {string} daoAddress - * @param {DecodedApplyUpdateParams} decodedApplyUpdateActionParams - * @return {*} {Promise} + * @param {string} proposalId + * @return {*} {Promise} * @memberof ClientMethods */ - private async checkApplyUpdateActionInvalidityCauses( - daoAddress: string, - decodedApplyUpdateActionParams: DecodedApplyUpdateParams, - ): Promise { - const causes: PluginUpdateProposalInValidityCause[] = []; - // get dao with plugins - type U = { dao: SubgraphDao }; - const { dao } = await this.graphql.request({ - query: QueryDao, - params: { address: daoAddress }, - name: "dao", - }); - // find the plugin with the same address - const plugin = dao.plugins.find((plugin) => - plugin.appliedPreparation?.pluginAddress === - decodedApplyUpdateActionParams.pluginAddress - ); - if (plugin) { - // check release is the same as the one installed - if ( - plugin.appliedVersion?.release.release !== - decodedApplyUpdateActionParams.versionTag.release - ) { - causes.push(PluginUpdateProposalInValidityCause.INVALID_PLUGIN_RELEASE); - } - // check build is higher than the one installed - if ( - !plugin.appliedVersion?.build || - plugin.appliedVersion?.build >= - decodedApplyUpdateActionParams.versionTag.build - ) { - causes.push(PluginUpdateProposalInValidityCause.INVALID_PLUGIN_BUILD); - } - } else { - causes.push(PluginUpdateProposalInValidityCause.PLUGIN_NOT_INSTALLED); - return causes; - } - // check if plugin repo (pluginSetupRepo) exist - type V = { pluginRepo: SubgraphPluginRepo }; - const { pluginRepo } = await this.graphql.request({ - query: QueryPlugin, - params: { id: decodedApplyUpdateActionParams.pluginRepo }, - name: "pluginRepo", + public async isPluginUpdateProposal( + 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 (pluginRepo) { - // check if is one of the aragon plugin repos - if ( - !SupportedPluginRepoArray.includes( - pluginRepo.subdomain as SupportedPluginRepo, - ) - ) { - causes.push(PluginUpdateProposalInValidityCause.NOT_ARAGON_PLUGIN_REPO); - } - } else { - causes.push(PluginUpdateProposalInValidityCause.MISSING_PLUGIN_REPO); - return causes; + if (!iproposal) { + return false; } - - // get the prepared setup id - const preparedSetupId = getPreparedSetupId( - decodedApplyUpdateActionParams, - PreparationType.UPDATE, - ); - // get plugin preparation - type W = { pluginPreparation: SubgraphPluginUpdatePreparation }; - const { pluginPreparation } = await this.graphql.request({ - query: QueryPluginPreparations, - params: { where: { preparedSetupId } }, - name: "pluginPreparation", - }); - if (pluginPreparation) { - // get the metadata of the plugin repo - // for the release and build specified - const release = pluginRepo.releases.find(( - release: SubgraphPluginRepoRelease, - ) => - release.release === decodedApplyUpdateActionParams.versionTag.release - ); - const build = release?.builds.find(( - build: { build: number; metadata: string }, - ) => build.build === decodedApplyUpdateActionParams.versionTag.build); - const metadataCid = build?.metadata; - - // fetch the metadata - const metadata = await this.ipfs.fetchString(metadataCid!); - const metadataJson = JSON.parse(metadata) as PluginRepoBuildMetadata; - // get the update abi for the specified build - const updateAbi = metadataJson.pluginSetup - .prepareUpdate[decodedApplyUpdateActionParams.versionTag.build]?.inputs; - if (updateAbi) { - // if the abi exists try to decode the data - try { - if ( - decodedApplyUpdateActionParams.initData.length > 0 && - updateAbi.length === 0 - ) { - throw new Error(); - } - // if the decode does not throw an error the data is valid - defaultAbiCoder.decode( - getNamedTypesFromMetadata(updateAbi), - decodedApplyUpdateActionParams.initData, - ); - } catch { - // if the decode throws an error the data is invalid - causes.push( - PluginUpdateProposalInValidityCause.INVALID_DATA, - ); - } - } else { - causes.push( - PluginUpdateProposalInValidityCause.INVALID_PLUGIN_REPO_METADATA, - ); - } - } else { - causes.push( - PluginUpdateProposalInValidityCause.MISSING_PLUGIN_PREPARATION, - ); + const subgraphActions = iproposal.actions; + let actions = toDaoActions(subgraphActions); + let classifiedActions = classifyProposalActions(actions); + if(containsDaoUpdateAction(classifiedActions)) { + classifiedActions = classifiedActions.slice(1); } - return causes; + return containsPluginUpdateActionBlock(classifiedActions) || + containsPluginUpdateActionBlockWithRootPermission(classifiedActions); } - /** * Check if the specified proposal id is valid for updating a plugin - * The failure map should be checked before calling this method * - * @param {DaoAction[]} actions + * @param {string} proposalId * @return {*} {Promise} * @memberof ClientMethods */ - public async isPluginUpdateValid( - params: IsPluginUpdateValidParams, + public async isPluginUpdateProposalValid( + proposalId: string, ): Promise { - await IsPluginUpdateValidSchema.strict().validate(params); - const causes: PluginUpdateProposalInValidityCause[] = []; - // get expected actions signatures - const grantSignature = DAO__factory.createInterface().getFunction("grant") - .format("minimal"); - const revokeSignature = DAO__factory.createInterface().getFunction("revoke") - .format("minimal"); - const applyUpdateSignature = PluginSetupProcessor__factory.createInterface() - .getFunction("applyUpdate").format("minimal"); - - // find signatures in the actions specified in the proposal - const grantIndex = findActionIndex(params.actions, grantSignature); - const applyUpdateIndex = findActionIndex( - params.actions, - applyUpdateSignature, - ); - const revokeIndex = findActionIndex(params.actions, revokeSignature); - - // check that all actions are present and in the correct order - if ( - [grantIndex, applyUpdateIndex, revokeIndex].includes(-1) || - grantIndex > applyUpdateIndex || - applyUpdateIndex > revokeIndex - ) { - causes.push(PluginUpdateProposalInValidityCause.INVALID_ACTIONS); + // not validating the proposalId because multiple proposal id formats can be used + // 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 invalid return { - isValid: causes.length === 0, - causes, + isValid: false, + actionErrorCauses: [], + proposalSettingsErrorCauses: [ProposalSettingsErrorCause.PROPOSAL_NOT_FOUND] }; } - - // check grant action - if ( - !this.isPluginUpdatePermissionValid( - decodeGrantAction(params.actions[grantIndex].data), - params.daoAddress, - ) - ) { - causes.push(PluginUpdateProposalInValidityCause.INVALID_GRANT_PERMISSION); - } - - // check revoke action - if ( - !this.isPluginUpdatePermissionValid( - decodeRevokeAction(params.actions[revokeIndex].data), - params.daoAddress, - ) - ) { - causes.push( - PluginUpdateProposalInValidityCause.INVALID_REVOKE_PERMISSION, - ); + // check failure map + if (iproposal.allowFailureMap !== "0") { + // if the failure map is not 0 return invalid failure map + return { + isValid: false, + actionErrorCauses: [], + proposalSettingsErrorCauses: [ProposalSettingsErrorCause.NON_ZERO_ALLOW_FAILURE_MAP_VALUE] + }; } - - // check apply update action - const decodedApplyUpdateActionParams = decodeApplyUpdateAction( - params.actions[applyUpdateIndex].data, - ); - const applyUpdateCauses = await this.checkApplyUpdateActionInvalidityCauses( - params.daoAddress, - decodedApplyUpdateActionParams, + // validate actions + return validateUpdatePluginProposalActions( + toDaoActions(iproposal.actions), + iproposal.dao.id, + this.web3.getAddress("pluginSetupProcessorAddress"), + this.graphql, + this.ipfs, ); - causes.push(...applyUpdateCauses); - return { - isValid: causes.length === 0, - causes, - }; } /** - * 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, + public async isDaoUpdateProposalValid( + 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, + proposalSettingsErrorCauses: [ + ProposalSettingsErrorCause.PROPOSAL_NOT_FOUND, + ], + actionErrorCauses: [], }; } - 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 failure map + if (iproposal.allowFailureMap !== "0") { + // if the failure map is not 0 return invalid failure map + return { + isValid: false, + actionErrorCauses: [], + proposalSettingsErrorCauses: [ProposalSettingsErrorCause.NON_ZERO_ALLOW_FAILURE_MAP_VALUE] + }; } - // 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/constants.ts b/modules/client/src/internal/constants.ts index 06c3ffa69..cb8fec306 100644 --- a/modules/client/src/internal/constants.ts +++ b/modules/client/src/internal/constants.ts @@ -1,4 +1,7 @@ -import { DAO__factory } from "@aragon/osx-ethers"; +import { + DAO__factory, + PluginSetupProcessor__factory, +} from "@aragon/osx-ethers"; import { AddressZero } from "@ethersproject/constants"; import { Contract } from "@ethersproject/contracts"; import { @@ -10,6 +13,7 @@ import { defaultAbiCoder } from "@ethersproject/abi"; import { keccak256 } from "@ethersproject/keccak256"; import { abi as ERC20_ABI } from "@openzeppelin/contracts/build/contracts/ERC20.json"; import { abi as ERC721_ABI } from "@openzeppelin/contracts/build/contracts/ERC721.json"; +import { ProposalActionTypes } from "./types"; export const AVAILABLE_FUNCTION_SIGNATURES: string[] = [ new Contract(AddressZero, ERC20_ABI).interface.getFunction("transfer") @@ -121,3 +125,32 @@ export enum PreparationType { UPDATE = 2, UNINSTALLATION = 3, } + +export const UPDATE_PLUGIN_SIGNATURES: string[] = [ + DAO__factory.createInterface().getFunction("grant") + .format("minimal"), + DAO__factory.createInterface().getFunction("revoke") + .format("minimal"), + PluginSetupProcessor__factory.createInterface().getFunction( + "applyUpdate", + ).format("minimal"), + DAO__factory.createInterface().getFunction( + "upgradeTo", + ).format("minimal"), + DAO__factory.createInterface() + .getFunction("upgradeToAndCall") + .format("minimal"), +]; + +export const PLUGIN_UPDATE_ACTION_PATTERN = [ + ProposalActionTypes.GRANT_PLUGIN_UPGRADE_PERMISSION, + ProposalActionTypes.APPLY_UPDATE, + ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION, +]; +export const PLUGIN_UPDATE_WITH_ROOT_ACTION_PATTERN = [ + ProposalActionTypes.GRANT_PLUGIN_UPGRADE_PERMISSION, + ProposalActionTypes.GRANT_ROOT_PERMISSION, + ProposalActionTypes.APPLY_UPDATE, + ProposalActionTypes.REVOKE_ROOT_PERMISSION, + ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION, +]; diff --git a/modules/client/src/internal/graphql-queries/dao.ts b/modules/client/src/internal/graphql-queries/dao.ts index 644742fab..909ae0509 100644 --- a/modules/client/src/internal/graphql-queries/dao.ts +++ b/modules/client/src/internal/graphql-queries/dao.ts @@ -47,44 +47,3 @@ export const QueryDaos = gql` } } `; - -export const QueryDaoTransfersByAddress = gql` -query DaoTransfersByAddress($address: ID!) { - dao(id: $address) { - withdraws { - id - token { - symbol - decimals - symbol - } - to - dao { - id - subdomain - } - amount - reference - transaction - creationDate - } - deposits { - id - token { - symbol - decimals - symbol - } - sender - dao { - id - subdomain - } - amount - reference - transaction - creationDate - } - } -} -`; 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..d47aa19cc 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, @@ -97,20 +97,21 @@ export interface IClientMethods { contractAddress: string, ) => Promise<[number, number, number]>; - isPluginUpdate: ( - actions: DaoAction[], - ) => boolean; + isPluginUpdateProposal: ( + proposalId: string, + ) => Promise; - isPluginUpdateValid: ( - params: IsDaoUpdateValidParams, + isPluginUpdateProposalValid: ( + proposalId: string, ) => Promise; - isDaoUpdate: ( - actions: DaoAction[], - ) => boolean; + isDaoUpdateProposal: ( + proposalId: string, + ) => Promise; - isDaoUpdateValid: ( - params: IsDaoUpdateValidParams, + isDaoUpdateProposalValid: ( + proposalId: string, + version?: SupportedVersion, ) => Promise; getDaoImplementation: ( @@ -170,7 +171,7 @@ export interface IClientEncoding { daoAddressOrEns: string, params: ApplyUninstallationParams, ) => DaoAction[]; - applyUpdateAction: ( + applyUpdateAndPermissionsActionBlock: ( daoAddressOrEns: string, params: ApplyUpdateParams, ) => DaoAction[]; diff --git a/modules/client/src/internal/schemas.ts b/modules/client/src/internal/schemas.ts index e93434c70..8061ee032 100644 --- a/modules/client/src/internal/schemas.ts +++ b/modules/client/src/internal/schemas.ts @@ -171,7 +171,7 @@ export const DaoUpdateSchema = object({ daoFactoryAddress: AddressOrEnsSchema.notRequired(), }); -export const IsPluginUpdateValidSchema = object({ +export const IsPluginUpdateProposalValidSchema = object({ actions: array().of(object({ to: AddressOrEnsSchema.required(), value: BigintSchema.required(), @@ -179,7 +179,7 @@ export const IsPluginUpdateValidSchema = object({ })).required().min(1), daoAddress: AddressOrEnsSchema.required(), }); -export const IsDaoUpdateValidSchema = object({ +export const IsDaoUpdateProposalValidSchema = object({ actions: array().of(object({ to: AddressOrEnsSchema.required(), value: BigintSchema.required(), diff --git a/modules/client/src/internal/types.ts b/modules/client/src/internal/types.ts index e72cbd627..c1d2a2b52 100644 --- a/modules/client/src/internal/types.ts +++ b/modules/client/src/internal/types.ts @@ -237,3 +237,19 @@ export enum SubgraphPluginPermissionOperation { REVOKE = "Revoke", GRANT_WITH_CONDITION = "GrantWithCondition", } + +export enum ProposalActionTypes { + UPGRADE_TO = "upgradeTo", + UPGRADE_TO_AND_CALL = "upgradeToAndCall", + APPLY_UPDATE = "applyUpdate", + GRANT_PLUGIN_UPGRADE_PERMISSION = "grantUpgradePluginPermission", + REVOKE_PLUGIN_UPGRADE_PERMISSION = "revokeUpgradePluginPermission", + 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 6a96a95d7..41a274b92 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, @@ -17,6 +19,9 @@ import { PluginRepo, PluginRepoBuildMetadata, PluginRepoReleaseMetadata, + PluginUpdateProposalInValidityCause, + PluginUpdateProposalValidity, + ProposalSettingsErrorCause, RevokePermissionDecodedParams, RevokePermissionParams, Transfer, @@ -27,6 +32,7 @@ import { import { ContractPermissionParams, ContractPermissionWithConditionParams, + ProposalActionTypes, SubgraphBalance, SubgraphDao, SubgraphDaoListItem, @@ -38,10 +44,13 @@ import { SubgraphErc721TransferListItem, SubgraphNativeBalance, SubgraphNativeTransferListItem, + SubgraphPluginInstallationListItem, SubgraphPluginListItem, SubgraphPluginPermissionOperation, SubgraphPluginPreparationListItem, SubgraphPluginRepo, + SubgraphPluginRepoRelease, + SubgraphPluginUpdatePreparation, SubgraphTransferListItem, SubgraphTransferType, } from "./types"; @@ -63,6 +72,7 @@ import { DecodedApplyInstallationParams, DecodedApplyUpdateParams, getFunctionFragment, + getNamedTypesFromMetadata, hexToBytes, InterfaceParams, InvalidParameter, @@ -71,6 +81,7 @@ import { NotImplementedError, PermissionIds, PermissionOperationType, + Permissions, TokenType, } from "@aragon/sdk-client-common"; import { Signer } from "@ethersproject/abstract-signer"; @@ -79,13 +90,31 @@ import { BigNumber } from "@ethersproject/bignumber"; import { abi as ERC721_ABI } from "@openzeppelin/contracts/build/contracts/ERC721.json"; import { abi as ERC1155_ABI } from "@openzeppelin/contracts/build/contracts/ERC1155.json"; import { SubgraphAction } from "../client-common"; -import { PreparationType, ZERO_BYTES_HASH } from "./constants"; +import { + PLUGIN_UPDATE_ACTION_PATTERN, + PLUGIN_UPDATE_WITH_ROOT_ACTION_PATTERN, + PreparationType, + SupportedPluginRepo, + SupportedPluginRepoArray, + UPDATE_PLUGIN_SIGNATURES, + ZERO_BYTES_HASH, +} from "./constants"; import { DepositErc1155Schema, DepositErc20Schema, DepositErc721Schema, DepositEthSchema, } from "./schemas"; +import { + IClientGraphQLCore, + IClientIpfsCore, +} from "@aragon/sdk-client-common/dist/internal"; +import { + QueryDao, + QueryPlugin, + QueryPluginInstallations, + QueryPluginPreparations, +} from "./graphql-queries"; export function unwrapDepositParams( params: DepositEthParams | DepositErc20Params, @@ -768,6 +797,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 { @@ -824,3 +865,821 @@ export function toPluginPermissionOperationType( throw new InvalidPermissionOperationType(); } } + +// function that compares 2 generic arrays +// and returns true if they are equal +// and false if they are not +export function compareArrays(array1: T[], array2: T[]): boolean { + return JSON.stringify(array1) === JSON.stringify(array2); +} +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 validateGrantUpgradePluginPermissionAction( + action: DaoAction, + pspAddress: string, + daoAddress: string, + graphql: IClientGraphQLCore, +): Promise { + const causes: PluginUpdateProposalInValidityCause[] = []; + // decode the action + const decodedPermission = decodeGrantAction(action.data); + // retrieve the plugin installations from subgraph + // with the same plugin address and the specified + // dao address + const pluginInstallations = await getPluginInstallations( + daoAddress, + decodedPermission.where, + graphql, + ); + // if the plugin installations length is 0 means that + // that the address in the where field is not a plugin + // or is not installed in the specified dao + if (pluginInstallations.length === 0) { + causes.push( + PluginUpdateProposalInValidityCause + .PLUGIN_NOT_INSTALLED, + ); + } + // Value must be 0 + if (action.value.toString() !== "0") { + causes.push( + PluginUpdateProposalInValidityCause + .NON_ZERO_GRANT_UPGRADE_PLUGIN_PERMISSION_CALL_VALUE, + ); + } + // The permission should be granted to the PSP + if (decodedPermission.who !== pspAddress) { + causes.push( + PluginUpdateProposalInValidityCause + .INVALID_GRANT_UPGRADE_PLUGIN_PERMISSION_WHO_ADDRESS, + ); + } + // The permission should be `UPGRADE_PLUGIN_PERMISSION` + if ( + decodedPermission.permission !== Permissions.UPGRADE_PLUGIN_PERMISSION + ) { + causes.push( + PluginUpdateProposalInValidityCause + .INVALID_GRANT_UPGRADE_PLUGIN_PERMISSION_PERMISSION_NAME, + ); + } + // The permissionId should be `UPGRADE_PLUGIN_PERMISSION_ID` + if ( + decodedPermission.permissionId !== + PermissionIds.UPGRADE_PLUGIN_PERMISSION_ID + ) { + causes.push( + PluginUpdateProposalInValidityCause + .INVALID_GRANT_UPGRADE_PLUGIN_PERMISSION_PERMISSION_ID, + ); + } + return causes; +} + +export async function validateRevokeUpgradePluginPermissionAction( + action: DaoAction, + pspAddress: string, + daoAddress: string, + graphql: IClientGraphQLCore, +): Promise { + const causes: PluginUpdateProposalInValidityCause[] = []; + const decodedPermission = decodeRevokeAction(action.data); + const pluginInstallations = await getPluginInstallations( + daoAddress, + decodedPermission.where, + graphql, + ); + if (pluginInstallations.length === 0) { + causes.push( + PluginUpdateProposalInValidityCause + .PLUGIN_NOT_INSTALLED, + ); + } + if (action.value.toString() !== "0") { + causes.push( + PluginUpdateProposalInValidityCause + .NON_ZERO_REVOKE_UPGRADE_PLUGIN_PERMISSION_CALL_VALUE, + ); + } + if (decodedPermission.who !== pspAddress) { + causes.push( + PluginUpdateProposalInValidityCause + .INVALID_REVOKE_UPGRADE_PLUGIN_PERMISSION_WHO_ADDRESS, + ); + } + if ( + decodedPermission.permission !== Permissions.UPGRADE_PLUGIN_PERMISSION + ) { + causes.push( + PluginUpdateProposalInValidityCause + .INVALID_REVOKE_UPGRADE_PLUGIN_PERMISSION_PERMISSION_NAME, + ); + } + if ( + decodedPermission.permissionId !== + PermissionIds.UPGRADE_PLUGIN_PERMISSION_ID + ) { + causes.push( + PluginUpdateProposalInValidityCause + .INVALID_REVOKE_UPGRADE_PLUGIN_PERMISSION_PERMISSION_ID, + ); + } + return causes; +} +export function validateGrantRootPermissionAction( + action: DaoAction, + daoAddress: string, + pspAddress: string, +): PluginUpdateProposalInValidityCause[] { + const causes: PluginUpdateProposalInValidityCause[] = []; + const decodedPermission = decodeGrantAction(action.data); + if (action.value.toString() !== "0") { + causes.push( + PluginUpdateProposalInValidityCause + .NON_ZERO_GRANT_ROOT_PERMISSION_CALL_VALUE, + ); + } + if (decodedPermission.where !== daoAddress) { + causes.push( + PluginUpdateProposalInValidityCause + .INVALID_GRANT_ROOT_PERMISSION_WHERE_ADDRESS, + ); + } + if (decodedPermission.who !== pspAddress) { + causes.push( + PluginUpdateProposalInValidityCause + .INVALID_GRANT_ROOT_PERMISSION_WHO_ADDRESS, + ); + } + if ( + decodedPermission.permission !== Permissions.ROOT_PERMISSION + ) { + causes.push( + PluginUpdateProposalInValidityCause + .INVALID_GRANT_ROOT_PERMISSION_PERMISSION_NAME, + ); + } + if ( + decodedPermission.permissionId !== + PermissionIds.ROOT_PERMISSION_ID + ) { + causes.push( + PluginUpdateProposalInValidityCause + .INVALID_GRANT_ROOT_PERMISSION_PERMISSION_ID, + ); + } + return causes; +} +export function validateRevokeRootPermissionAction( + action: DaoAction, + daoAddress: string, + pspAddress: string, +): PluginUpdateProposalInValidityCause[] { + const causes: PluginUpdateProposalInValidityCause[] = []; + const decodedPermission = decodeRevokeAction(action.data); + if (action.value.toString() !== "0") { + causes.push( + PluginUpdateProposalInValidityCause + .NON_ZERO_REVOKE_ROOT_PERMISSION_CALL_VALUE, + ); + } + if (decodedPermission.where !== daoAddress) { + causes.push( + PluginUpdateProposalInValidityCause + .INVALID_REVOKE_ROOT_PERMISSION_WHERE_ADDRESS, + ); + } + if (decodedPermission.who !== pspAddress) { + causes.push( + PluginUpdateProposalInValidityCause + .INVALID_REVOKE_ROOT_PERMISSION_WHO_ADDRESS, + ); + } + if ( + decodedPermission.permission !== Permissions.ROOT_PERMISSION + ) { + causes.push( + PluginUpdateProposalInValidityCause + .INVALID_REVOKE_ROOT_PERMISSION_PERMISSION_NAME, + ); + } + if ( + decodedPermission.permissionId !== + PermissionIds.ROOT_PERMISSION_ID + ) { + causes.push( + PluginUpdateProposalInValidityCause + .INVALID_REVOKE_ROOT_PERMISSION_PERMISSION_ID, + ); + } + 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, + graphql: IClientGraphQLCore, + ipfs: IClientIpfsCore, +): Promise { + const causes: PluginUpdateProposalInValidityCause[] = []; + if (action.value.toString() !== "0") { + causes.push( + PluginUpdateProposalInValidityCause.NON_ZERO_APPLY_UPDATE_CALL_VALUE, + ); + } + const decodedParams = decodeApplyUpdateAction( + action.data, + ); + // get dao with plugins + type U = { dao: SubgraphDao }; + const { dao } = await graphql.request({ + query: QueryDao, + params: { address: daoAddress }, + name: "dao", + }); + // find the plugin with the same address + const plugin = dao.plugins.find((plugin) => + plugin.appliedPreparation?.pluginAddress === + decodedParams.pluginAddress + ); + if (!plugin) { + causes.push(PluginUpdateProposalInValidityCause.PLUGIN_NOT_INSTALLED); + return causes; + } + // check release is the same as the one installed + if ( + plugin.appliedVersion?.release.release !== + decodedParams.versionTag.release + ) { + causes.push( + PluginUpdateProposalInValidityCause.UPDATE_TO_INCOMPATIBLE_RELEASE, + ); + } + // check build is higher than the one installed + if ( + !plugin.appliedVersion?.build || + plugin.appliedVersion?.build >= + decodedParams.versionTag.build + ) { + causes.push( + PluginUpdateProposalInValidityCause.UPDATE_TO_OLDER_OR_SAME_BUILD, + ); + } + // check if plugin repo (pluginSetupRepo) exist + type V = { pluginRepo: SubgraphPluginRepo }; + const { pluginRepo } = await graphql.request({ + query: QueryPlugin, + params: { id: decodedParams.pluginRepo }, + name: "pluginRepo", + }); + if (!pluginRepo) { + causes.push(PluginUpdateProposalInValidityCause.MISSING_PLUGIN_REPO); + return causes; + } + // check if is one of the aragon plugin repos + if ( + !SupportedPluginRepoArray.includes( + pluginRepo.subdomain as SupportedPluginRepo, + ) + ) { + causes.push(PluginUpdateProposalInValidityCause.NOT_ARAGON_PLUGIN_REPO); + } + + // get the prepared setup id + const preparedSetupId = getPreparedSetupId( + decodedParams, + PreparationType.UPDATE, + ); + // get plugin preparation + type W = { pluginPreparation: SubgraphPluginUpdatePreparation }; + const { pluginPreparation } = await graphql.request({ + query: QueryPluginPreparations, + params: { where: { preparedSetupId } }, + name: "pluginPreparation", + }); + if (!pluginPreparation) { + causes.push( + PluginUpdateProposalInValidityCause.MISSING_PLUGIN_PREPARATION, + ); + return causes; + } + // get the metadata of the plugin repo + // for the release and build specified + const release = pluginRepo.releases.find(( + release: SubgraphPluginRepoRelease, + ) => release.release === decodedParams.versionTag.release); + const build = release?.builds.find(( + build: { build: number; metadata: string }, + ) => build.build === decodedParams.versionTag.build); + const metadataCid = build?.metadata; + + // fetch the metadata + 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 the abi exists try to decode the data + const updateAbi = metadataJson.pluginSetup.prepareUpdate[ + decodedParams.versionTag.build + ].inputs; + try { + if ( + decodedParams.initData.length > 0 && + updateAbi.length === 0 + ) { + throw new Error(); + } + // if the decode does not throw an error the data is valid + defaultAbiCoder.decode( + getNamedTypesFromMetadata(updateAbi), + decodedParams.initData, + ); + } catch { + // if the decode throws an error the data is invalid + causes.push( + PluginUpdateProposalInValidityCause.INVALID_DATA, + ); + } + } else { + causes.push( + PluginUpdateProposalInValidityCause.INVALID_PLUGIN_REPO_METADATA, + ); + } + 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[] { + const classifiedActions: ProposalActionTypes[] = []; + + for (const action of actions) { + try { + let decodedPermission: + | GrantPermissionDecodedParams + | RevokePermissionDecodedParams; + const func = getFunctionFragment(action.data, UPDATE_PLUGIN_SIGNATURES); + switch (func.name) { + case "upgradeTo": + classifiedActions.push(ProposalActionTypes.UPGRADE_TO); + break; + case "upgradeToAndCall": + classifiedActions.push(ProposalActionTypes.UPGRADE_TO_AND_CALL); + break; + case "grant": + decodedPermission = decodeGrantAction(action.data); + // check the permission that is being granted + switch (decodedPermission.permission) { + case Permissions.UPGRADE_PLUGIN_PERMISSION: + classifiedActions.push( + ProposalActionTypes.GRANT_PLUGIN_UPGRADE_PERMISSION, + ); + break; + case Permissions.ROOT_PERMISSION: + classifiedActions.push( + ProposalActionTypes.GRANT_ROOT_PERMISSION, + ); + break; + default: + classifiedActions.push( + ProposalActionTypes.UNKNOWN, + ); + break; + } + break; + case "revoke": + decodedPermission = decodeRevokeAction(action.data); + // check the permission that is being granted + switch (decodedPermission.permission) { + case Permissions.UPGRADE_PLUGIN_PERMISSION: + classifiedActions.push( + ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION, + ); + break; + case Permissions.ROOT_PERMISSION: + classifiedActions.push( + ProposalActionTypes.REVOKE_ROOT_PERMISSION, + ); + break; + default: + classifiedActions.push( + ProposalActionTypes.UNKNOWN, + ); + break; + } + break; + case "applyUpdate": + classifiedActions.push(ProposalActionTypes.APPLY_UPDATE); + break; + default: + classifiedActions.push(ProposalActionTypes.ACTION_NOT_ALLOWED); + break; + } + } catch { + classifiedActions.push(ProposalActionTypes.UNKNOWN); + } + } + 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 containsPluginUpdateActionBlockWithRootPermission( + actions: ProposalActionTypes[], +): boolean { + // get the first 5 actions + const receivedPattern = actions.slice(0, 5); + // check if it matches the expected pattern + // length should be 5 + return receivedPattern.length === 5 && + compareArrays(receivedPattern, PLUGIN_UPDATE_WITH_ROOT_ACTION_PATTERN); +} + +/** + * Returns true if the actions are valid for a plugin update proposal without root permission + * + * @export + * @param {ProposalActionTypes[]} actions + * @return {*} {boolean} + */ +export function containsPluginUpdateActionBlock( + actions: ProposalActionTypes[], +): boolean { + // get the first 3 action + const receivedPattern = actions.slice(0, 3); + // check if it matches the expected pattern + // length should be 3 + return receivedPattern.length === 3 && + compareArrays(receivedPattern, PLUGIN_UPDATE_ACTION_PATTERN); +} +/** + * Returns true if the actions are valid for a plugin update proposal + * + * @export + * @param {ProposalActionTypes[]} actions + * @return {*} {boolean} + */ +export function containsDaoUpdateAction( + 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, + expectedImplementationAddress: string, + currentDaoVersion: [number, number, number], +): DaoUpdateProposalValidity { + const classifiedActions = classifyProposalActions(actions); + const actionErrorCauses: DaoUpdateProposalInvalidityCause[] = []; + const proposalSettingsErrorCauses: ProposalSettingsErrorCause[] = []; + // check if the actions are valid + if (!containsDaoUpdateAction(classifiedActions)) { + // If actions are not valid, add the cause to the causes array + // and return + return { + isValid: false, + proposalSettingsErrorCauses: [ProposalSettingsErrorCause.INVALID_ACTIONS], + actionErrorCauses: [], + }; + } + // if they are valid, this means that + // 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) { + actionErrorCauses.push( + DaoUpdateProposalInvalidityCause.INVALID_TO_ADDRESS, + ); + } + // if the value is different from 0 + // add the cause to the causes array + if (upgradeAction.value.toString() !== "0") { + actionErrorCauses.push( + DaoUpdateProposalInvalidityCause.NON_ZERO_CALL_VALUE, + ); + } + switch (upgradeActionType) { + case ProposalActionTypes.UPGRADE_TO: + // decode the upgradeTo action + const decodedImplementationAddress = decodeUpgradeToAction( + actions[0].data, + ); + // check that the implementation address is the same + if (expectedImplementationAddress !== decodedImplementationAddress) { + actionErrorCauses.push( + DaoUpdateProposalInvalidityCause + .INVALID_UPGRADE_TO_IMPLEMENTATION_ADDRESS, + ); + } + break; + case 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 ( + expectedImplementationAddress !== + upgradeToAndCallDecodedParams.implementationAddress + ) { + actionErrorCauses.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) + ) { + actionErrorCauses.push( + DaoUpdateProposalInvalidityCause.INVALID_UPGRADE_TO_AND_CALL_VERSION, + ); + } + + // For now, we check that the `bytes calldata _initData` parameter of the `initializeFrom` function call is empty (because updates related to 1.0.0, 1.3.0, or 1.4.0 don't require `_initData`). + // TODO For future upgrade requiring non-empty `_initData`, we must define a place to obtain this information from permissionlessly. + if (initializeFromDecodedParams.initData.length !== 0) { + actionErrorCauses.push( + DaoUpdateProposalInvalidityCause.INVALID_UPGRADE_TO_AND_CALL_DATA, + ); + } + break; + default: + proposalSettingsErrorCauses.push( + ProposalSettingsErrorCause.INVALID_ACTIONS, + ); + break; + } + // return the validity of the proposal + return { + isValid: actionErrorCauses.length === 0 && + proposalSettingsErrorCauses.length === 0, + actionErrorCauses, + proposalSettingsErrorCauses, + }; +} + +export async function validateUpdatePluginProposalActions( + actions: DaoAction[], + daoAddress: string, + pspAddress: string, + graphql: IClientGraphQLCore, + ipfs: IClientIpfsCore, +): Promise { + // Declare variables + let actionErrorCauses: PluginUpdateProposalInValidityCause[][] = []; + let resCauses: PluginUpdateProposalInValidityCause[]; + let proposalSettingsErrorCauses: ProposalSettingsErrorCause[] = []; + const classifiedActions = classifyProposalActions(actions); + // check if is an update plugin proposal + if (containsPluginUpdateActionBlock(classifiedActions)) { + // initialize the causes array + // we always use the index 0 + // because this is going to be called recursively + // and then joined + actionErrorCauses[0] = []; + // iterate over the first 3 actions actions + for (const [index, action] of classifiedActions.slice(0, 3).entries()) { + switch (action) { + // if the action is grant plugin update permission + // validate the action + case ProposalActionTypes.GRANT_PLUGIN_UPGRADE_PERMISSION: + resCauses = await validateGrantUpgradePluginPermissionAction( + actions[index], + pspAddress, + daoAddress, + graphql, + ); + actionErrorCauses[0] = [...actionErrorCauses[0], ...resCauses]; + break; + // if the action is revoke plugin update permission + // validate the action + case ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION: + resCauses = await validateRevokeUpgradePluginPermissionAction( + actions[index], + pspAddress, + daoAddress, + graphql, + ); + actionErrorCauses[0] = [...actionErrorCauses[0], ...resCauses]; + break; + // if the action is grant root permission + // validate the action + case ProposalActionTypes.APPLY_UPDATE: + resCauses = await validateApplyUpdateFunction( + actions[index], + daoAddress, + graphql, + ipfs, + ); + actionErrorCauses[0] = [...actionErrorCauses[0], ...resCauses]; + break; + // other cases are not allowed and should have been + // caught by the containsPluginUpdateActionBlock function + } + } + // slice the first 3 actions + // because they have already been validated + // and recursively call the function + actions = actions.slice(3); + if (actions.length !== 0) { + // recursively call the function + const recCauses = await validateUpdatePluginProposalActions( + actions, + daoAddress, + pspAddress, + graphql, + ipfs, + ); + // join the causes + actionErrorCauses = [ + ...actionErrorCauses, + ...recCauses.actionErrorCauses, + ]; + proposalSettingsErrorCauses = [ + ...proposalSettingsErrorCauses, + ...recCauses.proposalSettingsErrorCauses, + ]; + } + return { + // every item in the array should be empty + isValid: actionErrorCauses.every((cause) => cause.length === 0) && + proposalSettingsErrorCauses.length === 0, + actionErrorCauses, + proposalSettingsErrorCauses, + }; + } + + if (containsPluginUpdateActionBlockWithRootPermission(classifiedActions)) { + // initialize the causes array + // we always use the index 0 + // because this is going to be called recursively + // and then joined + actionErrorCauses[0] = []; + // iterate over the first 5 actions actions + for (const [index, action] of classifiedActions.slice(0, 5).entries()) { + switch (action) { + // if the action is grant plugin update permission + // validate the action + case ProposalActionTypes.GRANT_PLUGIN_UPGRADE_PERMISSION: + resCauses = await validateGrantUpgradePluginPermissionAction( + actions[index], + pspAddress, + daoAddress, + graphql, + ); + actionErrorCauses[0] = [...actionErrorCauses[0], ...resCauses]; + break; + // if the action is revoke plugin update permission + // validate the action + case ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION: + resCauses = await validateRevokeUpgradePluginPermissionAction( + actions[index], + pspAddress, + daoAddress, + graphql, + ); + actionErrorCauses[0] = [...actionErrorCauses[0], ...resCauses]; + break; + // if the action is grant root permission + // validate the action + case ProposalActionTypes.GRANT_ROOT_PERMISSION: + resCauses = validateGrantRootPermissionAction( + actions[index], + daoAddress, + pspAddress, + ); + actionErrorCauses[0] = [...actionErrorCauses[0], ...resCauses]; + break; + // if the action is revoke root permission + // validate the action + + case ProposalActionTypes.REVOKE_ROOT_PERMISSION: + resCauses = validateRevokeRootPermissionAction( + actions[index], + daoAddress, + pspAddress, + ); + actionErrorCauses[0] = [...actionErrorCauses[0], ...resCauses]; + break; + // if the action is apply update + // validate the action + case ProposalActionTypes.APPLY_UPDATE: + resCauses = await validateApplyUpdateFunction( + actions[index], + daoAddress, + graphql, + ipfs, + ); + actionErrorCauses[0] = [...actionErrorCauses[0], ...resCauses]; + break; + + // other cases are not allowed and should have been + // caught by the containsPluginUpdateActionBlockWithRootPermission function + } + } + // slice the first 5 actions + // because they have already been validated + actions = actions.slice(5); + if (actions.length !== 0) { + // recursively call the function + const recCauses = await validateUpdatePluginProposalActions( + actions, + daoAddress, + pspAddress, + graphql, + ipfs, + ); + // join the causes + actionErrorCauses = [ + ...actionErrorCauses, + ...recCauses.actionErrorCauses, + ]; + proposalSettingsErrorCauses = [ + ...proposalSettingsErrorCauses, + ...recCauses.proposalSettingsErrorCauses, + ]; + } + return { + // every item in the array should be empty + isValid: actionErrorCauses.every((cause) => cause.length === 0) && + proposalSettingsErrorCauses.length === 0, + actionErrorCauses, + proposalSettingsErrorCauses, + }; + } + // add invalid actions to the causes array + // return, if this is inside the recursion + // it will be added to the array + return { + isValid: false, + proposalSettingsErrorCauses: [ProposalSettingsErrorCause.INVALID_ACTIONS], + actionErrorCauses: actionErrorCauses, + }; +} + +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 041b12b51..dcd87321b 100644 --- a/modules/client/src/types.ts +++ b/modules/client/src/types.ts @@ -1,11 +1,9 @@ import { - DaoAction, MetadataAbiInput, MultiTargetPermission, Pagination, PluginInstallItem, TokenType, - VersionTag, } from "@aragon/sdk-client-common"; /* DAO creation */ @@ -420,50 +418,88 @@ export type DecodedInitializeFromParams = { export type PluginUpdateProposalValidity = { isValid: boolean; - causes: PluginUpdateProposalInValidityCause[]; + actionErrorCauses: PluginUpdateProposalInValidityCause[][]; + proposalSettingsErrorCauses: ProposalSettingsErrorCause[]; }; -export enum PluginUpdateProposalInValidityCause { +export enum ProposalSettingsErrorCause { + NON_ZERO_ALLOW_FAILURE_MAP_VALUE = "nonZeroAllowFailureMapValue", INVALID_ACTIONS = "invalidActions", - INVALID_GRANT_PERMISSION = "invalidGrantPermission", - INVALID_REVOKE_PERMISSION = "invalidRevokePermission", + PROPOSAL_NOT_FOUND = "proposalNotFound", +} + +export enum PluginUpdateProposalInValidityCause { + // Grant UPDATE_PLUGIN_PERMISSION action + INVALID_GRANT_UPGRADE_PLUGIN_PERMISSION_WHO_ADDRESS = + "invalidGrantUpgradePluginPermissionWhoAddress", + INVALID_GRANT_UPGRADE_PLUGIN_PERMISSION_WHERE_ADDRESS = + "invalidGrantUpgradePluginPermissionWhereAddress", + INVALID_GRANT_UPGRADE_PLUGIN_PERMISSION_PERMISSION_NAME = + "invalidGrantUpgradePluginPermissionPermissionName", + NON_ZERO_GRANT_UPGRADE_PLUGIN_PERMISSION_CALL_VALUE = + "nonZeroGrantUpgradePluginPermissionCallValue", + INVALID_GRANT_UPGRADE_PLUGIN_PERMISSION_PERMISSION_ID = + "invalidGrantUpgradePluginPermissionPermissionId", + // Revoke UPDATE_PLUGIN_PERMISSION action + INVALID_REVOKE_UPGRADE_PLUGIN_PERMISSION_WHO_ADDRESS = + "invalidRevokeUpgradePluginPermissionWhoAddress", + INVALID_REVOKE_UPGRADE_PLUGIN_PERMISSION_WHERE_ADDRESS = + "invalidRevokeUpgradePluginPermissionWhereAddress", + INVALID_REVOKE_UPGRADE_PLUGIN_PERMISSION_PERMISSION_NAME = + "invalidRevokeUpgradePluginPermissionPermissionName", + NON_ZERO_REVOKE_UPGRADE_PLUGIN_PERMISSION_CALL_VALUE = + "nonZeroRevokeUpgradePluginPermissionCallValue", + INVALID_REVOKE_UPGRADE_PLUGIN_PERMISSION_PERMISSION_ID = + "invalidRevokeUpgradePluginPermissionPermissionId", + // Grant ROOT_PERMISSION action + INVALID_GRANT_ROOT_PERMISSION_WHO_ADDRESS = + "invalidGrantRootPermissionWhoAddress", + INVALID_GRANT_ROOT_PERMISSION_WHERE_ADDRESS = + "invalidGrantRootPermissionWhereAddress", + INVALID_GRANT_ROOT_PERMISSION_PERMISSION_NAME = + "invalidGrantRootPermissionPermissionName", + NON_ZERO_GRANT_ROOT_PERMISSION_CALL_VALUE = + "nonZeroGrantRootPermissionCallValue", + INVALID_GRANT_ROOT_PERMISSION_PERMISSION_ID = + "invalidGrantRootPermissionPermissionId", + // Revoke ROOT_PERMISSION action + INVALID_REVOKE_ROOT_PERMISSION_WHO_ADDRESS = + "invalidRevokeRootPermissionWhoAddress", + INVALID_REVOKE_ROOT_PERMISSION_WHERE_ADDRESS = + "invalidRevokeRootPermissionWhereAddress", + INVALID_REVOKE_ROOT_PERMISSION_PERMISSION_NAME = + "invalidRevokeRootPermissionPermissionName", + NON_ZERO_REVOKE_ROOT_PERMISSION_CALL_VALUE = + "nonZeroRevokeRootPermissionCallValue", + INVALID_REVOKE_ROOT_PERMISSION_PERMISSION_ID = + "invalidRevokeRootPermissionPermissionId", + // applyUpdate action + NON_ZERO_APPLY_UPDATE_CALL_VALUE = "nonZeroApplyUpdateCallValue", PLUGIN_NOT_INSTALLED = "pluginNotInstalled", NOT_ARAGON_PLUGIN_REPO = "notAragonPluginRepo", MISSING_PLUGIN_REPO = "missingPluginRepo", MISSING_PLUGIN_PREPARATION = "missingPluginPreparation", - INVALID_ALLOW_FAILURE_MAP = "invalidAllowFailureMap", - INVALID_PLUGIN_RELEASE = "invalidPluginRelease", - INVALID_PLUGIN_BUILD = "invalidPluginBuild", + UPDATE_TO_INCOMPATIBLE_RELEASE = "updateToIncompatibleRelease", + UPDATE_TO_OLDER_OR_SAME_BUILD = "updateToOlderOrSameBuild", INVALID_DATA = "invalidData", INVALID_PLUGIN_REPO_METADATA = "invalidPluginRepoMetadata", } -export type IsPluginUpdateProposalValidParams = { - proposalId: string; - version: VersionTag; - pluginAddress: string; - pluginPreparationIndex?: number; -}; - export enum DaoUpdateProposalInvalidityCause { - INVALID_ACTIONS = "invalidActions", - INVALID_IMPLEMENTATION = "invalidImplementation", - INVALID_VERSION = "invalidVersion", - INVALID_INIT_DATA = "invalidInitData", + NON_ZERO_CALL_VALUE = "nonZeroCallValue", + INVALID_TO_ADDRESS = "invalidToAddress", + 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 = { isValid: boolean; - causes: DaoUpdateProposalInvalidityCause[]; -}; - -type IsUpdateParamsBase = { - actions: DaoAction[]; - daoAddress: string; -}; - -export type IsDaoUpdateValidParams = IsUpdateParamsBase & { - version?: [number, number, number]; + actionErrorCauses: DaoUpdateProposalInvalidityCause[]; + proposalSettingsErrorCauses: ProposalSettingsErrorCause[]; }; export type DaoUpdateParams = InitializeFromParams & { @@ -473,7 +509,6 @@ export type DaoUpdateParams = InitializeFromParams & { export type DaoUpdateDecodedParams = InitializeFromParams & { implementationAddress: string; }; -export type IsPluginUpdateValidParams = IsUpdateParamsBase; export type PluginPreparationQueryParams = Pagination & { sortBy?: PluginPreparationSortBy; type?: PluginPreparationType; diff --git a/modules/client/test/integration/client/decoding.test.ts b/modules/client/test/integration/client/decoding.test.ts index 0607f69d5..135b5ffe9 100644 --- a/modules/client/test/integration/client/decoding.test.ts +++ b/modules/client/test/integration/client/decoding.test.ts @@ -759,14 +759,14 @@ describe("Client", () => { pluginRepo: "0x2345678901234567890123456789012345678901", pluginAddress: "0x1234567890123456789012345678901234567890", }; - const actions = client.encoding.applyUpdateAction( + const actions = client.encoding.applyUpdateAndPermissionsActionBlock( "0x1234567890123456789012345678901234567890", applyUpdateParams, ); - expect(actions.length).toBe(3); + expect(actions.length).toBe(5); const decodedApplyUpdateParams = client.decoding .applyUpdateAction( - actions[1].data, + actions[2].data, ); expect(applyUpdateParams.versionTag.build).toBe( decodedApplyUpdateParams.versionTag.build, diff --git a/modules/client/test/integration/client/encoding.test.ts b/modules/client/test/integration/client/encoding.test.ts index 142f7412c..f93eb5e6b 100644 --- a/modules/client/test/integration/client/encoding.test.ts +++ b/modules/client/test/integration/client/encoding.test.ts @@ -585,17 +585,12 @@ describe("Client", () => { ); } }); - it("Should encode an applyUpdate action", async () => { + it("Should encode an applyUpdate action without permissions", async () => { const context = new Context(contextParamsLocalChain); const client = new Client(context); const applyUpdateParams: ApplyUpdateParams = { - permissions: [{ - operation: 1, - permissionId: PermissionIds.EXECUTE_PERMISSION_ID, - where: "0x1234567890123456789012345678901234567890", - who: "0x2345678901234567890123456789012345678901", - }], + permissions: [], versionTag: { build: 1, release: 1, @@ -606,54 +601,204 @@ describe("Client", () => { helpers: [], }; const daoAddress = "0x1234567890123456789012345678901234567890"; - const actions = client.encoding.applyUpdateAction( + const actions = client.encoding.applyUpdateAndPermissionsActionBlock( daoAddress, applyUpdateParams, ); - + const pspInterface = PluginSetupProcessor__factory.createInterface(); + const daoInterface = DAO__factory.createInterface(); + const expectedActions = ["grant", "applyUpdate", "revoke"]; + let hexString, argsDecoded, action; expect(actions.length).toBe(3); - expect(typeof actions[1]).toBe("object"); - expect(actions[1].data).toBeInstanceOf(Uint8Array); + for (const [index, actionName] of expectedActions.entries()) { + + action = actions[index]; + expect(typeof action).toBe("object"); + expect(action.data).toBeInstanceOf(Uint8Array); + hexString = bytesToHex(action.data); + switch (actionName) { + case "grant": + case "revoke": + argsDecoded = daoInterface.decodeFunctionData( + actionName, + hexString, + ); + expect(argsDecoded.length).toBe(3); + expect(argsDecoded[0]).toBe( + daoAddress, + ); + expect(argsDecoded[1]).toBe( + contextParamsLocalChain.pluginSetupProcessorAddress, + ); + expect(argsDecoded[2]).toBe( + keccak256(toUtf8Bytes(Permissions.UPGRADE_PLUGIN_PERMISSION)), + ); + break; + case "applyUpdate": + argsDecoded = pspInterface.decodeFunctionData( + actionName, + hexString, + ); + expect(argsDecoded.length).toBe(2); + expect(argsDecoded[0]).toBe( + daoAddress, + ); + expect(argsDecoded[1].pluginSetupRef.versionTag.build).toBe( + applyUpdateParams.versionTag.build, + ); + expect(argsDecoded[1].pluginSetupRef.versionTag.release).toBe( + applyUpdateParams.versionTag.release, + ); + expect(argsDecoded[1].plugin).toBe( + applyUpdateParams.pluginAddress, + ); + expect(argsDecoded[1].pluginSetupRef.pluginSetupRepo).toBe( + applyUpdateParams.pluginRepo, + ); + for (const index in argsDecoded[1].permissions) { + expect(argsDecoded[1].permissions[parseInt(index)].operation) + .toBe( + applyUpdateParams.permissions[parseInt(index)].operation, + ); + expect(argsDecoded[1].permissions[parseInt(index)].where).toBe( + applyUpdateParams.permissions[parseInt(index)].where, + ); + expect(argsDecoded[1].permissions[parseInt(index)].who).toBe( + applyUpdateParams.permissions[parseInt(index)].who, + ); + expect(argsDecoded[1].permissions[parseInt(index)].condition) + .toBe( + AddressZero, + ); + expect(argsDecoded[1].permissions[parseInt(index)].permissionId) + .toBe( + applyUpdateParams.permissions[parseInt(index)] + .permissionId, + ); + } + break; + default: + throw new Error("Unexpected action name"); + } + } + }); + it("Should encode an applyUpdate action with permissions", async () => { + const context = new Context(contextParamsLocalChain); + const client = new Client(context); - const daoInterface = PluginSetupProcessor__factory.createInterface(); - const hexString = bytesToHex(actions[1].data); - const argsDecoded = daoInterface.decodeFunctionData( - "applyUpdate", - hexString, - ); - expect(argsDecoded.length).toBe(2); - expect(argsDecoded[0]).toBe( + const daoAddress = ADDRESS_ONE; + const pluginAddress = ADDRESS_TWO; + const applyUpdateParams: ApplyUpdateParams = { + permissions: [{ + operation: 1, + permissionId: PermissionIds.EXECUTE_PERMISSION_ID, + where: "0x1234567890123456789012345678901234567890", + who: "0x2345678901234567890123456789012345678901", + }], + versionTag: { + build: 1, + release: 1, + }, + pluginRepo: "0x2345678901234567890123456789012345678901", + pluginAddress, + initData: new Uint8Array([0, 1, 2, 3]), + helpers: [], + }; + const actions = client.encoding.applyUpdateAndPermissionsActionBlock( daoAddress, + applyUpdateParams, ); - expect(argsDecoded[1].pluginSetupRef.versionTag.build).toBe( - applyUpdateParams.versionTag.build, - ); - expect(argsDecoded[1].pluginSetupRef.versionTag.release).toBe( - applyUpdateParams.versionTag.release, - ); - expect(argsDecoded[1].plugin).toBe( - applyUpdateParams.pluginAddress, - ); - expect(argsDecoded[1].pluginSetupRef.pluginSetupRepo).toBe( - applyUpdateParams.pluginRepo, - ); - for (const index in argsDecoded[1].permissions) { - expect(argsDecoded[1].permissions[parseInt(index)].operation).toBe( - applyUpdateParams.permissions[parseInt(index)].operation, - ); - expect(argsDecoded[1].permissions[parseInt(index)].where).toBe( - applyUpdateParams.permissions[parseInt(index)].where, - ); - expect(argsDecoded[1].permissions[parseInt(index)].who).toBe( - applyUpdateParams.permissions[parseInt(index)].who, - ); - expect(argsDecoded[1].permissions[parseInt(index)].condition).toBe( - AddressZero, - ); - expect(argsDecoded[1].permissions[parseInt(index)].permissionId).toBe( - applyUpdateParams.permissions[parseInt(index)] - .permissionId, - ); + const pspInterface = PluginSetupProcessor__factory.createInterface(); + const daoInterface = DAO__factory.createInterface(); + const expectedActions = ["grant", "grant", "applyUpdate", "revoke", "revoke"]; + let hexString, argsDecoded, action; + expect(actions.length).toBe(5); + for (const [index, actionName] of expectedActions.entries()) { + action = actions[index]; + expect(typeof action).toBe("object"); + expect(action.data).toBeInstanceOf(Uint8Array); + hexString = bytesToHex(action.data); + switch (index) { + case 1: + case 3: + argsDecoded = daoInterface.decodeFunctionData( + actionName, + hexString, + ); + expect(argsDecoded.length).toBe(3); + expect(argsDecoded[0]).toBe( + daoAddress, + ); + expect(argsDecoded[1]).toBe( + contextParamsLocalChain.pluginSetupProcessorAddress, + ); + expect(argsDecoded[2]).toBe( + keccak256(toUtf8Bytes(Permissions.ROOT_PERMISSION)), + ); + break; + case 0: + case 4: + argsDecoded = daoInterface.decodeFunctionData( + actionName, + hexString, + ); + expect(argsDecoded.length).toBe(3); + expect(argsDecoded[0]).toBe( + pluginAddress, + ); + expect(argsDecoded[1]).toBe( + contextParamsLocalChain.pluginSetupProcessorAddress, + ); + expect(argsDecoded[2]).toBe( + keccak256(toUtf8Bytes(Permissions.UPGRADE_PLUGIN_PERMISSION)), + ); + break; + case 2: + argsDecoded = pspInterface.decodeFunctionData( + actionName, + hexString, + ); + expect(argsDecoded.length).toBe(2); + expect(argsDecoded[0]).toBe( + daoAddress, + ); + expect(argsDecoded[1].pluginSetupRef.versionTag.build).toBe( + applyUpdateParams.versionTag.build, + ); + expect(argsDecoded[1].pluginSetupRef.versionTag.release).toBe( + applyUpdateParams.versionTag.release, + ); + expect(argsDecoded[1].plugin).toBe( + applyUpdateParams.pluginAddress, + ); + expect(argsDecoded[1].pluginSetupRef.pluginSetupRepo).toBe( + applyUpdateParams.pluginRepo, + ); + for (const index in argsDecoded[1].permissions) { + expect(argsDecoded[1].permissions[parseInt(index)].operation) + .toBe( + applyUpdateParams.permissions[parseInt(index)].operation, + ); + expect(argsDecoded[1].permissions[parseInt(index)].where).toBe( + applyUpdateParams.permissions[parseInt(index)].where, + ); + expect(argsDecoded[1].permissions[parseInt(index)].who).toBe( + applyUpdateParams.permissions[parseInt(index)].who, + ); + expect(argsDecoded[1].permissions[parseInt(index)].condition) + .toBe( + AddressZero, + ); + expect(argsDecoded[1].permissions[parseInt(index)].permissionId) + .toBe( + applyUpdateParams.permissions[parseInt(index)] + .permissionId, + ); + } + break; + default: + throw new Error("Unexpected action name"); + } } }); diff --git a/modules/client/test/integration/client/methods.test.ts b/modules/client/test/integration/client/methods.test.ts index 4f6417f32..bd723a479 100644 --- a/modules/client/test/integration/client/methods.test.ts +++ b/modules/client/test/integration/client/methods.test.ts @@ -1,7 +1,6 @@ // mocks need to be at the top of the imports import { mockedIPFSClient } from "../../mocks/aragon-sdk-ipfs"; import * as mockedGraphqlRequest from "../../mocks/graphql-request"; - import * as ganacheSetup from "../../helpers/ganache-setup"; import * as deployContracts from "../../helpers/deployContracts"; import * as deployV1Contracts from "../../helpers/deploy-v1-contracts"; @@ -31,10 +30,8 @@ import { DaoDepositSteps, DaoQueryParams, DaoSortBy, - DaoUpdateProposalInvalidityCause, DepositParams, HasPermissionParams, - InitializeFromParams, PluginPreparationQueryParams, PluginPreparationSortBy, PluginPreparationType, @@ -42,19 +39,19 @@ import { PluginRepoBuildMetadata, PluginRepoReleaseMetadata, PluginSortBy, - PluginUpdateProposalInValidityCause, + ProposalSettingsErrorCause, SetAllowanceParams, SetAllowanceSteps, TransferQueryParams, TransferSortBy, TransferType, - UpgradeToAndCallParams, VotingMode, } from "../../../src"; import { Server } from "ganache"; import { SubgraphBalance, SubgraphDao, + SubgraphIProposal, SubgraphPluginInstallation, SubgraphPluginPermissionOperation, SubgraphPluginPreparationListItem, @@ -99,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; @@ -1867,30 +1867,50 @@ describe("Client", () => { // "Invalid ENS name" // ); }); - describe("isPluginUpdateValid", () => { + describe("isPluginUpdateProposalValid", () => { + const context = new Context(); + const client = new Client(context); 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(() => { - const ctx = new Context(contextParamsLocalChain); - const client = new Client(ctx); + 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, + permissions: [], + helpers: [], }; - updateActions = client.encoding.applyUpdateAction( + + updateActions = client.encoding.applyUpdateAndPermissionsActionBlock( daoAddress, applyUpdateParams, ); + subgraphIProposal = { + dao: { + id: daoAddress, + }, + allowFailureMap: "0", + actions: toSubgraphActions(updateActions), + }; + subgraphDao = { id: daoAddress, subdomain: "test-tokenvoting-dao", @@ -1911,8 +1931,9 @@ describe("Client", () => { }, }], }; + subgraphPluginRepo = { - id: deployment.tokenVotingRepo.address, + id: ADDRESS_ONE, subdomain: SupportedPluginRepo.TOKEN_VOTING, releases: [ { @@ -1931,602 +1952,134 @@ describe("Client", () => { }, ], }; - subgraphPluginPreparation = { - data: "0x", - }; - }); - 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({ + it("Should return true if the update is valid", async () => { + mockedClient.request.mockResolvedValue({ + iproposal: subgraphIProposal, + pluginInstallations: [{ + id: ADDRESS_ONE, + }], 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(), + const res = await client.methods.isPluginUpdateProposalValid( + TEST_MULTISIG_PROPOSAL_ID, ); - 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); + expect(res.isValid).toBe(true); + expect(res.proposalSettingsErrorCauses).toMatchObject([]); + expect(res.actionErrorCauses).toMatchObject([[]]); }); - - 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({ + it("Should throw if the proposal does not exist", async () => { + mockedClient.request.mockResolvedValue({ + iproposal: null, + pluginInstallations: [{ + id: ADDRESS_ONE, + }], 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); + const res = await client.methods.isPluginUpdateProposalValid( + TEST_MULTISIG_PROPOSAL_ID, + ); + expect(res.isValid).toBe(false); + expect(res.proposalSettingsErrorCauses).toMatchObject([ + ProposalSettingsErrorCause.PROPOSAL_NOT_FOUND, + ]); + expect(res.actionErrorCauses).toMatchObject([]); }); - - 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({ + it("Should throw if the failure map is not 0", async () => { + mockedClient.request.mockResolvedValue({ + iproposal: { ...subgraphIProposal, allowFailureMap: "1" }, + pluginInstallations: [{ + id: ADDRESS_ONE, + }], 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); + const res = await client.methods.isPluginUpdateProposalValid( + TEST_MULTISIG_PROPOSAL_ID, + ); + expect(res.isValid).toBe(false); + expect(res.proposalSettingsErrorCauses).toMatchObject([ + ProposalSettingsErrorCause.NON_ZERO_ALLOW_FAILURE_MAP_VALUE, + ]); + expect(res.actionErrorCauses).toMatchObject([]); }); }); - describe("isDaoUpdateValid", () => { - let upgradeToAndCallAction: DaoAction; - let upgradeToAndCallParams: UpgradeToAndCallParams; - let initializeFromParams: InitializeFromParams; + describe("isDaoUpdateProposalValid", () => { + let client: Client; let initializeFromAction: DaoAction; - let implementationAddress: string; - beforeAll(async () => { - const ctx = new Context(contextParamsLocalChain); - const client = new Client(ctx); - initializeFromParams = { - previousVersion: [1, 0, 0], - }; + let upgradeToAndCallAction: DaoAction; + beforeAll(() => { + const context = new Context(contextParamsLocalChain); + client = new Client(context); initializeFromAction = client.encoding.initializeFromAction( - daoAddressV1, - initializeFromParams, - ); - implementationAddress = await client.methods.getDaoImplementation( - deployment.daoFactory.address, - ); - upgradeToAndCallParams = { - implementationAddress, - 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, + initData: new Uint8Array(), + previousVersion: [1, 0, 0], }, ); - 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], - }, + it("Should return true for a valid update", async () => { + const implementationAddress = await client.methods.getDaoImplementation( + client.web3.getAddress("daoFactoryAddress"), ); - const upgradeToAndCallAction = client.encoding.upgradeToAndCallAction( + upgradeToAndCallAction = client.encoding.upgradeToAndCallAction( daoAddressV1, { + data: initializeFromAction.data, 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 mockedClient = mockedGraphqlRequest.getMockedInstance( + client.graphql.getClient(), + ); + mockedClient.request.mockResolvedValueOnce({ + iproposal: { + dao: { + id: daoAddressV1, + }, + allowFailureMap: "0", + actions: toSubgraphActions([upgradeToAndCallAction]), }, + }); + const res = await client.methods.isDaoUpdateProposalValid( + TEST_MULTISIG_PROPOSAL_ID, ); - 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); + expect(res.isValid).toBe(true); + expect(res.actionErrorCauses).toMatchObject([]); + expect(res.proposalSettingsErrorCauses).toMatchObject([]); }); - 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]), - }, + it("Should PROPOSAL_NOT_FOUND when the proposal is null", async () => { + const implementationAddress = await client.methods.getDaoImplementation( + client.web3.getAddress("daoFactoryAddress"), ); - const upgradeToAndCallAction = client.encoding.upgradeToAndCallAction( + upgradeToAndCallAction = client.encoding.upgradeToAndCallAction( daoAddressV1, { + data: initializeFromAction.data, 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_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); + const mockedClient = mockedGraphqlRequest.getMockedInstance( + client.graphql.getClient(), + ); + mockedClient.request.mockResolvedValueOnce({ + iproposal: null, + }); + const res = await client.methods.isDaoUpdateProposalValid( + TEST_MULTISIG_PROPOSAL_ID, + ); + expect(res.isValid).toBe(false); + expect(res.proposalSettingsErrorCauses).toMatchObject([ProposalSettingsErrorCause.PROPOSAL_NOT_FOUND]); + expect(res.actionErrorCauses).toMatchObject([]); }); }); }); diff --git a/modules/client/test/unit/client/utils.test.ts b/modules/client/test/unit/client/utils.test.ts new file mode 100644 index 000000000..f778d210b --- /dev/null +++ b/modules/client/test/unit/client/utils.test.ts @@ -0,0 +1,1482 @@ +import * as mockedGraphqlRequest from "../../mocks/graphql-request"; +import { mockedIPFSClient } from "../../mocks/aragon-sdk-ipfs"; + +import { + ApplyUpdateParams, + Context, + DaoAction, + MultiTargetPermission, + PermissionIds, + Permissions, + TokenType, +} from "@aragon/sdk-client-common"; +import { + Client, + DaoUpdateProposalInvalidityCause, + PluginUpdateProposalInValidityCause, + ProposalSettingsErrorCause, +} from "../../../src"; +// import * as deployV1Contracts from "../../helpers/deploy-v1-contracts"; + +import { + ADDRESS_FOUR, + ADDRESS_ONE, + ADDRESS_THREE, + ADDRESS_TWO, + IPFS_CID, + TOKEN_VOTING_BUILD_METADATA, +} from "../../integration/constants"; +import { + containsDaoUpdateAction, + containsPluginUpdateActionBlock, + containsPluginUpdateActionBlockWithRootPermission, + validateApplyUpdateFunction, + validateGrantRootPermissionAction, + validateGrantUpgradePluginPermissionAction, + validateRevokeRootPermissionAction, + validateRevokeUpgradePluginPermissionAction, + validateUpdateDaoProposalActions, + validateUpdatePluginProposalActions, +} from "../../../src/internal/utils"; +import { + ProposalActionTypes, + SubgraphDao, + SubgraphPluginRepo, + SubgraphPluginUpdatePreparation, +} from "../../../src/internal/types"; +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("validateGrantUpgradePluginPermissionAction", () => { + 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 = await validateGrantUpgradePluginPermissionAction( + grantAction, + pspAddress, + daoAddress, + client.graphql, + ); + expect(result).toEqual([]); + }); + it("should return an error if the action is not a grant action", async () => { + expect(() => + validateGrantUpgradePluginPermissionAction( + { + to: daoAddress, + value: BigInt(0), + data: new Uint8Array(), + }, + pspAddress, + daoAddress, + client.graphql, + ) + ).rejects.toThrow(); + }); + 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 = await validateGrantUpgradePluginPermissionAction( + grantAction, + pspAddress, + daoAddress, + client.graphql, + ); + expect(result).toEqual([ + PluginUpdateProposalInValidityCause + .NON_ZERO_GRANT_UPGRADE_PLUGIN_PERMISSION_CALL_VALUE, + ]); + }); + 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, + }); + mockedClient.request.mockResolvedValueOnce({ + pluginInstallations: [], + }); + const result = await validateGrantUpgradePluginPermissionAction( + grantAction, + pspAddress, + daoAddress, + client.graphql, + ); + expect(result).toEqual([ + PluginUpdateProposalInValidityCause + .PLUGIN_NOT_INSTALLED, + ]); + }); + 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 = await validateGrantUpgradePluginPermissionAction( + grantAction, + pspAddress, + daoAddress, + client.graphql, + ); + expect(result).toEqual([ + PluginUpdateProposalInValidityCause + .INVALID_GRANT_UPGRADE_PLUGIN_PERMISSION_WHO_ADDRESS, + ]); + }); + 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 = await validateGrantUpgradePluginPermissionAction( + grantAction, + pspAddress, + daoAddress, + client.graphql, + ); + expect(result).toEqual([ + PluginUpdateProposalInValidityCause + .INVALID_GRANT_UPGRADE_PLUGIN_PERMISSION_PERMISSION_NAME, + PluginUpdateProposalInValidityCause + .INVALID_GRANT_UPGRADE_PLUGIN_PERMISSION_PERMISSION_ID, + ]); + }); + 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: pluginAddress, + who: daoAddress, + permission: Permissions.UPGRADE_PLUGIN_PERMISSION, + }); + mockedClient.request.mockResolvedValueOnce({ + pluginInstallations: [], + }); + const result = await validateGrantUpgradePluginPermissionAction( + grantAction, + pspAddress, + daoAddress, + client.graphql, + ); + expect(result).toEqual([ + PluginUpdateProposalInValidityCause + .PLUGIN_NOT_INSTALLED, + PluginUpdateProposalInValidityCause + .INVALID_GRANT_UPGRADE_PLUGIN_PERMISSION_WHO_ADDRESS, + ]); + }); + }); + describe("validateRevokeUpgradePluginPermissionAction", () => { + 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 = await validateRevokeUpgradePluginPermissionAction( + revokeAction, + pspAddress, + daoAddress, + client.graphql, + ); + expect(result).toEqual([]); + }); + it("should return an error if the action is not a revoke action", async () => { + expect(() => + validateRevokeUpgradePluginPermissionAction( + { + to: daoAddress, + value: BigInt(0), + data: new Uint8Array(), + }, + pspAddress, + daoAddress, + client.graphql, + ) + ).rejects.toThrow(); + }); + 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 = await validateRevokeUpgradePluginPermissionAction( + revokeAction, + pspAddress, + daoAddress, + client.graphql, + ); + expect(result).toEqual([ + PluginUpdateProposalInValidityCause + .NON_ZERO_REVOKE_UPGRADE_PLUGIN_PERMISSION_CALL_VALUE, + ]); + }); + it("should return an error if the installation does not exist", async () => { + const revokeAction = client.encoding.revokeAction(daoAddress, { + where: daoAddress, + who: pspAddress, + permission: Permissions.UPGRADE_PLUGIN_PERMISSION, + }); + mockedClient.request.mockResolvedValueOnce({ + pluginInstallations: [], + }); + const result = await validateRevokeUpgradePluginPermissionAction( + revokeAction, + pspAddress, + daoAddress, + client.graphql, + ); + expect(result).toEqual([ + PluginUpdateProposalInValidityCause + .PLUGIN_NOT_INSTALLED, + ]); + }); + 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 = await validateRevokeUpgradePluginPermissionAction( + revokeAction, + pspAddress, + daoAddress, + client.graphql, + ); + expect(result).toEqual([ + PluginUpdateProposalInValidityCause + .INVALID_REVOKE_UPGRADE_PLUGIN_PERMISSION_WHO_ADDRESS, + ]); + }); + 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 = await validateRevokeUpgradePluginPermissionAction( + revokeAction, + pspAddress, + daoAddress, + client.graphql, + ); + expect(result).toEqual([ + PluginUpdateProposalInValidityCause + .INVALID_REVOKE_UPGRADE_PLUGIN_PERMISSION_PERMISSION_NAME, + PluginUpdateProposalInValidityCause + .INVALID_REVOKE_UPGRADE_PLUGIN_PERMISSION_PERMISSION_ID, + ]); + }); + 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, + }); + mockedClient.request.mockResolvedValueOnce({ + pluginInstallations: [], + }); + const result = await validateRevokeUpgradePluginPermissionAction( + revokeAction, + pspAddress, + daoAddress, + client.graphql, + ); + expect(result).toEqual([ + PluginUpdateProposalInValidityCause + .PLUGIN_NOT_INSTALLED, + PluginUpdateProposalInValidityCause + .INVALID_REVOKE_UPGRADE_PLUGIN_PERMISSION_WHO_ADDRESS, + ]); + }); + }); + describe("validateGrantRootPermissionAction", () => { + beforeEach(() => { + mockedClient.request.mockReset(); + }); + it("should return an empty array for a valid action", () => { + const revokeAction = client.encoding.grantAction(daoAddress, { + where: daoAddress, + who: pspAddress, + permission: Permissions.ROOT_PERMISSION, + }); + const result = validateGrantRootPermissionAction( + revokeAction, + daoAddress, + pspAddress, + ); + expect(result).toEqual([]); + }); + it("should return an error if the action is not a revoke action", () => { + expect(() => + validateGrantRootPermissionAction( + { + to: daoAddress, + value: BigInt(0), + data: new Uint8Array(), + }, + pspAddress, + daoAddress, + ) + ).toThrow(); + }); + it("should return an error value of the action is not 0", () => { + const grantAction = client.encoding.grantAction(daoAddress, { + where: daoAddress, + who: pspAddress, + permission: Permissions.ROOT_PERMISSION, + }); + grantAction.value = BigInt(10); + const result = validateGrantRootPermissionAction( + grantAction, + daoAddress, + pspAddress, + ); + expect(result).toEqual([ + PluginUpdateProposalInValidityCause + .NON_ZERO_GRANT_ROOT_PERMISSION_CALL_VALUE, + ]); + }); + it("should return an error if the permission is not granted in the DAO", () => { + const grantAction = client.encoding.grantAction(daoAddress, { + where: pluginAddress, + who: pspAddress, + permission: Permissions.ROOT_PERMISSION, + }); + const result = validateGrantRootPermissionAction( + grantAction, + daoAddress, + pspAddress, + ); + expect(result).toEqual([ + PluginUpdateProposalInValidityCause + .INVALID_GRANT_ROOT_PERMISSION_WHERE_ADDRESS, + ]); + }); + it("should return an error if the permission is not granted to the psp", () => { + const grantAction = client.encoding.grantAction(daoAddress, { + where: daoAddress, + who: daoAddress, + permission: Permissions.ROOT_PERMISSION, + }); + const result = validateGrantRootPermissionAction( + grantAction, + daoAddress, + pspAddress, + ); + expect(result).toEqual([ + PluginUpdateProposalInValidityCause + .INVALID_GRANT_ROOT_PERMISSION_WHO_ADDRESS, + ]); + }); + it("should return an error if the permission is not correct", () => { + const grantAction = client.encoding.grantAction(daoAddress, { + where: daoAddress, + who: pspAddress, + permission: Permissions.MINT_PERMISSION, + }); + const result = validateGrantRootPermissionAction( + grantAction, + daoAddress, + pspAddress, + ); + expect(result).toEqual([ + PluginUpdateProposalInValidityCause + .INVALID_GRANT_ROOT_PERMISSION_PERMISSION_NAME, + PluginUpdateProposalInValidityCause + .INVALID_GRANT_ROOT_PERMISSION_PERMISSION_ID, + ]); + }); + it("should return two causes if the permission is not granted to the psp and is not granted in the plugin", () => { + const grantAction = client.encoding.grantAction(daoAddress, { + where: pluginAddress, + who: daoAddress, + permission: Permissions.ROOT_PERMISSION, + }); + const result = validateGrantRootPermissionAction( + grantAction, + daoAddress, + pspAddress, + ); + expect(result).toEqual([ + PluginUpdateProposalInValidityCause + .INVALID_GRANT_ROOT_PERMISSION_WHERE_ADDRESS, + PluginUpdateProposalInValidityCause + .INVALID_GRANT_ROOT_PERMISSION_WHO_ADDRESS, + ]); + }); + }); + describe("validateRevokeRootPermissionAction", () => { + beforeEach(() => { + mockedClient.request.mockReset(); + }); + it("should return an empty array for a valid action", () => { + const revokeAction = client.encoding.revokeAction(daoAddress, { + where: pluginAddress, + who: pspAddress, + permission: Permissions.ROOT_PERMISSION, + }); + const result = validateRevokeRootPermissionAction( + revokeAction, + pluginAddress, + pspAddress, + ); + expect(result).toEqual([]); + }); + it("should return an error if the action is not a revoke action", () => { + expect(() => + validateRevokeRootPermissionAction( + { + to: daoAddress, + value: BigInt(0), + data: new Uint8Array(), + }, + pspAddress, + daoAddress, + ) + ).toThrow(); + }); + it("should return an error value of the action is not 0", () => { + const revokeAction = client.encoding.revokeAction(daoAddress, { + where: daoAddress, + who: pspAddress, + permission: Permissions.ROOT_PERMISSION, + }); + revokeAction.value = BigInt(10); + const result = validateRevokeRootPermissionAction( + revokeAction, + daoAddress, + pspAddress, + ); + expect(result).toEqual([ + PluginUpdateProposalInValidityCause + .NON_ZERO_REVOKE_ROOT_PERMISSION_CALL_VALUE, + ]); + }); + it("should return an error if the permission is not revoked in the DAO", () => { + const revokeAction = client.encoding.revokeAction(daoAddress, { + where: pluginAddress, + who: pspAddress, + permission: Permissions.ROOT_PERMISSION, + }); + const result = validateRevokeRootPermissionAction( + revokeAction, + daoAddress, + pspAddress, + ); + expect(result).toEqual([ + PluginUpdateProposalInValidityCause + .INVALID_REVOKE_ROOT_PERMISSION_WHERE_ADDRESS, + ]); + }); + it("should return an error if the is not revoked from the psp", () => { + const revokeAction = client.encoding.revokeAction(daoAddress, { + where: daoAddress, + who: daoAddress, + permission: Permissions.ROOT_PERMISSION, + }); + const result = validateRevokeRootPermissionAction( + revokeAction, + daoAddress, + pspAddress, + ); + expect(result).toEqual([ + PluginUpdateProposalInValidityCause + .INVALID_REVOKE_ROOT_PERMISSION_WHO_ADDRESS, + ]); + }); + it("should return an error if the permission is not correct", () => { + const revokeAction = client.encoding.revokeAction(daoAddress, { + where: daoAddress, + who: pspAddress, + permission: Permissions.MINT_PERMISSION, + }); + const result = validateRevokeRootPermissionAction( + revokeAction, + daoAddress, + pspAddress, + ); + expect(result).toEqual([ + PluginUpdateProposalInValidityCause + .INVALID_REVOKE_ROOT_PERMISSION_PERMISSION_NAME, + PluginUpdateProposalInValidityCause + .INVALID_REVOKE_ROOT_PERMISSION_PERMISSION_ID, + ]); + }); + it("should return two causes if the permission is not granted to the psp and is not granted in the plugin", () => { + const revokeAction = client.encoding.revokeAction(daoAddress, { + where: pluginAddress, + who: daoAddress, + permission: Permissions.ROOT_PERMISSION, + }); + const result = validateRevokeRootPermissionAction( + revokeAction, + daoAddress, + pspAddress, + ); + expect(result).toEqual([ + PluginUpdateProposalInValidityCause + .INVALID_REVOKE_ROOT_PERMISSION_WHERE_ADDRESS, + PluginUpdateProposalInValidityCause + .INVALID_REVOKE_ROOT_PERMISSION_WHO_ADDRESS, + ]); + }); + }); + describe("validateApplyUpdateFunction", () => { + beforeEach(() => { + mockedClient.request.mockReset(); + }); + beforeAll(() => { + applyUpdateParams = { + versionTag: { + build: 2, + release: 1, + }, + initData: new Uint8Array(), + pluginRepo, + pluginAddress, + 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: tokenVotingRepoAddress, + subdomain: SupportedPluginRepo.TOKEN_VOTING, + releases: [ + { + release: 1, + metadata: `ipfs://${IPFS_CID}`, + builds: [ + { + build: 1, + metadata: `ipfs://${IPFS_CID}`, + }, + { + build: 2, + metadata: `ipfs://${IPFS_CID}`, + }, + ], + }, + ], + }; + subgraphPluginPreparation = { + data: "0x", + }; + }); + it("should return an empty array for a valid action", async () => { + const applyUpdateActions = client.encoding.applyUpdateAndPermissionsActionBlock( + daoAddress, + applyUpdateParams, + ); + const action = applyUpdateActions[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 result = await validateApplyUpdateFunction( + action, + daoAddress, + client.graphql, + client.ipfs, + ); + expect(result).toEqual([]); + }); + it("should return an `INVALID_APPLY_UPDATE_ACTION_VALUE` when the value in the action is not 0", async () => { + const applyUpdateActions = client.encoding.applyUpdateAndPermissionsActionBlock( + daoAddress, + applyUpdateParams, + ); + const action = applyUpdateActions[1]; + action.value = BigInt(10); + 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 result = await validateApplyUpdateFunction( + action, + daoAddress, + client.graphql, + client.ipfs, + ); + expect(result).toMatchObject([ + PluginUpdateProposalInValidityCause.NON_ZERO_APPLY_UPDATE_CALL_VALUE, + ]); + }); + it("should return an `UPDATE_TO_INCOMPATIBLE_RELEASE` when the release is different from the one on subgraph", async () => { + const updatedApplyUpdateParams = { + ...applyUpdateParams, + versionTag: { + release: 2, + build: 2, + }, + }; + const applyUpdateActions = client.encoding.applyUpdateAndPermissionsActionBlock( + daoAddress, + updatedApplyUpdateParams, + ); + const action = applyUpdateActions[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 result = await validateApplyUpdateFunction( + action, + daoAddress, + client.graphql, + client.ipfs, + ); + expect(result).toMatchObject([ + PluginUpdateProposalInValidityCause.UPDATE_TO_INCOMPATIBLE_RELEASE, + ]); + }); + it("should return an `UPDATE_TO_OLDER_OR_SAME_BUILD` when the release is different from the one on subgraph", async () => { + const updatedApplyUpdateParams = { + ...applyUpdateParams, + versionTag: { + release: 1, + build: 1, + }, + }; + const applyUpdateActions = client.encoding.applyUpdateAndPermissionsActionBlock( + daoAddress, + updatedApplyUpdateParams, + ); + const action = applyUpdateActions[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 result = await validateApplyUpdateFunction( + action, + daoAddress, + client.graphql, + client.ipfs, + ); + expect(result).toMatchObject([ + PluginUpdateProposalInValidityCause.UPDATE_TO_OLDER_OR_SAME_BUILD, + ]); + }); + it("should return an `PLUGIN_NOT_INSTALLED` when the plugin is not found on subgraph", async () => { + const subgraphDaoWithoutPlugin = { + ...subgraphDao, + plugins: [], + }; + const applyUpdateActions = client.encoding.applyUpdateAndPermissionsActionBlock( + daoAddress, + applyUpdateParams, + ); + const action = applyUpdateActions[1]; + mockedClient.request.mockResolvedValueOnce({ + dao: subgraphDaoWithoutPlugin, + }); + const result = await validateApplyUpdateFunction( + action, + daoAddress, + client.graphql, + client.ipfs, + ); + expect(result).toMatchObject([ + PluginUpdateProposalInValidityCause.PLUGIN_NOT_INSTALLED, + ]); + }); + it("should return an `NOT_ARAGON_PLUGIN_REPO` when the plugin is not an aragon plugin", async () => { + const externalPluginRepo = { + ...subgraphPluginRepo, + subdomain: "external-plugin-repo", + }; + const applyUpdateActions = client.encoding.applyUpdateAndPermissionsActionBlock( + daoAddress, + applyUpdateParams, + ); + const action = applyUpdateActions[1]; + mockedClient.request.mockResolvedValueOnce({ + dao: subgraphDao, + }); + mockedClient.request.mockResolvedValueOnce({ + pluginRepo: externalPluginRepo, + }); + mockedClient.request.mockResolvedValueOnce({ + pluginPreparation: subgraphPluginPreparation, + }); + mockedIPFSClient.cat.mockResolvedValueOnce(Buffer.from( + JSON.stringify(TOKEN_VOTING_BUILD_METADATA), + )); + const result = await validateApplyUpdateFunction( + action, + daoAddress, + client.graphql, + client.ipfs, + ); + expect(result).toMatchObject([ + PluginUpdateProposalInValidityCause.NOT_ARAGON_PLUGIN_REPO, + ]); + }); + it("should return an `MISSING_PLUGIN_REPO` when the plugin repo is not on subgraph", async () => { + const applyUpdateActions = client.encoding.applyUpdateAndPermissionsActionBlock( + daoAddress, + applyUpdateParams, + ); + const action = applyUpdateActions[1]; + mockedClient.request.mockResolvedValueOnce({ + dao: subgraphDao, + }); + mockedClient.request.mockResolvedValueOnce({ + pluginRepo: null, + }); + const result = await validateApplyUpdateFunction( + action, + daoAddress, + client.graphql, + client.ipfs, + ); + expect(result).toMatchObject([ + PluginUpdateProposalInValidityCause.MISSING_PLUGIN_REPO, + ]); + }); + it("should return an `INVALID_DATA` when the provided init data does not match the abi in the metadata", async () => { + const updatedApplyUpdateParams = { + ...applyUpdateParams, + initData: new Uint8Array([1, 2, 3]), + }; + const applyUpdateActions = client.encoding.applyUpdateAndPermissionsActionBlock( + daoAddress, + updatedApplyUpdateParams, + ); + const action = applyUpdateActions[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 result = await validateApplyUpdateFunction( + action, + daoAddress, + client.graphql, + client.ipfs, + ); + expect(result).toMatchObject([ + PluginUpdateProposalInValidityCause.INVALID_DATA, + ]); + }); + it("should return an `INVALID_PLUGIN_REPO_METADATA` when the provided metadata does not exist or is not correct", async () => { + const applyUpdateActions = client.encoding.applyUpdateAndPermissionsActionBlock( + daoAddress, + applyUpdateParams, + ); + const action = applyUpdateActions[1]; + mockedClient.request.mockResolvedValueOnce({ + dao: subgraphDao, + }); + mockedClient.request.mockResolvedValueOnce({ + pluginRepo: subgraphPluginRepo, + }); + mockedClient.request.mockResolvedValueOnce({ + pluginPreparation: subgraphPluginPreparation, + }); + mockedIPFSClient.cat.mockResolvedValueOnce(Buffer.from( + JSON.stringify({}), + )); + const result = await validateApplyUpdateFunction( + action, + daoAddress, + client.graphql, + client.ipfs, + ); + expect(result).toMatchObject([ + PluginUpdateProposalInValidityCause.INVALID_PLUGIN_REPO_METADATA, + ]); + }); + it("should return an `MISSING_PLUGIN_PREPARATION` when the pluginPreparation is null", async () => { + const applyUpdateActions = client.encoding.applyUpdateAndPermissionsActionBlock( + daoAddress, + applyUpdateParams, + ); + const action = applyUpdateActions[1]; + mockedClient.request.mockResolvedValueOnce({ + dao: subgraphDao, + }); + mockedClient.request.mockResolvedValueOnce({ + pluginRepo: subgraphPluginRepo, + }); + mockedClient.request.mockResolvedValueOnce({ + pluginPreparation: null, + }); + const result = await validateApplyUpdateFunction( + action, + daoAddress, + client.graphql, + client.ipfs, + ); + expect(result).toMatchObject([ + PluginUpdateProposalInValidityCause.MISSING_PLUGIN_PREPARATION, + ]); + }); + }); + 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.applyUpdateAndPermissionsActionBlock( + 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, + }], + }); + mockedIPFSClient.cat.mockResolvedValueOnce(Buffer.from( + JSON.stringify(TOKEN_VOTING_BUILD_METADATA), + )); + const result = await validateUpdatePluginProposalActions( + actions, + daoAddress, + pspAddress, + client.graphql, + client.ipfs, + ); + expect(result.isValid).toEqual(true); + expect(result.actionErrorCauses).toMatchObject([[]]); + expect(result.proposalSettingsErrorCauses).toMatchObject([]); + }); + it("should return an empty array for a valid actions where root is granted", async () => { + const actions = client.encoding.applyUpdateAndPermissionsActionBlock( + 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, + }], + }); + mockedIPFSClient.cat.mockResolvedValueOnce(Buffer.from( + JSON.stringify(TOKEN_VOTING_BUILD_METADATA), + )); + const result = await validateUpdatePluginProposalActions( + actions, + daoAddress, + pspAddress, + client.graphql, + client.ipfs, + ); + expect(result.isValid).toEqual(true); + expect(result.actionErrorCauses).toMatchObject([[]]); + expect(result.proposalSettingsErrorCauses).toMatchObject([]); + }); + it("should return an empty for two groups of apply update", async () => { + const actionsGroupOne = client.encoding.applyUpdateAndPermissionsActionBlock( + daoAddress, + applyUpdateParams, + ); + const actionsGroupTwo = client.encoding.applyUpdateAndPermissionsActionBlock( + 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, + }], + }); + mockedIPFSClient.cat.mockResolvedValueOnce(Buffer.from( + JSON.stringify(TOKEN_VOTING_BUILD_METADATA), + )); + } + + const result = await validateUpdatePluginProposalActions( + [...actionsGroupOne, ...actionsGroupTwo], + daoAddress, + pspAddress, + client.graphql, + client.ipfs, + ); + expect(result.isValid).toEqual(true); + expect(result.actionErrorCauses).toMatchObject([[],[]]); + expect(result.proposalSettingsErrorCauses).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.actionErrorCauses).toMatchObject([]); + expect(result.proposalSettingsErrorCauses).toMatchObject([ProposalSettingsErrorCause.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.actionErrorCauses).toMatchObject([]); + expect(result.proposalSettingsErrorCauses).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.actionErrorCauses).toMatchObject([]); + expect(result.proposalSettingsErrorCauses).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.proposalSettingsErrorCauses).toMatchObject([ + ProposalSettingsErrorCause.INVALID_ACTIONS, + ]); + expect(result.actionErrorCauses).toMatchObject([]); + }); + 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.actionErrorCauses).toMatchObject([ + DaoUpdateProposalInvalidityCause.INVALID_TO_ADDRESS, + ]); + expect(result.proposalSettingsErrorCauses).toMatchObject([]); + }); + it("should return NON_ZERO_CALL_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.actionErrorCauses).toMatchObject([ + DaoUpdateProposalInvalidityCause.NON_ZERO_CALL_VALUE, + ]); + expect(result.proposalSettingsErrorCauses).toMatchObject([]); + + }); + 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.actionErrorCauses).toMatchObject([ + DaoUpdateProposalInvalidityCause + .INVALID_UPGRADE_TO_IMPLEMENTATION_ADDRESS, + ]); + expect(result.proposalSettingsErrorCauses).toMatchObject([]); + }); + 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.actionErrorCauses).toMatchObject([ + DaoUpdateProposalInvalidityCause + .INVALID_UPGRADE_TO_AND_CALL_IMPLEMENTATION_ADDRESS, + ]); + expect(result.proposalSettingsErrorCauses).toMatchObject([]); + }); + 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.actionErrorCauses).toMatchObject([ + DaoUpdateProposalInvalidityCause + .INVALID_UPGRADE_TO_AND_CALL_VERSION, + ]); + expect(result.proposalSettingsErrorCauses).toMatchObject([]); + }); + 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.actionErrorCauses).toMatchObject([ + DaoUpdateProposalInvalidityCause + .INVALID_UPGRADE_TO_AND_CALL_DATA, + ]); + expect(result.proposalSettingsErrorCauses).toMatchObject([]); + }); + }); + describe("containsDaoUpdateAction", () => { + 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_UPGRADE_PERMISSION, + ProposalActionTypes.APPLY_UPDATE, + ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION, + ], + expected: true, + }, + { + input: [ + ProposalActionTypes.UPGRADE_TO_AND_CALL, + ProposalActionTypes.GRANT_PLUGIN_UPGRADE_PERMISSION, + ProposalActionTypes.APPLY_UPDATE, + ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION, + ], + expected: true, + }, + { + input: [ + ProposalActionTypes.GRANT_PLUGIN_UPGRADE_PERMISSION, + ProposalActionTypes.APPLY_UPDATE, + ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION, + ], + expected: false, + }, + { + input: [ + ProposalActionTypes.GRANT_PLUGIN_UPGRADE_PERMISSION, + ProposalActionTypes.APPLY_UPDATE, + ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION, + ProposalActionTypes.UPGRADE_TO, + ], + expected: false, + }, + { + input: [ + ProposalActionTypes.GRANT_PLUGIN_UPGRADE_PERMISSION, + ProposalActionTypes.APPLY_UPDATE, + ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION, + ProposalActionTypes.UPGRADE_TO_AND_CALL, + ], + expected: false, + }, + { + input: [ + ProposalActionTypes.UPGRADE_TO, + ProposalActionTypes.GRANT_PLUGIN_UPGRADE_PERMISSION, + ProposalActionTypes.APPLY_UPDATE, + ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION, + // + ProposalActionTypes.GRANT_PLUGIN_UPGRADE_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 = containsDaoUpdateAction(input); + expect(result).toEqual(expected); + } + }); + }); + describe("containsPluginUpdateActionBlock", () => { + 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.GRANT_PLUGIN_UPGRADE_PERMISSION, + ProposalActionTypes.APPLY_UPDATE, + ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION, + ], + expected: true, + }, + { + input: [ + ProposalActionTypes.GRANT_ROOT_PERMISSION, + ProposalActionTypes.APPLY_UPDATE, + ProposalActionTypes.REVOKE_ROOT_PERMISSION, + ], + expected: false, + }, + { + input: [ + ProposalActionTypes.GRANT_PLUGIN_UPGRADE_PERMISSION, + ProposalActionTypes.APPLY_UPDATE, + ], + expected: false, + }, + { + input: [ + ProposalActionTypes.GRANT_PLUGIN_UPGRADE_PERMISSION, + ProposalActionTypes.APPLY_UPDATE, + ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION, + ProposalActionTypes.GRANT_PLUGIN_UPGRADE_PERMISSION, + ProposalActionTypes.APPLY_UPDATE, + ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION, + ], + expected: true, + }, + ]; + for (const { input, expected } of cases) { + const result = containsPluginUpdateActionBlock(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.GRANT_PLUGIN_UPGRADE_PERMISSION, + ProposalActionTypes.GRANT_ROOT_PERMISSION, + ProposalActionTypes.APPLY_UPDATE, + ProposalActionTypes.REVOKE_ROOT_PERMISSION, + ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION, + ], + expected: true, + }, + { + input: [ + ProposalActionTypes.GRANT_PLUGIN_UPGRADE_PERMISSION, + ProposalActionTypes.GRANT_ROOT_PERMISSION, + ProposalActionTypes.APPLY_UPDATE, + ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION, + ], + expected: false, + }, + { + input: [ + ProposalActionTypes.GRANT_PLUGIN_UPGRADE_PERMISSION, + ProposalActionTypes.GRANT_ROOT_PERMISSION, + ProposalActionTypes.APPLY_UPDATE, + ProposalActionTypes.REVOKE_ROOT_PERMISSION, + ProposalActionTypes.REVOKE_PLUGIN_UPGRADE_PERMISSION, + ProposalActionTypes.GRANT_PLUGIN_UPGRADE_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 = containsPluginUpdateActionBlockWithRootPermission(input); + expect(result).toEqual(expected); + } + }); + }); +});