Skip to content

Commit

Permalink
use ethers abi parser to validate function abi
Browse files Browse the repository at this point in the history
  • Loading branch information
piotr-roslaniec committed Jun 28, 2023
1 parent 50dd01c commit a0bd371
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 40 deletions.
57 changes: 40 additions & 17 deletions src/conditions/base/contract.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ethers } from 'ethers';
import Joi from 'joi';

import { ETH_ADDRESS_REGEXP } from '../const';
Expand All @@ -6,31 +7,53 @@ 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(),
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',
});
}

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(),
}).custom((functionAbi, helper) => {
// 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"',
});
Expand All @@ -39,7 +62,7 @@ const functionAbiSchema = Joi.object({
return functionAbi;
});

export const contractConditionRecord: Record<string, Joi.Schema> = {
export const contractConditionRecord = {
...rpcConditionRecord,
contractAddress: Joi.string().pattern(ETH_ADDRESS_REGEXP).required(),
standardContractType: Joi.string()
Expand Down
24 changes: 2 additions & 22 deletions test/unit/conditions/base/contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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: {
Expand Down
2 changes: 1 addition & 1 deletion test/unit/testVariables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export const testContractConditionObj = {
export const testFunctionAbi = {
name: 'myFunction',
type: 'function',
stateMutability: 'view',
inputs: [
{
internalType: 'address',
Expand All @@ -67,5 +68,4 @@ export const testFunctionAbi = {
type: 'uint256',
},
],
stateMutability: 'view',
};

0 comments on commit a0bd371

Please sign in to comment.