Skip to content

Commit

Permalink
Merge pull request #1047 from DarkFlorist/gnosis-transaction-simulation
Browse files Browse the repository at this point in the history
gnosis transaction simulation
  • Loading branch information
KillariDev committed Jun 21, 2024
2 parents 694e7d0 + 46f576b commit fa736d1
Show file tree
Hide file tree
Showing 11 changed files with 338 additions and 146 deletions.
2 changes: 1 addition & 1 deletion app/css/interceptor.css
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ button:where(:not(.btn)):hover {
background-color: var(--highlighted-primary-color);
}

button:not(.btn):disabled, button[disabled]) {
button:not(.btn):disabled, button:not(.btn)[disabled] {
background-color: var(--disabled-primary-color);
border-color: transparent;
}
Expand Down
52 changes: 44 additions & 8 deletions app/ts/background/background.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { InpageScriptRequest, PopupMessage, RPCReply, Settings } from '../types/interceptor-messages.js'
import { InpageScriptRequest, PopupMessage, RPCReply, Settings, SimulateExecutionReplyData } from '../types/interceptor-messages.js'
import 'webextension-polyfill'
import { Simulator, parseEvents, parseInputData, runProtectorsForTransaction } from '../simulation/simulator.js'
import { getSimulationResults, getTabState, setLatestUnexpectedError, updateSimulationResults, updateSimulationResultsWithCallBack } from './storageVariables.js'
import { changeSimulationMode, getSettings, getMakeMeRich, getWethForChainId } from './settings.js'
import { blockNumber, call, chainId, estimateGas, gasPrice, getAccounts, getBalance, getBlockByNumber, getCode, getLogs, getPermissions, getSimulationStack, getTransactionByHash, getTransactionCount, getTransactionReceipt, netVersion, personalSign, sendTransaction, subscribe, switchEthereumChain, unsubscribe, web3ClientVersion, getBlockByHash, feeHistory, installNewFilter, uninstallNewFilter, getFilterChanges, getFilterLogs, handleIterceptorError } from './simulationModeHanders.js'
import { changeActiveAddress, changeMakeMeRich, changePage, confirmDialog, refreshSimulation, removeTransactionOrSignedMessage, requestAccountsFromSigner, refreshPopupConfirmTransactionSimulation, confirmRequestAccess, changeInterceptorAccess, changeChainDialog, popupChangeActiveRpc, enableSimulationMode, addOrModifyAddressBookEntry, getAddressBookData, removeAddressBookEntry, refreshHomeData, interceptorAccessChangeAddressOrRefresh, refreshPopupConfirmTransactionMetadata, changeSettings, importSettings, exportSettings, setNewRpcList, simulateGovernanceContractExecutionOnPass, openNewTab, settingsOpened, changeAddOrModifyAddressWindowState, popupFetchAbiAndNameFromEtherscan, openWebPage, disableInterceptor, requestNewHomeData, setEnsNameForHash } from './popupMessageHandlers.js'
import { CompleteVisualizedSimulation, ProtectorResults, SimulationState, VisualizedSimulatorState, WebsiteCreatedEthereumUnsignedTransactionOrFailed } from '../types/visualizer-types.js'
import { changeActiveAddress, changeMakeMeRich, changePage, confirmDialog, refreshSimulation, removeTransactionOrSignedMessage, requestAccountsFromSigner, refreshPopupConfirmTransactionSimulation, confirmRequestAccess, changeInterceptorAccess, changeChainDialog, popupChangeActiveRpc, enableSimulationMode, addOrModifyAddressBookEntry, getAddressBookData, removeAddressBookEntry, refreshHomeData, interceptorAccessChangeAddressOrRefresh, refreshPopupConfirmTransactionMetadata, changeSettings, importSettings, exportSettings, setNewRpcList, simulateGovernanceContractExecutionOnPass, openNewTab, settingsOpened, changeAddOrModifyAddressWindowState, popupFetchAbiAndNameFromEtherscan, openWebPage, disableInterceptor, requestNewHomeData, setEnsNameForHash, simulateGnosisSafeTransactionOnPass } from './popupMessageHandlers.js'
import { CompleteVisualizedSimulation, ProtectorResults, SimulationState, VisualizedSimulatorState, WebsiteCreatedEthereumUnsignedTransaction, WebsiteCreatedEthereumUnsignedTransactionOrFailed } from '../types/visualizer-types.js'
import { WebsiteTabConnections } from '../types/user-interface-types.js'
import { askForSignerAccountsFromSignerIfNotAvailable, interceptorAccessMetadataRefresh, requestAccessFromUser, updateInterceptorAccessViewWithPendingRequests } from './windows/interceptorAccess.js'
import { FourByteExplanations, METAMASK_ERROR_FAILED_TO_PARSE_REQUEST, METAMASK_ERROR_NOT_AUTHORIZED, METAMASK_ERROR_NOT_CONNECTED_TO_CHAIN, ERROR_INTERCEPTOR_DISABLED, NEW_BLOCK_ABORT, ETHEREUM_LOGS_LOGGER_ADDRESS } from '../utils/constants.js'
import { sendActiveAccountChangeToApprovedWebsitePorts, sendMessageToApprovedWebsitePorts, updateWebsiteApprovalAccesses, verifyAccess } from './accessManagement.js'
import { getActiveAddressEntry, getAddressBookEntriesForVisualiser, identifyAddress, nameTokenIds, retrieveEnsLabelHashes, retrieveEnsNodeHashes } from './metadataUtils.js'
import { getActiveAddress, sendPopupMessageToOpenWindows } from './backgroundUtils.js'
import { assertNever, assertUnreachable, modifyObject } from '../utils/typescript.js'
import { DistributiveOmit, assertNever, assertUnreachable, modifyObject } from '../utils/typescript.js'
import { EthereumClientService } from '../simulation/services/EthereumClientService.js'
import { appendTransaction, calculateGasPrice, copySimulationState, getEmptySimulationStateWithRichAddress, getNonceFixedSimulatedTransactions, getTokenBalancesAfter, getWebsiteCreatedEthereumUnsignedTransactions, mockSignTransaction, setSimulationTransactionsAndSignedMessages } from '../simulation/services/SimulationModeEthereumClientService.js'
import { appendTransaction, calculateGasPrice, copySimulationState, getEmptySimulationStateWithRichAddress, getNonceFixedSimulatedTransactions, getTokenBalancesAfter, getWebsiteCreatedEthereumUnsignedTransactions, mockSignTransaction, setSimulationTransactionsAndSignedMessages, simulateEstimateGas } from '../simulation/services/SimulationModeEthereumClientService.js'
import { Semaphore } from '../utils/semaphore.js'
import { JsonRpcResponseError, handleUnexpectedError, isFailedToFetchError, isNewBlockAbort } from '../utils/errors.js'
import { formSimulatedAndVisualizedTransaction } from '../components/formVisualizerResults.js'
Expand All @@ -39,6 +39,7 @@ import { makeSureInterceptorIsNotSleeping } from './sleeping.js'
import { decodeEthereumError } from '../utils/errorDecoding.js'
import { estimateEthereumPricesForTokens } from '../simulation/priceEstimator.js'
import { EnrichedEthereumEvents, EnrichedEthereumInputData } from '../types/EnrichedEthereumData.js'
import { VisualizedPersonalSignRequestSafeTx } from '../types/personal-message-definitions.js'

async function updateMetadataForSimulation(simulationState: SimulationState, ethereum: EthereumClientService, requestAbortController: AbortController | undefined, eventsForEachTransaction: readonly EnrichedEthereumEvents[], inputData: readonly EnrichedEthereumInputData[], protectorResults: readonly ProtectorResults[]) {
const settingsPromise = getSettings()
Expand All @@ -60,8 +61,8 @@ async function updateMetadataForSimulation(simulationState: SimulationState, eth
}
}

export const simulateGovernanceContractExecution = async (pendingTransaction: PendingTransaction, ethereum: EthereumClientService) => {
const returnError = (text: string) => ({ success: false as const, error: { type: 'Other' as const, message: text } })
export const simulateGovernanceContractExecution = async (pendingTransaction: PendingTransaction, ethereum: EthereumClientService): Promise<DistributiveOmit<SimulateExecutionReplyData, 'transactionOrMessageIdentifier'>> => {
const returnError = (errorMessage: string) => ({ success: false as const, errorType: 'Other' as const, errorMessage })
try {
// identifies compound governane call and performs simulation if the vote passes
if (pendingTransaction.transactionOrMessageCreationStatus !== 'Simulated') return returnError('Still simulating the voting transaction')
Expand All @@ -85,7 +86,7 @@ export const simulateGovernanceContractExecution = async (pendingTransaction: Pe
if (pendingTransaction.transactionToSimulate.transaction.to === null) return returnError('The transaction creates a contract instead of casting a vote')
const params = governanceContractInterface.decodeFunctionData(voteFunction, dataStringWith0xStart(pendingTransaction.transactionToSimulate.transaction.input))
const addr = await identifyAddress(ethereum, undefined, pendingTransaction.transactionToSimulate.transaction.to)
if (!('abi' in addr) || addr.abi === undefined) return { success: false as const, error: { type: 'MissingAbi' as const, message: 'ABi for the governance contract is missing', addressBookEntry: addr } }
if (!('abi' in addr) || addr.abi === undefined) return { success: false as const, errorType: 'MissingAbi' as const, errorMessage: 'ABi for the governance contract is missing', errorAddressBookEntry: addr }
const contractExecutionResult = await simulateCompoundGovernanceExecution(ethereum, addr, params[0])
if (contractExecutionResult === undefined) return returnError('Failed to simulate governance execution')
const parentBlock = await ethereum.getBlock(undefined)
Expand Down Expand Up @@ -122,6 +123,40 @@ export const simulateGovernanceContractExecution = async (pendingTransaction: Pe
}
}

export const simulateGnosisSafeMetaTransaction = async (gnosisSafeMessage: VisualizedPersonalSignRequestSafeTx, simulationState: SimulationState | undefined, ethereumClientService: EthereumClientService): Promise<DistributiveOmit<SimulateExecutionReplyData, 'transactionOrMessageIdentifier'>> => {
const returnError = (errorMessage: string) => ({ success: false as const, errorType: 'Other' as const, errorMessage })
try {
const transactionWithoutGas = {
value: gnosisSafeMessage.message.message.value,
to: gnosisSafeMessage.to.address,
maxPriorityFeePerGas: 0n,
maxFeePerGas: 0n,
input: gnosisSafeMessage.parsedMessageData.input,
type: '1559' as const,
from: gnosisSafeMessage.verifyingContract.address,
nonce: 0n,
chainId: ethereumClientService.getChainId(),
}
const gasLimit = gnosisSafeMessage.message.message.baseGas !== 0n ? { gas: gnosisSafeMessage.message.message.baseGas } : await simulateEstimateGas(ethereumClientService, undefined, simulationState, transactionWithoutGas)
if ('error' in gasLimit) return returnError(gasLimit.error.message)
const transaction = { ...transactionWithoutGas, gas: gasLimit.gas }
const metaTransaction: WebsiteCreatedEthereumUnsignedTransaction = {
website: gnosisSafeMessage.website,
created: new Date(),
originalRequestParameters: { method: 'eth_sendTransaction', params: [transaction] },
transactionIdentifier: gnosisSafeMessage.messageIdentifier,
success: true,
transaction,
}
const simulationStateAfterGnosisSafeMetaTransaction = await appendTransaction(ethereumClientService, undefined, simulationState, metaTransaction)
return { success: true as const, result: await visualizeSimulatorState(simulationStateAfterGnosisSafeMetaTransaction, ethereumClientService, undefined) }
} catch(error) {
console.warn(error)
if (error instanceof Error) return returnError(error.message)
return returnError('Unknown error occured')
}
}

async function visualizeSimulatorState(simulationState: SimulationState, ethereum: EthereumClientService, requestAbortController: AbortController | undefined): Promise<VisualizedSimulatorState> {
const transactions = getWebsiteCreatedEthereumUnsignedTransactions(simulationState.simulatedTransactions)
const eventsForEachTransactionPromise = Promise.all(simulationState.simulatedTransactions.map(async (simulatedTransaction) => simulatedTransaction.ethSimulateV1CallResult.status === 'failure' ? [] : await parseEvents(simulatedTransaction.ethSimulateV1CallResult.logs, ethereum, requestAbortController)))
Expand Down Expand Up @@ -613,6 +648,7 @@ export async function popupMessageHandler(
case 'popup_get_export_settings': return await exportSettings()
case 'popup_set_rpc_list': return await setNewRpcList(simulator, parsedRequest, settings)
case 'popup_simulateGovernanceContractExecution': return await simulateGovernanceContractExecutionOnPass(simulator.ethereum, parsedRequest)
case 'popup_simulateGnosisSafeTransaction': return await simulateGnosisSafeTransactionOnPass(simulator.ethereum, parsedRequest)
case 'popup_changeAddOrModifyAddressWindowState': return await changeAddOrModifyAddressWindowState(simulator.ethereum, parsedRequest)
case 'popup_fetchAbiAndNameFromEtherscan': return await popupFetchAbiAndNameFromEtherscan(parsedRequest)
case 'popup_openWebPage': return await openWebPage(parsedRequest)
Expand Down
19 changes: 14 additions & 5 deletions app/ts/background/popupMessageHandlers.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { changeActiveAddressAndChainAndResetSimulation, changeActiveRpc, refreshConfirmTransactionSimulation, updateSimulationState, updateSimulationMetadata, simulateGovernanceContractExecution, resetSimulatorStateFromConfig } from './background.js'
import { changeActiveAddressAndChainAndResetSimulation, changeActiveRpc, refreshConfirmTransactionSimulation, updateSimulationState, updateSimulationMetadata, simulateGovernanceContractExecution, resetSimulatorStateFromConfig, simulateGnosisSafeMetaTransaction } from './background.js'
import { getSettings, setUseTabsInsteadOfPopup, setMakeMeRich, setPage, setUseSignersAddressAsActiveAddress, updateWebsiteAccess, exportSettingsAndAddressBook, importSettingsAndAddressBook, getMakeMeRich, getUseTabsInsteadOfPopup, getMetamaskCompatibilityMode, setMetamaskCompatibilityMode, getPage } from './settings.js'
import { getPendingTransactionsAndMessages, getCurrentTabId, getTabState, saveCurrentTabId, setRpcList, getRpcList, getPrimaryRpcForChain, getRpcConnectionStatus, updateUserAddressBookEntries, getSimulationResults, setIdsOfOpenedTabs, getIdsOfOpenedTabs, updatePendingTransactionOrMessage, getLatestUnexpectedError, addEnsLabelHash, addEnsNodeHash } from './storageVariables.js'
import { Simulator, parseEvents, parseInputData } from '../simulation/simulator.js'
import { ChangeActiveAddress, ChangeMakeMeRich, ChangePage, RemoveTransaction, RequestAccountsFromSigner, TransactionConfirmation, InterceptorAccess, ChangeInterceptorAccess, ChainChangeConfirmation, EnableSimulationMode, ChangeActiveChain, AddOrEditAddressBookEntry, GetAddressBookData, RemoveAddressBookEntry, InterceptorAccessRefresh, InterceptorAccessChangeAddress, Settings, RefreshConfirmTransactionMetadata, ChangeSettings, ImportSettings, SetRpcList, UpdateHomePage, SimulateGovernanceContractExecutionReply, SimulateGovernanceContractExecution, ChangeAddOrModifyAddressWindowState, FetchAbiAndNameFromEtherscan, OpenWebPage, DisableInterceptor, SetEnsNameForHash, UpdateConfirmTransactionDialog, UpdateConfirmTransactionDialogPendingTransactions } from '../types/interceptor-messages.js'
import { ChangeActiveAddress, ChangeMakeMeRich, ChangePage, RemoveTransaction, RequestAccountsFromSigner, TransactionConfirmation, InterceptorAccess, ChangeInterceptorAccess, ChainChangeConfirmation, EnableSimulationMode, ChangeActiveChain, AddOrEditAddressBookEntry, GetAddressBookData, RemoveAddressBookEntry, InterceptorAccessRefresh, InterceptorAccessChangeAddress, Settings, RefreshConfirmTransactionMetadata, ChangeSettings, ImportSettings, SetRpcList, UpdateHomePage, SimulateGovernanceContractExecution, ChangeAddOrModifyAddressWindowState, FetchAbiAndNameFromEtherscan, OpenWebPage, DisableInterceptor, SetEnsNameForHash, UpdateConfirmTransactionDialog, UpdateConfirmTransactionDialogPendingTransactions, SimulateGnosisSafeTransaction, SimulateExecutionReply } from '../types/interceptor-messages.js'
import { formEthSendTransaction, formSendRawTransaction, resolvePendingTransactionOrMessage, updateConfirmTransactionView } from './windows/confirmTransaction.js'
import { getAddressMetadataForAccess, requestAddressChange, resolveInterceptorAccess } from './windows/interceptorAccess.js'
import { resolveChainChange } from './windows/changeChain.js'
Expand Down Expand Up @@ -383,9 +383,18 @@ export async function simulateGovernanceContractExecutionOnPass(ethereum: Ethere
const transaction = pendingTransactions.find((tx) => tx.type === 'Transaction' && tx.transactionIdentifier === request.data.transactionIdentifier)
if (transaction === undefined || transaction.type !== 'Transaction') throw new Error(`Could not find transactionIdentifier: ${ request.data.transactionIdentifier }`)
const governanceContractExecutionVisualisation = await simulateGovernanceContractExecution(transaction, ethereum)
return await sendPopupMessageToOpenWindows(serialize(SimulateGovernanceContractExecutionReply, {
method: 'popup_simulateGovernanceContractExecutionReply' as const,
data: { ...governanceContractExecutionVisualisation, transactionIdentifier: request.data.transactionIdentifier }
return await sendPopupMessageToOpenWindows(serialize(SimulateExecutionReply, {
method: 'popup_simulateExecutionReply' as const,
data: { ...governanceContractExecutionVisualisation, transactionOrMessageIdentifier: request.data.transactionIdentifier }
}))
}

export async function simulateGnosisSafeTransactionOnPass(ethereum: EthereumClientService, request: SimulateGnosisSafeTransaction) {
const simulationResults = await getSimulationResults()
const gnosisTransactionExecutionVisualisation = await simulateGnosisSafeMetaTransaction(request.data.gnosisSafeMessage, simulationResults.simulationState, ethereum)
return await sendPopupMessageToOpenWindows(serialize(SimulateExecutionReply, {
method: 'popup_simulateExecutionReply' as const,
data: { ...gnosisTransactionExecutionVisualisation, transactionOrMessageIdentifier: request.data.gnosisSafeMessage.messageIdentifier }
}))
}

Expand Down
2 changes: 1 addition & 1 deletion app/ts/background/windows/confirmTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ export async function openConfirmTransactionDialogForTransaction(
const openedDialog = await getPendingTransactionWindow(simulator, websiteTabConnections)
if (openedDialog === undefined) throw new Error('Failed to get pending transaction window!')

const pendingTransaction = {
const pendingTransaction = {
type: 'Transaction' as const,
popupOrTabId: openedDialog,
originalRequestParameters: transactionParams,
Expand Down
5 changes: 3 additions & 2 deletions app/ts/components/pages/ConfirmTransaction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,7 @@ export function ConfirmTransaction() {
setActiveAddressAndInformAboutIt = { undefined }
modifyAddressWindowState = { modalState.state }
close = { () => { setModalState({ page: 'noModal' }) } }
activeAddress = { undefined }
activeAddress = { currentPendingTransactionOrSignableMessage?.activeAddress }
/>
: <></> }
</div>
Expand Down Expand Up @@ -568,7 +568,7 @@ export function ConfirmTransaction() {
setActiveAddressAndInformAboutIt = { undefined }
modifyAddressWindowState = { modalState.state }
close = { () => { setModalState({ page: 'noModal' }) } }
activeAddress = { undefined }
activeAddress = { currentPendingTransactionOrSignableMessage?.activeAddress }
/>
: <></> }
</div>
Expand Down Expand Up @@ -615,6 +615,7 @@ export function ConfirmTransaction() {
renameAddressCallBack = { renameAddressCallBack }
removeTransactionOrSignedMessage = { undefined }
numberOfUnderTransactions = { underTransactions.length }
editEnsNamedHashCallBack = { editEnsNamedHashCallBack }
/>
</> }
</div>
Expand Down
Loading

0 comments on commit fa736d1

Please sign in to comment.