#474 Feature: upgrade contract version from interface (#544)

* Adds update safe modal

* Implements update safe transaction

* Moves the safeMasterCopyAddress and defaultFallbackHandlerAddress to a constant

* Adds mainnet network
Refactor logic

* Removes the update button to non-owners

* Updates safe-contracts package
Removes hardcoded addresses

* Refactor upgradeSafeToLastVersion
Refactor getSafeVersion
Refactor getEncodedMultiSendCallData
Removes unused constants

Adds tests for getEncodedMultiSendCallData and checkIfSafeNeedUpdate

* Adds retro-compatibility with v1.0.0

* Improves test description

* Fixs comments

* Fix typo
Removes spread

* Exports proxy code to historicProxyCode.js

* Updates yarn.lock
This commit is contained in:
Agustin Pane 2020-02-17 09:56:23 -03:00 committed by GitHub
parent d51303f348
commit 2698dcb6e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 2155 additions and 2069 deletions

View File

@ -33,8 +33,8 @@
"precommit" "precommit"
], ],
"dependencies": { "dependencies": {
"@gnosis.pm/safe-contracts": "1.0.0", "@gnosis.pm/safe-contracts": "1.1.1-dev.1",
"@gnosis.pm/util-contracts": "2.0.4", "@gnosis.pm/util-contracts": "2.0.6",
"@material-ui/core": "4.8.0", "@material-ui/core": "4.8.0",
"@material-ui/icons": "4.5.1", "@material-ui/icons": "4.5.1",
"@material-ui/lab": "^4.0.0-alpha.39", "@material-ui/lab": "^4.0.0-alpha.39",

View File

@ -0,0 +1,11 @@
// @flow
// Code of the safe v1.0.0
const proxyCodeV10 =
'0x608060405273ffffffffffffffffffffffffffffffffffffffff600054163660008037600080366000845af43d6000803e6000811415603d573d6000fd5b3d6000f3fe'
// Old PayingProxyCode
const oldProxyCode =
'0x60806040526004361061004c576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680634555d5c91461008b5780635c60da1b146100b6575b73ffffffffffffffffffffffffffffffffffffffff600054163660008037600080366000845af43d6000803e6000811415610086573d6000fd5b3d6000f35b34801561009757600080fd5b506100a061010d565b6040518082815260200191505060405180910390f35b3480156100c257600080fd5b506100cb610116565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b60006002905090565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff169050905600'
export const isProxyCode = codeToCheck =>
codeToCheck === oldProxyCode || codeToCheck === proxyCodeV10

View File

@ -1,15 +1,21 @@
// @flow // @flow
import contract from 'truffle-contract' import contract from 'truffle-contract'
import ProxyFactorySol from '@gnosis.pm/safe-contracts/build/contracts/ProxyFactory.json' import ProxyFactorySol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafeProxyFactory.json'
import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json' import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json'
import SafeProxy from '@gnosis.pm/safe-contracts/build/contracts/Proxy.json' import SafeProxy from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafeProxy.json'
import { ensureOnce } from '~/utils/singleton' import { ensureOnce } from '~/utils/singleton'
import { simpleMemoize } from '~/components/forms/validator' import { simpleMemoize } from '~/components/forms/validator'
import { getWeb3 } from '~/logic/wallets/getWeb3' import { getWeb3 } from '~/logic/wallets/getWeb3'
import { calculateGasOf, calculateGasPrice } from '~/logic/wallets/ethTransactions' import { calculateGasOf, calculateGasPrice } from '~/logic/wallets/ethTransactions'
import { ZERO_ADDRESS } from '~/logic/wallets/ethAddresses' import { ZERO_ADDRESS } from '~/logic/wallets/ethAddresses'
import { isProxyCode } from '~/logic/contracts/historicProxyCode'
export const SENTINEL_ADDRESS = '0x0000000000000000000000000000000000000001' export const SENTINEL_ADDRESS = '0x0000000000000000000000000000000000000001'
export const MULTI_SEND_ADDRESS = '0xB522a9f781924eD250A11C54105E51840B138AdD'
export const SAFE_MASTER_COPY_ADDRESS = '0x34CfAC646f301356fAa8B21e94227e3583Fe3F5F'
export const DEFAULT_FALLBACK_HANDLER_ADDRESS = '0xd5D82B6aDDc9027B22dCA772Aa68D5d74cdBdF44'
export const SAFE_MASTER_COPY_ADDRESS_V10 = '0xb6029EA3B2c51D09a50B53CA8012FeEB05bDa35A'
let proxyFactoryMaster let proxyFactoryMaster
let safeMaster let safeMaster
@ -66,7 +72,7 @@ export const getSafeMasterContract = async () => {
export const deploySafeContract = async (safeAccounts: string[], numConfirmations: number, userAccount: string) => { export const deploySafeContract = async (safeAccounts: string[], numConfirmations: number, userAccount: string) => {
const gnosisSafeData = await safeMaster.contract.methods const gnosisSafeData = await safeMaster.contract.methods
.setup(safeAccounts, numConfirmations, ZERO_ADDRESS, '0x', ZERO_ADDRESS, 0, ZERO_ADDRESS) .setup(safeAccounts, numConfirmations, ZERO_ADDRESS, '0x', DEFAULT_FALLBACK_HANDLER_ADDRESS, ZERO_ADDRESS, 0, ZERO_ADDRESS)
.encodeABI() .encodeABI()
const proxyFactoryData = proxyFactoryMaster.contract.methods const proxyFactoryData = proxyFactoryMaster.contract.methods
.createProxy(safeMaster.address, gnosisSafeData) .createProxy(safeMaster.address, gnosisSafeData)
@ -88,7 +94,7 @@ export const estimateGasForDeployingSafe = async (
userAccount: string, userAccount: string,
) => { ) => {
const gnosisSafeData = await safeMaster.contract.methods const gnosisSafeData = await safeMaster.contract.methods
.setup(safeAccounts, numConfirmations, ZERO_ADDRESS, '0x', ZERO_ADDRESS, 0, ZERO_ADDRESS) .setup(safeAccounts, numConfirmations, ZERO_ADDRESS, '0x', DEFAULT_FALLBACK_HANDLER_ADDRESS, ZERO_ADDRESS, 0, ZERO_ADDRESS)
.encodeABI() .encodeABI()
const proxyFactoryData = proxyFactoryMaster.contract.methods const proxyFactoryData = proxyFactoryMaster.contract.methods
.createProxy(safeMaster.address, gnosisSafeData) .createProxy(safeMaster.address, gnosisSafeData)
@ -126,7 +132,49 @@ export const validateProxy = async (safeAddress: string): Promise<boolean> => {
return true return true
} }
} }
// Old PayingProxyCode
const oldProxyCode = '0x60806040526004361061004c576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680634555d5c91461008b5780635c60da1b146100b6575b73ffffffffffffffffffffffffffffffffffffffff600054163660008037600080366000845af43d6000803e6000811415610086573d6000fd5b3d6000f35b34801561009757600080fd5b506100a061010d565b6040518082815260200191505060405180910390f35b3480156100c257600080fd5b506100cb610116565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b60006002905090565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff169050905600'
return codeWithoutMetadata === oldProxyCode return isProxyCode(codeWithoutMetadata)
}
export type MultiSendTransactionInstanceType = {
operation: number,
to: string,
value: number,
data: string,
}
export const getEncodedMultiSendCallData = (txs: Array<MultiSendTransactionInstanceType>, web3: Object) => {
const multiSendAbi = [
{
type: 'function',
name: 'multiSend',
constant: false,
payable: false,
stateMutability: 'nonpayable',
inputs: [{ type: 'bytes', name: 'transactions' }],
outputs: [],
},
]
const multiSend = new web3.eth.Contract(multiSendAbi, MULTI_SEND_ADDRESS)
const encodeMultiSendCallData = multiSend.methods
.multiSend(
`0x${txs
.map((tx) => [
web3.eth.abi.encodeParameter('uint8', 0).slice(-2),
web3.eth.abi.encodeParameter('address', tx.to).slice(-40),
web3.eth.abi.encodeParameter('uint256', tx.value).slice(-64),
web3.eth.abi
.encodeParameter(
'uint256',
web3.utils.hexToBytes(tx.data).length,
)
.slice(-64),
tx.data.replace(/^0x/, ''),
].join(''))
.join('')}`,
)
.encodeABI()
return encodeMultiSendCallData
} }

View File

@ -5,7 +5,6 @@ import { type Operation } from '~/logic/safe/transactions'
export const CALL = 0 export const CALL = 0
export const DELEGATE_CALL = 1 export const DELEGATE_CALL = 1
export const TX_TYPE_EXECUTION = 'execution' export const TX_TYPE_EXECUTION = 'execution'
export const TX_TYPE_CONFIRMATION = 'confirmation' export const TX_TYPE_CONFIRMATION = 'confirmation'

View File

@ -1,20 +1,24 @@
// @flow // @flow
import semverValid from 'semver/functions/valid' import semverValid from 'semver/functions/valid'
import semverLessThan from 'semver/functions/lt' import semverLessThan from 'semver/functions/lt'
import { getSafeMasterContract } from '~/logic/contracts/safeContracts' import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
import { getSafeLastVersion } from '~/config' import { getSafeLastVersion } from '~/config'
export const getSafeVersion = async () => { export const checkIfSafeNeedsUpdate = async (gnosisSafeInstance, lastSafeVersion) => {
let current if (!gnosisSafeInstance || !lastSafeVersion) {
let latest return null
try { }
const safeMaster = await getSafeMasterContract() const safeMasterVersion = await gnosisSafeInstance.VERSION()
const safeMasterVersion = await safeMaster.VERSION() const current = semverValid(safeMasterVersion)
current = semverValid(safeMasterVersion) const latest = semverValid(lastSafeVersion)
latest = semverValid(getSafeLastVersion()) const needUpdate = latest ? semverLessThan(current, latest) : false
const needUpdate = latest ? semverLessThan(current, latest) : false return { current, latest, needUpdate }
}
return { current, latest, needUpdate } export const getSafeVersion = async (safeAddress: string) => {
try {
const safeMaster = await getGnosisSafeInstanceAt(safeAddress)
return checkIfSafeNeedsUpdate(safeMaster, getSafeLastVersion())
} catch (err) { } catch (err) {
console.error(err) console.error(err)
throw err throw err

View File

@ -0,0 +1,45 @@
// @flow
import {
DEFAULT_FALLBACK_HANDLER_ADDRESS,
getEncodedMultiSendCallData,
getGnosisSafeInstanceAt,
MULTI_SEND_ADDRESS, SAFE_MASTER_COPY_ADDRESS,
} from '~/logic/contracts/safeContracts'
import type { MultiSendTransactionInstanceType } from '~/logic/contracts/safeContracts'
import { DELEGATE_CALL } from '~/logic/safe/transactions'
import { getWeb3 } from '~/logic/wallets/getWeb3'
export const upgradeSafeToLastVersion = async (safeAddress: string, createTransaction: Function) => {
const sendTransactions = async (txs: Array<MultiSendTransactionInstanceType>) => {
const web3 = getWeb3()
const encodeMultiSendCallData = getEncodedMultiSendCallData(txs, web3)
createTransaction({
safeAddress,
to: MULTI_SEND_ADDRESS,
valueInWei: 0,
txData: encodeMultiSendCallData,
notifiedTransaction: 'STANDARD_TX',
enqueueSnackbar: () => {},
closeSnackbar: () => {},
operation: DELEGATE_CALL,
})
}
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
const fallbackHandlerTxData = safeInstance.contract.methods.setFallbackHandler(DEFAULT_FALLBACK_HANDLER_ADDRESS).encodeABI()
const updateSafeTxData = safeInstance.contract.methods.changeMasterCopy(SAFE_MASTER_COPY_ADDRESS).encodeABI()
const txs = [
{
operation: 0,
to: safeAddress,
value: 0,
data: fallbackHandlerTxData,
},
{
operation: 0,
to: safeAddress,
value: 0,
data: updateSafeTxData,
},
]
return sendTransactions(txs)
}

View File

@ -14,7 +14,7 @@ import Paragraph from '~/components/layout/Paragraph'
import OpenPaper from '~/components/Stepper/OpenPaper' import OpenPaper from '~/components/Stepper/OpenPaper'
import { FIELD_LOAD_NAME, FIELD_LOAD_ADDRESS } from '~/routes/load/components/fields' import { FIELD_LOAD_NAME, FIELD_LOAD_ADDRESS } from '~/routes/load/components/fields'
import { getWeb3 } from '~/logic/wallets/getWeb3' import { getWeb3 } from '~/logic/wallets/getWeb3'
import { getSafeMasterContract, validateProxy } from '~/logic/contracts/safeContracts' import { getSafeMasterContract, SAFE_MASTER_COPY_ADDRESS_V10, validateProxy } from '~/logic/contracts/safeContracts'
import { secondary } from '~/theme/variables' import { secondary } from '~/theme/variables'
type Props = { type Props = {
@ -72,8 +72,7 @@ export const safeFieldsValidation = async (values: Object) => {
) )
const safeMaster = await getSafeMasterContract() const safeMaster = await getSafeMasterContract()
const masterCopy = safeMaster.address const masterCopy = safeMaster.address
const sameMasterCopy = checksummedProxyAddress === masterCopy || checksummedProxyAddress === SAFE_MASTER_COPY_ADDRESS_V10
const sameMasterCopy = checksummedProxyAddress === masterCopy
if (!sameMasterCopy) { if (!sameMasterCopy) {
errors[FIELD_LOAD_ADDRESS] = SAFE_MASTERCOPY_ERROR errors[FIELD_LOAD_ADDRESS] = SAFE_MASTERCOPY_ERROR
} }

View File

@ -57,7 +57,7 @@ export const calculateValuesAfterRemoving = (
const initialValues = { ...values } const initialValues = { ...values }
const numOwnersAfterRemoving = notRemovedOwners - 1 const numOwnersAfterRemoving = notRemovedOwners - 1
// muevo indices
for (let i = index; i < numOwnersAfterRemoving; i += 1) { for (let i = index; i < numOwnersAfterRemoving; i += 1) {
initialValues[getOwnerNameBy(i)] = values[getOwnerNameBy(i + 1)] initialValues[getOwnerNameBy(i)] = values[getOwnerNameBy(i + 1)]
initialValues[getOwnerAddressBy(i)] = values[getOwnerAddressBy(i + 1)] initialValues[getOwnerAddressBy(i)] = values[getOwnerAddressBy(i + 1)]

View File

@ -1,7 +1,8 @@
// @flow // @flow
import React, { useEffect } from 'react' import React, { useEffect, useState } from 'react'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import { withSnackbar } from 'notistack' import { withSnackbar } from 'notistack'
import { useSelector } from 'react-redux'
import Block from '~/components/layout/Block' import Block from '~/components/layout/Block'
import Col from '~/components/layout/Col' import Col from '~/components/layout/Col'
import Field from '~/components/forms/Field' import Field from '~/components/forms/Field'
@ -16,15 +17,20 @@ import { getNotificationsFromTxType, showSnackbar } from '~/logic/notifications'
import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions' import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
import { styles } from './style' import { styles } from './style'
import { getSafeVersion } from '~/logic/safe/utils/safeVersion' import { getSafeVersion } from '~/logic/safe/utils/safeVersion'
import UpdateSafeModal from '~/routes/safe/components/Settings/UpdateSafeModal'
import Modal from '~/components/Modal'
import { grantedSelector } from '~/routes/safe/container/selector'
export const SAFE_NAME_INPUT_TEST_ID = 'safe-name-input' export const SAFE_NAME_INPUT_TEST_ID = 'safe-name-input'
export const SAFE_NAME_SUBMIT_BTN_TEST_ID = 'change-safe-name-btn' export const SAFE_NAME_SUBMIT_BTN_TEST_ID = 'change-safe-name-btn'
export const SAFE_NAME_UPDATE_SAFE_BTN_TEST_ID = 'update-safe-name-btn'
type Props = { type Props = {
safeAddress: string, safeAddress: string,
safeName: string, safeName: string,
updateSafe: Function, updateSafe: Function,
enqueueSnackbar: Function, enqueueSnackbar: Function,
createTransaction: Function,
closeSnackbar: Function, closeSnackbar: Function,
} }
@ -33,10 +39,17 @@ const useStyles = makeStyles(styles)
const SafeDetails = (props: Props) => { const SafeDetails = (props: Props) => {
const classes = useStyles() const classes = useStyles()
const [safeVersions, setSafeVersions] = React.useState({ current: null, latest: null, needUpdate: false }) const [safeVersions, setSafeVersions] = React.useState({ current: null, latest: null, needUpdate: false })
const isUserOwner = useSelector(grantedSelector)
const { const {
safeAddress, safeName, updateSafe, enqueueSnackbar, closeSnackbar, safeAddress, safeName, updateSafe, enqueueSnackbar, closeSnackbar, createTransaction,
} = props } = props
const [isModalOpen, setModalOpen] = useState(false)
const toggleModal = () => {
setModalOpen((prevOpen) => !prevOpen)
}
const handleSubmit = (values) => { const handleSubmit = (values) => {
updateSafe({ address: safeAddress, name: values.safeName }) updateSafe({ address: safeAddress, name: values.safeName })
@ -44,10 +57,14 @@ const SafeDetails = (props: Props) => {
showSnackbar(notification.afterExecution.noMoreConfirmationsNeeded, enqueueSnackbar, closeSnackbar) showSnackbar(notification.afterExecution.noMoreConfirmationsNeeded, enqueueSnackbar, closeSnackbar)
} }
const handleUpdateSafe = () => {
setModalOpen(true)
}
useEffect(() => { useEffect(() => {
const getVersion = async () => { const getVersion = async () => {
try { try {
const { current, latest, needUpdate } = await getSafeVersion() const { current, latest, needUpdate } = await getSafeVersion(safeAddress)
setSafeVersions({ current, latest, needUpdate }) setSafeVersions({ current, latest, needUpdate })
} catch (err) { } catch (err) {
setSafeVersions({ current: 'Version not defined' }) setSafeVersions({ current: 'Version not defined' })
@ -70,6 +87,22 @@ const SafeDetails = (props: Props) => {
{safeVersions.needUpdate && ` (there's a newer version: ${safeVersions.latest})`} {safeVersions.needUpdate && ` (there's a newer version: ${safeVersions.latest})`}
</Paragraph> </Paragraph>
</Row> </Row>
{safeVersions.needUpdate && isUserOwner ? (
<Row align="end" grow>
<Paragraph>
<Button
onClick={handleUpdateSafe}
className={classes.saveBtn}
size="small"
variant="contained"
color="primary"
testId={SAFE_NAME_UPDATE_SAFE_BTN_TEST_ID}
>
Update Safe
</Button>
</Paragraph>
</Row>
) : null}
</Block> </Block>
<Block className={classes.formContainer}> <Block className={classes.formContainer}>
<Heading tag="h2">Modify Safe name</Heading> <Heading tag="h2">Modify Safe name</Heading>
@ -104,6 +137,14 @@ const SafeDetails = (props: Props) => {
</Button> </Button>
</Col> </Col>
</Row> </Row>
<Modal
title="Update Safe"
description="Update Safe"
handleClose={toggleModal}
open={isModalOpen}
>
<UpdateSafeModal onClose={toggleModal} safeAddress={safeAddress} createTransaction={createTransaction} />
</Modal>
</> </>
)} )}
</GnoForm> </GnoForm>

View File

@ -0,0 +1,81 @@
// @flow
import React from 'react'
import IconButton from '@material-ui/core/IconButton'
import Close from '@material-ui/icons/Close'
import { withStyles } from '@material-ui/styles'
import Row from '~/components/layout/Row'
import Paragraph from '~/components/layout/Paragraph'
import Hairline from '~/components/layout/Hairline'
import GnoForm from '~/components/forms/GnoForm'
import Block from '~/components/layout/Block'
import Button from '~/components/layout/Button'
import { styles } from './style'
import { upgradeSafeToLastVersion } from '~/logic/safe/utils/updateSafe'
type Props = {
onClose: Function,
createTransaction: Function,
classes: Object,
safeAddress: string,
}
const UpdateSafeModal = ({
onClose, classes, safeAddress, createTransaction,
}: Props) => {
const handleSubmit = async () => {
// Call the update safe method
await upgradeSafeToLastVersion(safeAddress, createTransaction)
onClose()
}
return (
<>
<Row align="center" grow className={classes.heading}>
<Paragraph className={classes.headingText} weight="bolder" noMargin>
Change required confirmations
</Paragraph>
<IconButton onClick={onClose} disableRipple>
<Close className={classes.close} />
</IconButton>
</Row>
<Hairline />
<GnoForm onSubmit={handleSubmit}>
{() => (
<>
<Block className={classes.modalContent}>
<Row>
<Paragraph>
Update now to take advantage of new features and the highest security standards available.
</Paragraph>
<Block>
This update includes:
<ul>
<li>Compatibility with new asset types (ERC-721 / ERC-1155)</li>
<li>Improved interoperability with modules</li>
<li>Minor security improvements</li>
</ul>
</Block>
<Paragraph>
You will need to confirm this update just like any other transaction. This means other owners will have to confirm the update in case more than one confirmation is required for this Safe.
</Paragraph>
</Row>
</Block>
<Hairline style={{ position: 'absolute', bottom: 85 }} />
<Row align="center" className={classes.buttonRow}>
<Button minWidth={140} onClick={onClose}>
Back
</Button>
<Button type="submit" color="primary" minWidth={140} variant="contained">
Update Safe
</Button>
</Row>
</>
)}
</GnoForm>
</>
)
}
export default withStyles(styles)(UpdateSafeModal)

View File

@ -0,0 +1,46 @@
// @flow
import {
lg, md, sm, secondaryText,
} from '~/theme/variables'
export const styles = () => ({
heading: {
padding: `${sm} ${lg}`,
justifyContent: 'space-between',
boxSizing: 'border-box',
maxHeight: '75px',
},
annotation: {
letterSpacing: '-1px',
color: secondaryText,
marginRight: 'auto',
marginLeft: '20px',
},
headingText: {
fontSize: '20px',
},
close: {
height: '35px',
width: '35px',
},
modalContent: {
padding: `${md} ${lg}`,
},
ownersText: {
marginLeft: sm,
},
buttonRow: {
height: '84px',
justifyContent: 'center',
position: 'absolute',
bottom: 0,
width: '100%',
},
inputRow: {
position: 'relative',
},
errorText: {
position: 'absolute',
bottom: '-25px',
},
})

View File

@ -170,11 +170,7 @@ class Settings extends React.Component<Props, State> {
<Col className={classes.contents} layout="column"> <Col className={classes.contents} layout="column">
<Block className={classes.container}> <Block className={classes.container}>
{menuOptionIndex === 1 && ( {menuOptionIndex === 1 && (
<SafeDetails <SafeDetails safeAddress={safeAddress} safeName={safeName} updateSafe={updateSafe} createTransaction={createTransaction} />
safeAddress={safeAddress}
safeName={safeName}
updateSafe={updateSafe}
/>
)} )}
{menuOptionIndex === 2 && ( {menuOptionIndex === 2 && (
<ManageOwners <ManageOwners

View File

@ -116,35 +116,13 @@ const createTransaction = ({
try { try {
if (isExecution) { if (isExecution) {
tx = await getExecutionTransaction( tx = await getExecutionTransaction(
safeInstance, safeInstance, to, valueInWei, txData, operation, nonce,
to, 0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, from, sigs,
valueInWei,
txData,
operation,
nonce,
0,
0,
0,
ZERO_ADDRESS,
ZERO_ADDRESS,
from,
sigs
) )
} else { } else {
tx = await getApprovalTransaction( tx = await getApprovalTransaction(
safeInstance, safeInstance, to, valueInWei, txData, operation, nonce,
to, 0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, from, sigs,
valueInWei,
txData,
operation,
nonce,
0,
0,
0,
ZERO_ADDRESS,
ZERO_ADDRESS,
from,
sigs
) )
} }

View File

@ -118,8 +118,9 @@ const notificationsMiddleware = (store: Store<GlobalState>) => (
break break
} }
case ADD_SAFE: { case ADD_SAFE: {
const { needUpdate } = await getSafeVersion()
const { safe } = action.payload const { safe } = action.payload
const { needUpdate } = await getSafeVersion(safe.address)
const notificationKey = `${safe.address}` const notificationKey = `${safe.address}`
if (needUpdate) { if (needUpdate) {
dispatch( dispatch(

View File

@ -0,0 +1,37 @@
// @flow
import Web3 from 'web3'
import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json'
import {
DEFAULT_FALLBACK_HANDLER_ADDRESS,
getEncodedMultiSendCallData,
SAFE_MASTER_COPY_ADDRESS,
} from '~/logic/contracts/safeContracts'
import { ZERO_ADDRESS } from '~/logic/wallets/ethAddresses'
describe('Upgrade a Safe', () => {
it('Calls getEncodedMultiSendCallData with a list of MultiSendTransactionInstanceType and returns the multiSend data encoded', async () => {
const safeAddress = ZERO_ADDRESS
const web3 = new Web3(new Web3.providers.HttpProvider(''))
const safeInstance = new web3.eth.Contract(GnosisSafeSol.abi)
const fallbackHandlerTxData = safeInstance.methods.setFallbackHandler(DEFAULT_FALLBACK_HANDLER_ADDRESS).encodeABI()
const updateSafeTxData = safeInstance.methods.changeMasterCopy(SAFE_MASTER_COPY_ADDRESS).encodeABI()
const txs = [
{
operation: 0,
to: safeAddress,
value: 0,
data: fallbackHandlerTxData,
},
{
operation: 0,
to: safeAddress,
value: 0,
data: updateSafeTxData,
},
]
const expectedEncodedData = '0x8d80ff0a000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000f200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024f08a0323000000000000000000000000d5d82b6addc9027b22dca772aa68d5d74cdbdf44000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000247de7edef00000000000000000000000034cfac646f301356faa8b21e94227e3583fe3f5f0000000000000000000000000000'
const multiSendTxData = getEncodedMultiSendCallData(txs, web3)
expect(multiSendTxData).toEqual(expectedEncodedData)
})
})

View File

@ -0,0 +1,32 @@
// @flow
import Web3 from 'web3'
import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json'
import { checkIfSafeNeedsUpdate } from '~/logic/safe/utils/safeVersion'
describe('Check safe version', () => {
it('Calls checkIfSafeNeedUpdate, should return true if the safe version is bellow the target one', async () => {
const web3 = new Web3(new Web3.providers.HttpProvider(''))
const safeInstance = new web3.eth.Contract(GnosisSafeSol.abi)
safeInstance.VERSION = () => '1.0.0'
const targetVersion = '1.1.1'
const { needUpdate } = await checkIfSafeNeedsUpdate(safeInstance, targetVersion)
expect(needUpdate).toEqual(true)
})
it('Calls checkIfSafeNeedUpdate, should return false if the safe version is over the target one', async () => {
const web3 = new Web3(new Web3.providers.HttpProvider(''))
const safeInstance = new web3.eth.Contract(GnosisSafeSol.abi)
safeInstance.VERSION = () => '2.0.0'
const targetVersion = '1.1.1'
const { needUpdate } = await checkIfSafeNeedsUpdate(safeInstance, targetVersion)
expect(needUpdate).toEqual(false)
})
it('Calls checkIfSafeNeedUpdate, should return false if the safe version is equal the target one', async () => {
const web3 = new Web3(new Web3.providers.HttpProvider(''))
const safeInstance = new web3.eth.Contract(GnosisSafeSol.abi)
safeInstance.VERSION = () => '1.1.1'
const targetVersion = '1.1.1'
const { needUpdate } = await checkIfSafeNeedsUpdate(safeInstance, targetVersion)
expect(needUpdate).toEqual(false)
})
})

3786
yarn.lock

File diff suppressed because it is too large Load Diff