UNPKG

0xweb

Version:

Contract package manager and other web3 tools

682 lines (610 loc) 26.9 kB
import { $logger, l } from '@dequanto/utils/$logger'; import { type TAbiItem } from '@dequanto/types/TAbi'; import { AssemblyBlock, AssemblyCall, BaseASTNode, BinaryOperation, Block, BooleanLiteral, ContractDefinition, EmitStatement, EventDefinition, Expression, FunctionCall, FunctionDefinition, Identifier, IndexAccess, MemberAccess, ModifierDefinition, NumberLiteral, SourceUnit, StringLiteral, UnaryOperation, VariableDeclaration } from '@solidity-parser/parser/dist/src/ast-types'; import alot from 'alot'; import { Ast } from './Ast'; import { ISlotsParserOption, ISlotVarDefinition } from './models'; import { SourceFile, TSourceFileContract } from './SourceFile'; import { $abiUtils } from '@dequanto/utils/$abiUtils'; import { $is } from '@dequanto/utils/$is'; type TMappingAccessor = { node: Identifier | MemberAccess key: string location: TVariableLocation } type TVariableLocation = { scope: 'local' | 'state' | 'global' } | { scope: 'argument', index: number } export type TMappingSetterEvent = { event: TAbiItem accessors: string[] accessorsIdxMapping: number[] } export type TMappingSetterMethod = { method: TAbiItem accessors: string[] } type TEventEmitStatement = { abi?: TAbiItem name: string; args: { node: Identifier | IndexAccess | MemberAccess | FunctionCall | NumberLiteral | StringLiteral | BooleanLiteral | Expression; key: any; location: TVariableLocation; }[] } type TEventForMappingMutation = { error: Error } | { event?: TEventEmitStatement, accessors: string[] accessorsIdxMapping?: number[] } | { method: TAbiItem accessors: string[] } export namespace MappingSettersResolver { export async function getEventsForMappingMutations(mappingVarName: string, source: { path: string, code?: string }, contractName?: string, opts?: ISlotsParserOption): Promise<{ errors: Error[] events: TMappingSetterEvent[] /** Methods with mutation but without Log Events */ methods: TMappingSetterMethod[] }> { const sourceFile = new SourceFile(source.path, source.code, opts?.files); const chain = await sourceFile.getContractInheritanceChain(contractName); const arr = await alot(chain) .mapAsync(async (item, i) => { return await extractSettersSingle( mappingVarName, item, chain.slice(0, i), ); }) .toArrayAsync({ threads: 1 }); let errors = alot(arr).mapMany(x => x.errors ?? []).toArray(); let events = alot(arr).mapMany(x => x.events ?? []).distinctBy(x => x.event.name + x.accessorsIdxMapping.join('')).toArray(); let methods = alot(arr).mapMany(x => x.methods ?? []).distinctBy(x => x.method.name).toArray(); return { errors, events, methods, }; } async function extractSettersSingle( mappingVarName: string , sourceContract: TSourceFileContract , inheritance: TSourceFileContract[] ): Promise<{ errors: Error[], events: TMappingSetterEvent[] methods?: TMappingSetterMethod[], }> { let allEvents = [] as EventDefinition[]; let allMethods = [] as FunctionDefinition[]; let allModifiers = [] as ModifierDefinition[]; if (Ast.isContractDefinition(sourceContract.contract)) { allEvents = Ast.getEventDefinitions(sourceContract.contract, inheritance?.map(x => x.contract as ContractDefinition)); allMethods = Ast.getFunctionDeclarations(sourceContract.contract, inheritance?.map(x => x.contract as ContractDefinition)); allModifiers = Ast.getModifierDefinitions(sourceContract.contract, inheritance?.map(x => x.contract as ContractDefinition)); } let arr = await alot(allMethods) .mapManyAsync(async method => { let mutations = await $astSetters.extractMappingMutations( mappingVarName , method , allMethods , allModifiers , allEvents , sourceContract , inheritance ); if (mutations == null || mutations.length == 0) { // No mutation return []; } return await alot(mutations) .mapAsync(async mutation => { if ('error' in mutation) { // Some error in mutation extractor $logger.error(`${mutation.error}`); return { error: mutation.error }; } if ('method' in mutation) { // We got only method, but no Event that CONTAINS the Mapping Key within the method return mutation; } let event = mutation.event; if (event == null) { return { error: new Error(`No event found for ${mappingVarName} mutation in method ${method.name}`) }; } let eventDeclaration = allEvents.find(ev => ev.name === event.name && ev.parameters.length === event.args.length); if (eventDeclaration == null && mutation.event.abi && $is.Hex(event.name) === false) { $logger.error(`Event ${event.name} not found in events`) } return <TMappingSetterEvent>{ event: mutation.event.abi ?? await Ast.getAbi(eventDeclaration, sourceContract, inheritance), accessors: mutation.accessors, accessorsIdxMapping: mutation.accessorsIdxMapping, }; }) .toArrayAsync(); }) .filterAsync(x => x != null) .toArrayAsync(); let errors = arr.map(x => 'error' in x ? x.error : null).filter(Boolean); let events = arr.map(x => 'event' in x ? x : null).filter(Boolean); let methods = arr.map(x => 'method' in x ? x : null).filter(Boolean) return { errors, events: alot(events).distinctBy(x => x.event.name + x.accessorsIdxMapping.join('')).toArray(), methods: methods } } } namespace $astSetters { export async function extractMappingMutations( mappingVarName: string , method: FunctionDefinition , allMethods: FunctionDefinition[] , allModifiers: ModifierDefinition[] , allEvents: EventDefinition[] , source: TSourceFileContract , inheritance: TSourceFileContract[] ): Promise<TEventForMappingMutation[]> { let body = method.body; // Find a variable setter in the method's body. let matches = Ast.findMany<BinaryOperation | UnaryOperation | IndexAccess>(body, node => { if (Ast.isBinaryOperation(node) && /^.?=$/.test(node.operator)) { let indexRootAccess: IndexAccess; if (Ast.isIndexAccess(node.left)) { indexRootAccess = node.left; } if (indexRootAccess == null && Ast.isMemberAccess(node.left)) { indexRootAccess = Ast.find<IndexAccess>(node.left, Ast.isIndexAccess)?.node; } if (indexRootAccess == null) { return false; } let fields = $node.getIndexAccessFields(indexRootAccess); if (fields.length === 0) { return false; } let [field] = fields; if (Ast.isIdentifier(field) && field.name === mappingVarName) { return true; } } if (Ast.isUnaryOperation(node) && Ast.isIndexAccess(node.subExpression)) { let fields = $node.getIndexAccessFields(node.subExpression); if (fields.length === 0) { return false; } let [field] = fields; if (Ast.isIdentifier(field) && field.name === mappingVarName) { return true; } } return false; }); if (matches.length === 0) { // Mapping mutation not found. Skip this method return []; } let results = await alot(matches).mapAsync(async match => { // Mapping mutation found // Get the accessors breadcrumbs let indexAccess = Ast.find<IndexAccess>([ (match.node as BinaryOperation).left, (match.node as UnaryOperation).subExpression, (match.node as IndexAccess) ], Ast.isIndexAccess)?.node; let keys = $node.getIndexAccessFields(indexAccess); let setterIdentifiersRaw: (Identifier | MemberAccess)[] = keys .slice(1) .filter(node => Ast.isIdentifier(node) || Ast.isMemberAccess(node) || Ast.isIndexAccess(node) || Ast.isFunctionCall(node)) as any[]; if (setterIdentifiersRaw.length === 0) { l`@TODO - just the dynamic fields are supported (by variable) in ${method.name}` return { error: new Error(`In method ${method.name} not supported setters found - only setters by identifier are allowed`) }; } let setterIdentifiers: TMappingAccessor[] = setterIdentifiersRaw.map(node => { return { node: node, key: Ast.serialize(node), location: $node.getVariableLocation(node, method) }; }); let eventInfo = await $node.findArgumentLogInFunction(method, null, setterIdentifiers.map(x => x.key), allEvents, source); if (eventInfo) { return eventInfo; } // Event in method not found // Check modifiers let modifiers = method .modifiers ?.map(modifier => { return allModifiers?.find(x => x.name === modifier.name); }) ?.filter(Boolean); if (modifiers?.length > 0) { let inModifiers = await alot(modifiers) .mapAsync(async mod => { return await $node.findArgumentLogInFunction(mod, method, setterIdentifiers.map(x => x.key), allEvents, source); }) .filterAsync(x => x != null) .toArrayAsync(); if (inModifiers.length > 0) { let [eventInfo] = inModifiers; return { event: eventInfo.event, accessors: eventInfo.accessors, accessorsIdxMapping: eventInfo.accessorsIdxMapping }; } } // Check method calls, which pass the setterIdentifiers into let methodCallInfos = $node.findMethodCallsInFunctionWithParameters(method, setterIdentifiers, allMethods); if (methodCallInfos.length > 0) { let eventInfos = await alot(methodCallInfos) .mapAsync(async methodCallInfo => { let eventInfo = await $node.findArgumentLogInFunction(methodCallInfo.method, null, methodCallInfo.argumentKeyMapping, allEvents, source); if (eventInfo == null) { return null; } return { eventInfo, methodCallInfo, } }) .filterAsync(x => x != null) .toArrayAsync(); if (eventInfos.length > 0) { let { eventInfo, methodCallInfo } = eventInfos[0]; return { event: eventInfo.event, accessors: setterIdentifiers.map(x => x.key), accessorsIdxMapping: eventInfo.accessorsIdxMapping } } } if (method.visibility === 'internal' || method.visibility === 'private') { // Check if some outer caller method emits events let methodArgs = method.parameters.map(x => x.identifier.name); // let methodArgsMapping = setterIdentifiers.map(accessor => { // return accessor.location // }) let argumentsMapping = setterIdentifiers.map(x => x.location.scope === 'argument' ? x.location.index : null); let fromArguments = argumentsMapping.every(x => x != null); if (fromArguments) { let methodCallInfos = $node.findMethodReferences(method, allMethods); let eventInfos = await alot(methodCallInfos) .mapAsync(async methodCallInfo => { let argumentKeyMapping = argumentsMapping.map(idx => { return Ast.serialize(methodCallInfo.ref.arguments[idx]); }); let eventInfo = await $node.findArgumentLogInFunction(methodCallInfo.method, null, argumentKeyMapping, allEvents, source); if (eventInfo == null) { return null; } return { eventInfo, methodCallInfo, } }) .filterAsync(x => x != null) .toArrayAsync(); if (eventInfos.length > 0) { let { eventInfo, methodCallInfo } = eventInfos[0]; return { event: eventInfo.event, accessors: setterIdentifiers.map(x => x.key), accessorsIdxMapping: eventInfo.accessorsIdxMapping } } } } // local variables are not logged to get the Mapping Key from. return { method: await Ast.getAbi(method, source, inheritance), accessors: setterIdentifiers.map(x => x.key) }; }).toArrayAsync(); return results; } } namespace $node { export function getIndexAccessFields(node: IndexAccess) { let arr = [] as (Identifier | NumberLiteral | StringLiteral)[]; if (Ast.isIndexAccess(node.base)) { arr.push(...getIndexAccessFields(node.base)) } else { arr.push(node.base as any) } arr.push(node.index as any) return arr; } export function getVariableLocation(variable: string | Identifier | MemberAccess | IndexAccess | FunctionCall, method: FunctionDefinition | ModifierDefinition): TVariableLocation { let varName: string; if (typeof variable === 'string') { varName = variable; } else if (Ast.isIdentifier(variable)) { varName = variable.name; } else if (Ast.isMemberAccess(variable)) { let identifier = Ast.find<Identifier>(variable, Ast.isIdentifier); if (identifier == null) { throw new Error(`Identifier not found in ${JSON.stringify(variable)}`); } variable = identifier.node.name; } else if (Ast.isIndexAccess(variable)) { let identifier = Ast.find<Identifier>(variable, Ast.isIdentifier); if (identifier == null) { throw new Error(`Identifier not found in ${JSON.stringify(variable)}`); } variable = identifier.node.name; } else if (Ast.isFunctionCall(variable)) { let identifier = Ast.find<Identifier>(variable.expression, Ast.isIdentifier); if (identifier == null) { throw new Error(`Identifier not found in ${JSON.stringify(variable)}`); } variable = identifier.node.name; } let localVars = Ast.findMany<VariableDeclaration>(method.body, node => { return Ast.isVariableDeclaration(node); }); if (localVars.some(x => x.node.identifier.name === varName)) { return { scope: 'local' }; } //console.log('params', method.parameters); let methodArg = method.parameters?.find(param => { return param.identifier?.name === varName; }); if (methodArg != null) { return { scope: 'argument', index: method.parameters.indexOf(methodArg) }; } if (varName === 'msg' || varName === 'tx') { return { scope: 'global' } } return { scope: 'state' } } export async function findEventsInFunction( method: FunctionDefinition | ModifierDefinition , parent: FunctionDefinition /** <0.5.0 was no emit statement, search for a method which equals to event declaration */ , allEvents: EventDefinition[] , source: TSourceFileContract ): Promise<TEventEmitStatement[]> { let body = method.body; let events = Ast.findMany<EmitStatement>(body, node => { return Ast.isEmitStatement(node) || (Ast.isFunctionCall(node) && allEvents.some(x => x.name === Ast.getFunctionName(node))); }).map(match => { // transform functionCall to eventCall in <0.5.0 if (Ast.isFunctionCall(match.node)) { match.node = <EmitStatement>{ type: 'EmitStatement', eventCall: match.node }; } return match; }); let eventInfos = events .map(event => { if (Ast.isIdentifier(event.node.eventCall.expression) === false) { $logger.error(`Extract events: expected the Identifier for the Event Name: ${JSON.stringify(event.node.eventCall, null, 2)}`); return null; } let expression = event.node.eventCall.expression as Identifier; let name = expression.name; let args = event .node .eventCall .arguments .map(node => { if (Ast.isIdentifier(node) || Ast.isMemberAccess(node) || Ast.isIndexAccess(node) || Ast.isFunctionCall(node)) { let location = getVariableLocation(node, method); return { node: node, key: Ast.serialize(node), location }; } if (Ast.isNumberLiteral(node) || Ast.isStringLiteral(node) || Ast.isBooleanLiteral(node)) { return { node: node, key: Ast.serialize(node), location: null }; } return { node: node, key: Ast.serialize(node), location: null } }) .filter(Boolean); return { name: name, args: args }; }) .filter(Boolean); if (eventInfos.length > 0) { return eventInfos; } let assemblyLogCall = Ast.find<AssemblyCall>(body, node => { return Ast.isAssemblyCall(node) && node.functionName?.startsWith('log'); }); if (assemblyLogCall) { let topics = await alot(assemblyLogCall.node.arguments.slice(2)).mapAsync(async arg => { let topic = Ast.serialize(arg); let $method = Ast.isModifierDefinition(method) ? parent : method; if (topic === 'shl(224, shr(224, calldataload(0)))') { let abi = await Ast.getAbi($method, source); let signature = $abiUtils.getTopicSignature(abi); return signature; } if (topic === 'caller') { return 'msg.sender' } let calldataMatch = /calldataload\((?<offset>\d+)\)/.exec(topic); if (calldataMatch) { /** @TODO: here we support simple calldata mapping to arguments. Complex argument types are not supported */ let offset = Number(calldataMatch.groups.offset) - 4; let slot = offset / 32; let param = $method.parameters[slot]; if (param) { return param.identifier.name; } } return topic; }).toArrayAsync(); let abi = <TAbiItem>{ name: topics[0], inputs: topics.slice(1).map(topic => { return { name: topic, } }) }; let event = { abi: abi, name: abi.name, args: abi.inputs.map(input => { return { key: input.name, location: null, node: null, } }) }; return [event]; } return []; } export function findMethodCallsInFunction(method: FunctionDefinition) { return Ast.findMany<FunctionCall>(method.body, node => { if (Ast.isFunctionCall(node)) { let expression = node.expression; if (Ast.isIdentifier(expression)) { let varName = expression.name; let varLocation = getVariableLocation(varName, method); if (varLocation.scope === 'state') { return true; } } } return false; }); } export function findMethodReferenceInFunction(method: FunctionDefinition, ref: FunctionDefinition): FunctionCall { let refs = findMethodCallsInFunction(method); let call = refs.find(x => { return Ast.isIdentifier(x.node.expression) && x.node.expression.name === ref.name }); return call?.node; } export function findMethodCallsInFunctionWithParameters(method: FunctionDefinition, accessors: TMappingAccessor[], allMethods: FunctionDefinition[]) { let methodCallInfos = $node .findMethodCallsInFunction(method) .map(methodCall => { let argumentIdxMapping = accessors.map(accessor => { let i = methodCall.node.arguments.findIndex(arg => { return (Ast.isIdentifier(arg) || Ast.isMemberAccess(arg)) && Ast.serialize(arg) === accessor.key }); return i; }); let hasNotFound = argumentIdxMapping.some(x => x === -1); if (hasNotFound) { return null; } let methodName = (methodCall.node.expression as Identifier).name; let method = allMethods.find(x => x.name === methodName); if (method == null) { $logger.error(`Method not found ${methodName}`); return null; } let methodArguments = method.parameters.map(param => param.identifier.name); let argumentKeyMapping = argumentIdxMapping.map(idx => { return methodArguments[idx]; }) return { method, methodCall, argumentIdxMapping: argumentIdxMapping, argumentKeyMapping: argumentKeyMapping, }; }) .filter(Boolean); return methodCallInfos; } export function findMethodReferences(refMethod: FunctionDefinition, allMethods: FunctionDefinition[]) { return allMethods .map(method => { if (method === refMethod) { return null; } let call = findMethodReferenceInFunction(method, refMethod); if (call == null) { return null; } return { method, ref: call }; }) .filter(Boolean); } export async function findArgumentLogInFunction( method: FunctionDefinition | ModifierDefinition , parent: FunctionDefinition , accessors: string[] , allEvents: EventDefinition[] , source: TSourceFileContract ): Promise<{ event: TEventEmitStatement, accessors: string[], accessorsIdxMapping: number[] }> { let eventsInFunction = await $node.findEventsInFunction(method, parent, allEvents, source); let events = eventsInFunction.filter(event => { return accessors.every(key => event.args.some(arg => arg.key === key)); }); if (events.length > 0) { // most of the time it will be only one event, so just take the first one. let event = events[0]; let mappings = accessors.map(key => { let index = event.args.findIndex(arg => arg.key === key); return index; }); return { // Transfer(from,to); event: event, // outer variable order: e.g. to,from accessors: accessors, // outer variable order to event argument mapping, e.g. 1,0 accessorsIdxMapping: mappings }; } return null; } }