UNPKG

contract-proxy-kit

Version:

Enable batched transactions and contract account interactions using a unique deterministic Gnosis Safe.

264 lines (235 loc) 7.35 kB
import EthLibAdapter, { Contract } from '../../ethLibAdapters/EthLibAdapter' import { Address, NumberLike } from '../../utils/basicTypes' import { zeroAddress } from '../../utils/constants' import { NormalizeGas, OperationType, SendOptions, SimpleTransactionResult, StandardTransaction, TransactionError, TransactionResult } from '../../utils/transactions' import TransactionManager, { ExecTransactionProps, TransactionManagerConfig, TransactionManagerNames } from '../TransactionManager' interface ContractTxObj { contract: Contract methodName: string params: any[] } class CpkTransactionManager implements TransactionManager { /** * Returns the configuration of the CpkTransactionManager. * * @returns The name of the TransactionManager in use and the URL of the service */ get config(): TransactionManagerConfig { return { name: TransactionManagerNames.CpkTxManager, url: undefined } } /** * Executes a list of transactions. * * @param options * @returns The transaction response */ async execTransactions({ ownerAccount, safeExecTxParams, transactions, ethLibAdapter, contractManager, saltNonce, isDeployed, isConnectedToSafe, sendOptions }: ExecTransactionProps): Promise<TransactionResult> { const { contract: safeContract, proxyFactory, masterCopyAddress, fallbackHandlerAddress } = contractManager if (!safeContract) { throw new Error('CPK Proxy contract uninitialized') } if (isConnectedToSafe) { return this.execTxsWhileConnectedToSafe(ethLibAdapter, transactions, sendOptions) } // (r, s, v) where v is 1 means this signature is approved by the address encoded in value r // "Hashes are automatically approved by the sender of the message" const autoApprovedSignature = ethLibAdapter.abiEncodePacked( { type: 'uint256', value: ownerAccount }, // r { type: 'uint256', value: 0 }, // s { type: 'uint8', value: 1 } // v ) const txObj: ContractTxObj = isDeployed ? this.getSafeProxyTxObj(safeContract, safeExecTxParams, autoApprovedSignature) : this.getCPKFactoryTxObj( masterCopyAddress, fallbackHandlerAddress, safeExecTxParams, saltNonce, proxyFactory ) const { success, gasLimit } = await this.findGasLimit(ethLibAdapter, txObj, sendOptions) sendOptions.gas = gasLimit const isSingleTx = transactions.length === 1 if (!success) { throw await this.makeTransactionError( ethLibAdapter, safeExecTxParams, safeContract.address, gasLimit, isDeployed, isSingleTx ) } const { contract, methodName, params } = txObj return contract.send(methodName, params, sendOptions) } private async execTxsWhileConnectedToSafe( ethLibAdapter: EthLibAdapter, transactions: StandardTransaction[], sendOptions: SendOptions ): Promise<SimpleTransactionResult> { if (transactions.some(({ operation }) => operation === OperationType.DelegateCall)) { throw new Error('DelegateCall unsupported by Gnosis Safe') } if (transactions.length === 1) { const { to, value, data } = transactions[0] return ethLibAdapter.ethSendTransaction({ to, value, data, ...sendOptions }) } return { hash: await ethLibAdapter.providerSend( 'gs_multi_send', transactions.map(({ to, value, data }) => ({ to, value, data })) ) } } private getSafeProxyTxObj( safeContract: Contract, { to, value, data, operation }: StandardTransaction, safeAutoApprovedSignature: string ): ContractTxObj { return { contract: safeContract, methodName: 'execTransaction', params: [ to, value, data, operation, 0, 0, 0, zeroAddress, zeroAddress, safeAutoApprovedSignature ] } } private getCPKFactoryTxObj( masterCopyAddress: Address, fallbackHandlerAddress: Address, { to, value, data, operation }: StandardTransaction, saltNonce: string, proxyFactory: Contract ): ContractTxObj { return { contract: proxyFactory, methodName: 'createProxyAndExecTransaction', params: [masterCopyAddress, saltNonce, fallbackHandlerAddress, to, value, data, operation] } } private async findGasLimit( ethLibAdapter: EthLibAdapter, { contract, methodName, params }: ContractTxObj, sendOptions: NormalizeGas<SendOptions> ): Promise<{ success: boolean; gasLimit: number }> { async function checkOptions(options: NormalizeGas<SendOptions>): Promise<boolean> { try { return await contract.call(methodName, params, options) } catch (e) { return false } } const toNumber = (num: NumberLike): number => Number(num.toString()) if (!sendOptions.gas) { const blockGasLimit = toNumber((await ethLibAdapter.getBlock('latest')).gasLimit) const gasEstimateOptions = { ...sendOptions, gas: blockGasLimit } if (!(await checkOptions(gasEstimateOptions))) { return { success: false, gasLimit: blockGasLimit } } const gasSearchError = 10000 let gasLow = await contract.estimateGas(methodName, params, sendOptions) let gasHigh = blockGasLimit gasEstimateOptions.gas = gasLow if (!(await checkOptions(gasEstimateOptions))) { while (gasLow + gasSearchError <= gasHigh) { const testGasLimit = Math.floor((gasLow + gasHigh) * 0.5) gasEstimateOptions.gas = testGasLimit if (await checkOptions(gasEstimateOptions)) { // values > gasHigh will work gasHigh = testGasLimit - 1 } else { // values <= gasLow will fail gasLow = testGasLimit + 1 } } } else { gasHigh = gasLow - 1 } // the final target gas value is > gasHigh const gasLimit = Math.min(Math.ceil((gasHigh + 1) * 1.02), blockGasLimit) return { success: true, gasLimit } } return { success: await checkOptions(sendOptions), gasLimit: toNumber(sendOptions.gas) } } private async makeTransactionError( ethLibAdapter: EthLibAdapter, { to, value, data, operation }: StandardTransaction, safeAddress: Address, gasLimit: number, isDeployed: boolean, isSingleTx: boolean ): Promise<Error> { let errorMessage = `${isDeployed ? '' : 'proxy creation and '}${ isSingleTx ? 'transaction' : 'batch transaction' } execution expected to fail` let revertData, revertMessage if (isSingleTx && operation === OperationType.Call) { try { revertData = await ethLibAdapter.getCallRevertData( { from: safeAddress, to, value, data, gasLimit }, 'latest' ) revertMessage = ethLibAdapter.decodeError(revertData) errorMessage = `${errorMessage}: ${revertMessage}` } catch (e) { // empty } } return new TransactionError(errorMessage, revertData, revertMessage) } } export default CpkTransactionManager