From 32e6a94ed6a6a216041af93bb7c43c4c3bbcc9e0 Mon Sep 17 00:00:00 2001 From: Piotr Roslaniec Date: Fri, 16 Jun 2023 17:00:49 +0200 Subject: [PATCH] use ethers abi parser to validate function abi --- src/conditions/base/contract.ts | 49 +++++++++++++++------- test/unit/conditions/base/contract.test.ts | 24 +---------- test/unit/testVariables.ts | 2 +- 3 files changed, 38 insertions(+), 37 deletions(-) diff --git a/src/conditions/base/contract.ts b/src/conditions/base/contract.ts index 83a13b6ae..c7f153f31 100644 --- a/src/conditions/base/contract.ts +++ b/src/conditions/base/contract.ts @@ -1,3 +1,4 @@ +import { ethers } from 'ethers'; import Joi from 'joi'; import { ETH_ADDRESS_REGEXP } from '../const'; @@ -6,31 +7,51 @@ import { RpcCondition, rpcConditionRecord } from './rpc'; export const STANDARD_CONTRACT_TYPES = ['ERC20', 'ERC721']; -const functionAbiVariable = Joi.object({ - internalType: Joi.string(), // TODO is this needed? - name: Joi.string().required(), - type: Joi.string().required(), -}); - const functionAbiSchema = Joi.object({ name: Joi.string().required(), type: Joi.string().valid('function').required(), - inputs: Joi.array().items(functionAbiVariable), - outputs: Joi.array().items(functionAbiVariable), - // TODO: Should we restrict this to 'view'? - stateMutability: Joi.string(), + inputs: Joi.array(), + outputs: Joi.array(), + stateMutability: Joi.string().valid('view', 'pure').required(), }).custom((functionAbi, helper) => { + // Is `functionABI` a valid function fragment? + let asInterface; + try { + asInterface = new ethers.utils.Interface([functionAbi]); + } catch (e: unknown) { + const { message } = e as Error; + return helper.message({ + custom: message, + }); + } + + if (!asInterface.fragments) { + return helper.message({ + custom: '"functionAbi" is missing a function fragment', + }); + } + + if (asInterface.fragments.length > 1) { + return helper.message({ + custom: '"functionAbi" must contain exactly one function fragment', + }); + } + + // Now we just need to validate against the parent schema // Validate method name const method = helper.state.ancestors[0].method; - if (functionAbi.name !== method) { + const functionFragment = asInterface.fragments.filter( + (f) => f.name === method + )[0]; + if (!functionFragment) { return helper.message({ - custom: '"method" must be the same as "functionAbi.name"', + custom: '"functionAbi" does not contain the method specified as "method"', }); } // Validate nr of parameters const parameters = helper.state.ancestors[0].parameters; - if (functionAbi.inputs?.length !== parameters.length) { + if (functionFragment.inputs.length !== parameters.length) { return helper.message({ custom: '"parameters" must have the same length as "functionAbi.inputs"', }); @@ -39,7 +60,7 @@ const functionAbiSchema = Joi.object({ return functionAbi; }); -export const contractConditionRecord: Record = { +export const contractConditionRecord = { ...rpcConditionRecord, contractAddress: Joi.string().pattern(ETH_ADDRESS_REGEXP).required(), standardContractType: Joi.string() diff --git a/test/unit/conditions/base/contract.test.ts b/test/unit/conditions/base/contract.test.ts index 25fdddd8b..05074ddd1 100644 --- a/test/unit/conditions/base/contract.test.ts +++ b/test/unit/conditions/base/contract.test.ts @@ -7,7 +7,7 @@ import { import { ContractCondition } from '../../../../src/conditions/base'; import { USER_ADDRESS_PARAM } from '../../../../src/conditions/const'; import { fakeWeb3Provider } from '../../../utils'; -import { testContractConditionObj } from '../../testVariables'; +import { testContractConditionObj, testFunctionAbi } from '../../testVariables'; describe('validation', () => { it('accepts on a valid schema', () => { @@ -103,30 +103,10 @@ describe('accepts either standardContractType or functionAbi but not both or non }); describe('supports custom function abi', () => { - const fakeFunctionAbi = { - name: 'myFunction', - type: 'function', - inputs: [ - { - name: 'account', - type: 'address', - }, - { - name: 'myCustomParam', - type: 'uint256', - }, - ], - outputs: [ - { - name: 'someValue', - type: 'uint256', - }, - ], - }; const contractConditionObj = { ...testContractConditionObj, standardContractType: undefined, - functionAbi: fakeFunctionAbi, + functionAbi: testFunctionAbi, method: 'myFunction', parameters: [USER_ADDRESS_PARAM, ':customParam'], returnValueTest: { diff --git a/test/unit/testVariables.ts b/test/unit/testVariables.ts index ef943513a..36031e816 100644 --- a/test/unit/testVariables.ts +++ b/test/unit/testVariables.ts @@ -48,6 +48,7 @@ export const testContractConditionObj = { export const testFunctionAbi = { name: 'myFunction', type: 'function', + stateMutability: 'view', inputs: [ { internalType: 'address', @@ -67,5 +68,4 @@ export const testFunctionAbi = { type: 'uint256', }, ], - stateMutability: 'view', };