Skip to content
This repository has been archived by the owner on Nov 10, 2023. It is now read-only.

Commit

Permalink
Tx Simulation (#3925)
Browse files Browse the repository at this point in the history
* feat: Add tx simulation

* refactor: Add function return types

* style: Format simulation message

* change: pass manualGasLimit to tx simulation

* simulateTx accepts an optional gasLimit parameter
* extends .env.example by simulation variables
* check that simulation env variables are set when displaying simulate button

* change: improve simulation for signed txs

- Signed transactions won't overwrite the threshold on tenderly
- This leads to a precise simulation

* feat: tx simulation for BatchExecute

extended useSimulation:
- dynamic to address as parameter
- return resetSimulation function

new design:
- Simulate button as secondary button next to submit
- SimulationResult styled and below buttons
- SimulationResult closable / resetable on gas paramter changes

* change: TxSimulation as reusable component / simulation is wrapped in accordion

* change: tracking for simulation

* change: refactor all specific hooks out of useSimulation

Goal of this refactor is to be able to reuse useSimulation in the web-core project. Therefore all safe-react specific hooks were moved into the TxSimulation component.

Adds unit test for useSimulation hook

* fix: simulation during tx creation

* change: show spinner when simulation is loading and disable button

other changes:
- renamed some variables / simplified some boolean conditions

* fix: review issues

- Use MUI Alert component for SimulationResult
- Comments
- remove functions which already existed in safe-react project

* Remove unused functions

* chore: add simulation .env to deploy script

* fix: remove unused type

* fix: typo in error message

Co-authored-by: Mikhail <[email protected]>

* change: Add Simulation to list of chain features

- new feature TX_SIMULATION
- TxSimulation checks if TX_SIMULATION is available and doesn't display simulation otherwise
- typo

* fix: disable simulate when submit is disabled

* chore: renamed variable

* refactor: hasFeature instead of looking through the list manually

* fix: upgrade CGW to 3.1.3

* Fix && -> ||

* Check the simulation toggle in txmodalwrapper

Co-authored-by: schmanu <[email protected]>
Co-authored-by: Manuel Gellfart <[email protected]>
Co-authored-by: Mikhail <[email protected]>
Co-authored-by: katspaugh <[email protected]>
  • Loading branch information
5 people authored Jun 29, 2022
1 parent 9165bd4 commit 4dc0bd9
Show file tree
Hide file tree
Showing 14 changed files with 1,193 additions and 69 deletions.
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ REACT_APP_COLLECTIBLES_SOURCE=
REACT_APP_ETHERSCAN_API_KEY=
REACT_APP_ETHGASSTATION_API_KEY=

# For tx simulation
REACT_APP_TENDERLY_SIMULATE_ENDPOINT_URL=
REACT_APP_TENDERLY_PROJECT_NAME=
REACT_APP_TENDERLY_ORG_NAME=


# Versions
REACT_APP_LATEST_SAFE_VERSION=

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ jobs:
REACT_APP_GOOGLE_TAG_MANAGER_ID: ${{ secrets.REACT_APP_GOOGLE_TAG_MANAGER_ID }}
REACT_APP_GOOGLE_TAG_MANAGER_LIVE_AUTH: ${{ secrets.REACT_APP_GOOGLE_TAG_MANAGER_LIVE_AUTH }}
REACT_APP_GOOGLE_TAG_MANAGER_DEVELOPMENT_AUTH: ${{ secrets.REACT_APP_GOOGLE_TAG_MANAGER_DEVELOPMENT_AUTH }}
REACT_APP_TENDERLY_SIMULATE_ENDPOINT_URL: ${{ secrets.REACT_APP_TENDERLY_SIMULATE_ENDPOINT_URL }}
REACT_APP_TENDERLY_PROJECT_NAME: ${{ secrets.REACT_APP_TENDERLY_PROJECT_NAME }}
REACT_APP_TENDERLY_ORG_NAME: ${{ secrets.REACT_APP_TENDERLY_ORG_NAME }}

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
Expand Down
60 changes: 25 additions & 35 deletions src/logic/safe/transactions/gas.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { List } from 'immutable'
import { FEATURES } from '@gnosis.pm/safe-react-gateway-sdk'

import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
import { calculateGasOf } from 'src/logic/wallets/ethTransactions'
import { generateSignaturesFromTxConfirmations } from 'src/logic/safe/safeTxSigner'
import { fetchSafeTxGasEstimation } from 'src/logic/safe/api/fetchSafeTxGasEstimation'
import { Confirmation } from 'src/logic/safe/store/models/types/confirmation'
import { checksumAddress } from 'src/utils/checksumAddress'
import { hasFeature } from '../utils/safeVersion'
import { PayableTx } from 'src/types/contracts/types'
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
import { TxArgs } from 'src/logic/safe/store/models/types/transaction'

export type SafeTxGasEstimationProps = {
safeAddress: string
Expand Down Expand Up @@ -61,59 +60,50 @@ export type TransactionExecutionEstimationProps = {
}

export const estimateGasForTransactionExecution = async ({
safeAddress,
safeVersion,
txRecipient,
txConfirmations,
txAmount,
txData,
operation,
from,
baseGas,
data,
gasPrice,
gasToken,
operation,
refundReceiver,
safeInstance,
safeTxGas,
approvalAndExecution,
}: TransactionExecutionEstimationProps): Promise<number> => {
const safeInstance = getGnosisSafeInstanceAt(safeAddress, safeVersion)
// If it's approvalAndExecution we have to add a preapproved signature else we have all signatures
const sigs = generateSignaturesFromTxConfirmations(txConfirmations, approvalAndExecution ? from : undefined)

sigs,
to,
valueInWei,
safeAddress,
sender,
}: TxArgs & { safeAddress: string }): Promise<number> => {
const estimationData = safeInstance.methods
.execTransaction(txRecipient, txAmount, txData, operation, safeTxGas, 0, gasPrice, gasToken, refundReceiver, sigs)
.execTransaction(to, valueInWei, data, operation, safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, sigs)
.encodeABI()

return calculateGasOf({
data: estimationData,
from,
from: sender,
to: safeAddress,
})
}

export const checkTransactionExecution = async ({
safeAddress,
safeVersion,
txRecipient,
txConfirmations,
txAmount,
txData,
operation,
from,
baseGas,
data,
gasPrice,
gasToken,
gasLimit,
operation,
refundReceiver,
safeInstance,
safeTxGas,
approvalAndExecution,
}: TransactionExecutionEstimationProps): Promise<boolean> => {
const safeInstance = getGnosisSafeInstanceAt(safeAddress, safeVersion)
// If it's approvalAndExecution we have to add a preapproved signature else we have all signatures
const sigs = generateSignaturesFromTxConfirmations(txConfirmations, approvalAndExecution ? from : undefined)

sigs,
to,
valueInWei,
sender,
gasLimit,
}: TxArgs & { gasLimit: string | undefined }): Promise<boolean> => {
return safeInstance.methods
.execTransaction(txRecipient, txAmount, txData, operation, safeTxGas, 0, gasPrice, gasToken, refundReceiver, sigs)
.execTransaction(to, valueInWei, data, operation, safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, sigs)
.call({
from,
from: sender,
gas: gasLimit,
})
.then(() => true)
Expand Down
45 changes: 31 additions & 14 deletions src/routes/safe/components/Transactions/TxList/BatchExecute.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { ReactElement, useState } from 'react'
import React, { ReactElement, useMemo, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import styled from 'styled-components'

Expand All @@ -11,7 +11,7 @@ import { generateSignaturesFromTxConfirmations } from 'src/logic/safe/safeTxSign
import { getExecutionTransaction } from 'src/logic/safe/transactions'
import { getGnosisSafeInstanceAt, getMultisendContractAddress } from 'src/logic/contracts/safeContracts'
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
import { getMultiSendJoinedTxs, MultiSendTx } from 'src/logic/safe/transactions/multisend'
import { encodeMultiSendCall, getMultiSendJoinedTxs, MultiSendTx } from 'src/logic/safe/transactions/multisend'
import { userAccountSelector } from 'src/logic/wallets/store/selectors'
import { getBatchableTransactions } from 'src/logic/safe/store/selectors/gatewayTransactions'
import { Dispatch } from 'src/logic/safe/store/actions/types'
Expand Down Expand Up @@ -42,6 +42,8 @@ import { TransactionFailText } from 'src/components/TransactionFailText'
import { EstimationStatus } from 'src/logic/hooks/useEstimateTransactionGas'
import { BatchExecuteButton } from 'src/routes/safe/components/Transactions/TxList/BatchExecuteButton'
import { Errors, logError } from 'src/logic/exceptions/CodedException'
import { BaseTransaction } from '@gnosis.pm/safe-apps-sdk'
import { TxSimulation } from '../helpers/Simulation/TxSimulation'

const DecodedTransactions = ({
transactions,
Expand Down Expand Up @@ -99,16 +101,15 @@ async function getTxDetails(transactions: Transaction[], dispatch: Dispatch) {
)
}

async function getBatchExecuteData(
dispatch: Dispatch,
function toMultiSendTxs(
transactions: Transaction[],
safeAddress: string,
safeVersion: string,
account: string,
) {
): MultiSendTx[] {
const safeInstance = getGnosisSafeInstanceAt(safeAddress, safeVersion)

const txs: MultiSendTx[] = transactions.map((transaction) => {
return transactions.map((transaction) => {
const txInfo = getTxInfo(transaction, safeAddress)
const confirmations = getTxConfirmations(transaction)
const sigs = generateSignaturesFromTxConfirmations(confirmations)
Expand All @@ -122,6 +123,15 @@ async function getBatchExecuteData(
data,
}
})
}

async function getBatchExecuteData(
transactions: Transaction[],
safeAddress: string,
safeVersion: string,
account: string,
) {
const txs = toMultiSendTxs(transactions, safeAddress, safeVersion, account)

return getMultiSendJoinedTxs(txs)
}
Expand Down Expand Up @@ -156,13 +166,7 @@ export const BatchExecute = React.memo((): ReactElement | null => {
setTxsWithDetails(transactionsWithDetails)

try {
const batchExecuteData = await getBatchExecuteData(
dispatch,
transactionsWithDetails,
safeAddress,
currentVersion,
account,
)
const batchExecuteData = await getBatchExecuteData(transactionsWithDetails, safeAddress, currentVersion, account)
setButtonStatus(isSameAddressAsSafe ? ButtonStatus.DISABLED : ButtonStatus.READY)
setMultiSendCallData(batchExecuteData)
} catch (err) {
Expand All @@ -184,6 +188,17 @@ export const BatchExecute = React.memo((): ReactElement | null => {
toggleModal()
}

const multiSendTx: Omit<BaseTransaction, 'value'> | null = useMemo(() => {
if (!account || !safeAddress || !currentVersion || txsWithDetails.length === 0) {
return null
}
const txs = toMultiSendTxs(txsWithDetails, safeAddress, currentVersion, account)
return {
data: encodeMultiSendCall(txs),
to: getMultisendContractAddress(),
}
}, [account, txsWithDetails, currentVersion, safeAddress])

if (!account) {
return null
}
Expand Down Expand Up @@ -217,7 +232,7 @@ export const BatchExecute = React.memo((): ReactElement | null => {
explorerUrl={getExplorerInfo(multiSendContractAddress)}
/>
</Row>
<Row margin="md">
<Row>
<DecodeTxsWrapper>
{txsWithDetails.length ? (
<DecodedTransactions transactions={txsWithDetails} safeAddress={safeAddress} />
Expand All @@ -230,6 +245,8 @@ export const BatchExecute = React.memo((): ReactElement | null => {
)}
</DecodeTxsWrapper>
</Row>
{multiSendTx && <TxSimulation canTxExecute tx={multiSendTx} disabled={buttonStatus !== ButtonStatus.READY} />}

<Paragraph size="md" align="center" color="disabled" noMargin>
Be aware that if any of the included transactions revert, none of them will be executed. This will result in
the loss of the allocated transaction fees.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Icon, Link, Text } from '@gnosis.pm/safe-react-components'
import { Alert, AlertTitle } from '@material-ui/lab'
import { FETCH_STATUS } from 'src/utils/requests'
import styled from 'styled-components'
import { TenderlySimulation } from './types'

const StyledAlert = styled(Alert)`
align-items: flex-start;
width: 100%;
&.MuiAlert-standardError {
background-color: #fff3f5;
}
&.MuiAlert-standardSuccess {
background-color: #effaf8;
}
& .MuiIconButton-root {
padding: 3px !important;
}
`

type SimulationResultProps = {
simulationRequestStatus: string
simulation?: TenderlySimulation
simulationLink?: string
requestError?: string
onClose: () => void
}

export const SimulationResult = ({
simulationRequestStatus,
simulation,
simulationLink,
requestError,
onClose,
}: SimulationResultProps): React.ReactElement => {
const isErroneous = !!requestError || !simulation?.simulation.status
const isSimulationFinished =
simulationRequestStatus === FETCH_STATUS.SUCCESS || simulationRequestStatus === FETCH_STATUS.ERROR

return (
<>
{isSimulationFinished && (
<>
{isErroneous ? (
<StyledAlert severity="error" onClose={onClose} icon={<Icon type="alert" color="error" size="sm" />}>
<AlertTitle>
<Text color={'error'} size="lg">
<b>Failed</b>
</Text>
</AlertTitle>
{requestError ? (
<Text color="error" size="lg">
An unexpected error occurred during simulation: <b>{requestError}</b>
</Text>
) : (
<Text color="inputFilled" size="lg">
The batch failed during the simulation throwing error <b>{simulation?.transaction.error_message}</b>{' '}
in the contract at <b>{simulation?.transaction.error_info?.address}</b>. Full simulation report is
available{' '}
<Link href={simulationLink} target="_blank" rel="noreferrer" size="lg">
<b>on Tenderly</b>
</Link>
.
</Text>
)}
</StyledAlert>
) : (
<StyledAlert severity="success" icon={<Icon type="check" color="primary" size="sm" />} onClose={onClose}>
<AlertTitle>
<Text color={'primary'} size="lg">
<b>Success</b>
</Text>
</AlertTitle>
<Text color="inputFilled" size="lg">
The batch was successfully simulated. Full simulation report is available{' '}
<Link href={simulationLink} target="_blank" rel="noreferrer" size="lg">
<b>on Tenderly</b>
</Link>
.
</Text>
</StyledAlert>
)}
</>
)}
</>
)
}
Loading

0 comments on commit 4dc0bd9

Please sign in to comment.