Skip to content

Commit

Permalink
Make governance tooling work with arbitrary transactions (#9735)
Browse files Browse the repository at this point in the history
* Make governance tooling work with arbitrary transactions
* Fix contractkit cache issue with Proxy contracts

Co-authored-by: Gaston Ponti <pontigaston@gmail.com>
  • Loading branch information
bowd and gastonponti authored Aug 8, 2022
1 parent 4dbc0b3 commit 8da97e9
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 18 deletions.
32 changes: 31 additions & 1 deletion packages/sdk/connect/src/utils/abi-utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ensureLeading0x } from '@celo/base/lib/address'
import { AbiCoder, AbiItem, DecodedParamsObject } from '../abi-types'
import { AbiCoder, ABIDefinition, AbiItem, DecodedParamsObject } from '../abi-types'

/** @internal */
export const getAbiByName = (abi: AbiItem[], methodName: string) =>
Expand All @@ -21,6 +21,36 @@ export const parseDecodedParams = (params: DecodedParamsObject) => {
return { args, params }
}

/** @internal */
export const signatureToAbiDefinition = (fnSignature: string): ABIDefinition => {
const matches = /(?<method>[^\(]+)\((?<args>.*)\)/.exec(fnSignature)
if (matches == null) {
throw new Error(`${fnSignature} is malformed`)
}
const method = matches.groups!.method
const args = matches.groups!.args.split(',')

return {
name: method,
signature: fnSignature,
type: 'function',
inputs: args.map((type, index) => {
const parts = type
.trim()
.split(' ')
.map((p) => p.trim())
if (parts.length > 2) {
throw new Error(`${fnSignature} is malformed`)
}

return {
name: parts.length > 1 ? parts[1] : `a${index}`,
type: parts[0],
}
}),
}
}

/** @internal */
export const decodeStringParameter = (ethAbi: AbiCoder, str: string) =>
ethAbi.decodeParameter('string', ensureLeading0x(str))
71 changes: 57 additions & 14 deletions packages/sdk/explorer/src/block-explorer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { ABIDefinition, Address, Block, CeloTxPending, parseDecodedParams } from '@celo/connect'
import {
ABIDefinition,
Address,
Block,
CeloTxPending,
parseDecodedParams,
signatureToAbiDefinition,
} from '@celo/connect'
import { CeloContract, ContractKit } from '@celo/contractkit'
import { PROXY_ABI } from '@celo/contractkit/lib/proxy'
import { fromFixed } from '@celo/utils/lib/fixidity'
Expand Down Expand Up @@ -53,7 +60,9 @@ export class BlockExplorer {

constructor(private kit: ContractKit, readonly contractDetails: ContractDetails[]) {
this.addressMapping = mapFromPairs(
contractDetails.map((cd) => [cd.address, getContractMappingFromDetails(cd)])
contractDetails
.filter((cd) => /Proxy$/.exec(cd.name) == null)
.map((cd) => [cd.address, getContractMappingFromDetails(cd)])
)
}

Expand Down Expand Up @@ -114,23 +123,28 @@ export class BlockExplorer {
}
}

async tryParseTxInput(address: string, input: string): Promise<null | CallDetails> {
const callSignature = input.slice(0, 10)
const { contract: contractName, abi: matchedAbi } = this.getContractMethodAbi(
address,
callSignature
)
if (!contractName || !matchedAbi) {
return null
getKnownFunction(selector: string): ABIDefinition | undefined {
// TODO(bogdan): This could be replaced with a call to 4byte.directory
// or a local database of common functions.
const knownFunctions: { [k: string]: string } = {
'0x095ea7b3': 'approve(address to, uint256 value)',
'0x4d49e87d': 'addLiquidity(uint256[] amounts, uint256 minLPToMint, uint256 deadline)',
}
const signature = knownFunctions[selector]
if (signature) {
return signatureToAbiDefinition(signature)
}
return undefined
}

buildCallDetails(contract: string, abi: ABIDefinition, input: string): CallDetails {
const encodedParameters = input.slice(10)
const { args, params } = parseDecodedParams(
this.kit.connection.getAbiCoder().decodeParameters(matchedAbi.inputs!, encodedParameters)
this.kit.connection.getAbiCoder().decodeParameters(abi.inputs!, encodedParameters)
)

// transform numbers to big numbers in params
matchedAbi.inputs!.forEach((abiInput, idx) => {
abi.inputs!.forEach((abiInput, idx) => {
if (abiInput.type === 'uint256') {
debug('transforming number param')
params[abiInput.name] = new BigNumber(args[idx])
Expand All @@ -146,10 +160,39 @@ export class BlockExplorer {
})

return {
contract: contractName,
function: matchedAbi.name!,
contract,
function: abi.name!,
paramMap: params,
argList: args,
}
}

tryParseAsCoreContractCall(address: string, input: string): CallDetails | null {
const selector = input.slice(0, 10)
const { contract: contractName, abi: matchedAbi } = this.getContractMethodAbi(address, selector)

if (matchedAbi === undefined || contractName === undefined) {
return null
}

return this.buildCallDetails(contractName, matchedAbi, input)
}

tryParseAsExternalContractCall(address: string, input: string): CallDetails | null {
const selector = input.slice(0, 10)
const matchedAbi = this.getKnownFunction(selector)
if (matchedAbi === undefined) {
return null
}

return this.buildCallDetails(address, matchedAbi, input)
}

async tryParseTxInput(address: string, input: string): Promise<CallDetails | null> {
let callDetails = this.tryParseAsCoreContractCall(address, input)
if (callDetails == null) {
callDetails = this.tryParseAsExternalContractCall(address, input)
}
return callDetails
}
}
13 changes: 10 additions & 3 deletions packages/sdk/governance/src/proposals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Contract,
getAbiByName,
parseDecodedParams,
signatureToAbiDefinition,
} from '@celo/connect'
import {
CeloContract,
Expand Down Expand Up @@ -290,6 +291,14 @@ export class ProposalBuilder {
RegisteredContracts.includes(stripProxy(contract)) ||
this.getRegistryAddition(contract) !== undefined

buildFunctionCallToExternalContract = async (
tx: ProposalTransactionJSON
): Promise<ProposalTransaction> => {
const methodABI = signatureToAbiDefinition(tx.function)
const input = this.kit.connection.getAbiCoder().encodeFunctionCall(methodABI, tx.args)
return { input, to: tx.contract, value: tx.value }
}

fromJsonTx = async (tx: ProposalTransactionJSON): Promise<ProposalTransaction> => {
if (isRegistryRepoint(tx)) {
// Update canonical registry addresses
Expand All @@ -304,9 +313,7 @@ export class ProposalBuilder {
`Transaction to unregistered contract ${tx.contract} only supported by address`
)
} else if (tx.function !== '' || tx.args !== []) {
throw new Error(
`Function ${tx.function} call with args ${tx.args} to unregistered contract not currently supported`
)
return this.buildFunctionCallToExternalContract(tx)
}
return { input: '', to: tx.contract, value: tx.value }
}
Expand Down

0 comments on commit 8da97e9

Please sign in to comment.