From a1f907c98b8413b7353f845a45a8e90ae0f22463 Mon Sep 17 00:00:00 2001 From: Burnt Val Date: Tue, 26 Nov 2024 12:46:45 -0500 Subject: [PATCH] contract exec auth comparison --- .../abstraxion-core/src/AbstraxionAuth.ts | 209 +++++++++++++++++- packages/abstraxion-core/src/types/index.ts | 24 ++ 2 files changed, 225 insertions(+), 8 deletions(-) diff --git a/packages/abstraxion-core/src/AbstraxionAuth.ts b/packages/abstraxion-core/src/AbstraxionAuth.ts index 02ec03a..a247caf 100644 --- a/packages/abstraxion-core/src/AbstraxionAuth.ts +++ b/packages/abstraxion-core/src/AbstraxionAuth.ts @@ -2,6 +2,14 @@ import { GasPrice } from "@cosmjs/stargate"; import { GenericAuthorization } from "cosmjs-types/cosmos/authz/v1beta1/authz"; import { StakeAuthorization } from "cosmjs-types/cosmos/staking/v1beta1/authz"; import { SendAuthorization } from "cosmjs-types/cosmos/bank/v1beta1/authz"; +import { + CombinedLimit, + ContractExecutionAuthorization, + MaxCallsLimit, + MaxFundsLimit, + AcceptedMessageKeysFilter, + AcceptedMessagesFilter, +} from "cosmjs-types/cosmwasm/wasm/v1/authz"; import { CosmWasmClient } from "@cosmjs/cosmwasm-stargate"; import { fetchConfig } from "@burnt-labs/constants"; import type { @@ -10,6 +18,7 @@ import type { GrantsResponse, SpendLimit, TreasuryGrantConfig, + DecodeAuthorizationResponse, } from "@/types"; import { GranteeSignerClient } from "./GranteeSignerClient"; import { SignArbSecp256k1HdWallet } from "./SignArbSecp256k1HdWallet"; @@ -437,14 +446,7 @@ export class AbstraxionAuth { decodeAuthorization( typeUrl: string, value: string, - ): { - msg?: string; - spendLimit?: string; - allowList?: string[]; - authorizationType?: string; - maxTokens?: string; - denyList?: string[]; - } | null { + ): DecodeAuthorizationResponse | null { const decodedValue = new Uint8Array(Buffer.from(value, "base64")); if (typeUrl === "/cosmos.authz.v1beta1.GenericAuthorization") { @@ -474,9 +476,191 @@ export class AbstraxionAuth { }; } + if (typeUrl === "/cosmwasm.wasm.v1.ContractExecutionAuthorization") { + const authorization = ContractExecutionAuthorization.decode(decodedValue); + + const contracts = authorization.grants.map((grant) => { + let limitType: string | undefined; + let maxCalls: string | undefined; + let maxFunds: { denom: string; amount: string }[] | undefined; + let combinedLimits: + | { + maxCalls: string; + maxFunds: { denom: string; amount: string }[]; + } + | undefined; + let filter = grant.filter + ? { + typeUrl: grant.filter.typeUrl, + keys: + grant.filter.typeUrl === + "/cosmwasm.wasm.v1.AcceptedMessageKeysFilter" + ? AcceptedMessageKeysFilter.decode(grant.filter.value).keys + : undefined, + messages: + grant.filter.typeUrl === + "/cosmwasm.wasm.v1.AcceptedMessagesFilter" + ? AcceptedMessagesFilter.decode(grant.filter.value).messages + : undefined, + } + : undefined; + + // Decode limit based on type_url + switch (grant.limit?.typeUrl) { + case "/cosmwasm.wasm.v1.MaxCallsLimit": { + const limit = MaxCallsLimit.decode(grant.limit.value); + limitType = "MaxCalls"; + maxCalls = String(limit.remaining); + break; + } + case "/cosmwasm.wasm.v1.MaxFundsLimit": { + const limit = MaxFundsLimit.decode( + new Uint8Array(grant.limit.value), + ); + limitType = "MaxFunds"; + maxFunds = limit.amounts.map((coin) => ({ + denom: coin.denom, + amount: coin.amount, + })); + break; + } + case "/cosmwasm.wasm.v1.CombinedLimit": { + const limit = CombinedLimit.decode( + new Uint8Array(grant.limit.value), + ); + limitType = "CombinedLimit"; + combinedLimits = { + maxCalls: String(limit.callsRemaining), + maxFunds: limit.amounts.map((coin) => ({ + denom: coin.denom, + amount: coin.amount, + })), + }; + break; + } + default: + limitType = "Unknown"; + break; + } + + return { + contract: grant.contract, + limitType, + maxCalls, + maxFunds, + combinedLimits, + filter, + }; + }); + + return { contracts }; + } + return null; } + private validateContractExecution( + decodedAuth: DecodeAuthorizationResponse | null, + chainAuth: any, + ): boolean { + const chainGrants = chainAuth.grants || []; + const decodedGrants = decodedAuth?.contracts || []; + + return decodedGrants.every((decodedGrant) => { + const matchingChainGrant = chainGrants.find((chainGrant: any) => { + // Basic contract match + if (chainGrant.contract !== decodedGrant.contract) { + return false; + } + + // Filter validation + if (decodedGrant.filter) { + const chainFilter = chainGrant.filter; + if (!chainFilter) { + return false; + } + + // Check type URL + if (chainFilter["@type"] !== decodedGrant.filter.typeUrl) { + return false; + } + + // Check keys array + const decodedKeys = decodedGrant.filter.keys || []; + const chainKeys = chainFilter.keys || []; + if (decodedKeys.length !== chainKeys.length) { + return false; + } + if (!decodedKeys.every((key, index) => key === chainKeys[index])) { + return false; + } + + // Check messages array + const decodedMessages = decodedGrant.filter.messages || []; + const chainMessages = chainFilter.messages || []; + if (decodedMessages.length !== chainMessages.length) { + return false; + } + + // Compare messages byte by byte + const messagesMatch = decodedMessages.every((msg, index) => { + const chainMsg = chainMessages[index]; + if (msg.length !== chainMsg.length) { + return false; + } + for (let i = 0; i < msg.length; i++) { + if (msg[i] !== chainMsg[i]) { + return false; + } + } + return true; + }); + if (!messagesMatch) { + return false; + } + } else if (chainGrant.filter) { + return false; + } + + return true; + }); + + if (!matchingChainGrant) { + return false; + } + + switch (decodedGrant.limitType) { + case "MaxCalls": + return ( + matchingChainGrant.limit?.["@type"] === + "/cosmwasm.wasm.v1.MaxCallsLimit" && + decodedGrant.maxCalls === matchingChainGrant.limit.remaining + ); + + case "MaxFunds": + return ( + matchingChainGrant.limit?.["@type"] === + "/cosmwasm.wasm.v1.MaxFundsLimit" && + JSON.stringify(decodedGrant.maxFunds) === + JSON.stringify(matchingChainGrant.limit.amounts) + ); + + case "CombinedLimit": + return ( + matchingChainGrant.limit?.["@type"] === + "/cosmwasm.wasm.v1.CombinedLimit" && + decodedGrant.combinedLimits?.maxCalls === + matchingChainGrant.limit.calls_remaining && + JSON.stringify(decodedGrant.combinedLimits?.maxFunds) === + JSON.stringify(matchingChainGrant.limit.amounts) + ); + + default: + return false; + } + }); + } + /** * Compares treasury grant configurations with the grants on-chain to ensure they match. * @@ -552,6 +736,15 @@ export class AbstraxionAuth { ); } + if ( + chainAuthType === "/cosmwasm.wasm.v1.ContractExecutionAuthorization" + ) { + return this.validateContractExecution( + decodedAuthorization, + chainAuthorization, + ); + } + return false; }); }); diff --git a/packages/abstraxion-core/src/types/index.ts b/packages/abstraxion-core/src/types/index.ts index 5b23db2..4fb7558 100644 --- a/packages/abstraxion-core/src/types/index.ts +++ b/packages/abstraxion-core/src/types/index.ts @@ -54,3 +54,27 @@ export type ContractGrantDescription = address: string; amounts: SpendLimit[]; }; + +export interface DecodeAuthorizationResponse { + msg?: string; + spendLimit?: string; + allowList?: string[]; + authorizationType?: string; + maxTokens?: string; + denyList?: string[]; + contracts?: { + contract: string; + limitType?: string; + maxCalls?: string; + maxFunds?: { denom: string; amount: string }[]; + combinedLimits?: { + maxCalls: string; + maxFunds: { denom: string; amount: string }[]; + }; + filter?: { + typeUrl: string; + keys?: string[]; + messages?: Uint8Array[]; + }; + }[]; +}