From 0958258ff24175392f2f80b2a002a841f4174d35 Mon Sep 17 00:00:00 2001 From: David Disu Date: Wed, 6 Jul 2022 12:08:55 +0100 Subject: [PATCH] Implement upgrade using address input --- apps/remix-ide/src/blockchain/blockchain.js | 19 + .../src/lib/constants/uups.ts | 14 + .../src/lib/openzeppelin-proxy.ts | 29 +- .../run-tab/src/lib/actions/deploy.ts | 7 +- .../run-tab/src/lib/actions/events.ts | 2 +- .../src/lib/components/contractGUI.tsx | 334 ++++++++++-------- .../run-tab/src/lib/reducers/runTab.ts | 2 +- libs/remix-ui/run-tab/src/lib/types/index.ts | 2 +- 8 files changed, 261 insertions(+), 148 deletions(-) diff --git a/apps/remix-ide/src/blockchain/blockchain.js b/apps/remix-ide/src/blockchain/blockchain.js index 3d6e885cec..515819b22d 100644 --- a/apps/remix-ide/src/blockchain/blockchain.js +++ b/apps/remix-ide/src/blockchain/blockchain.js @@ -179,6 +179,25 @@ export class Blockchain extends Plugin { this.runTx(args, confirmationCb, continueCb, promptCb, finalCb) } + async upgradeProxy(proxyAddress, data, newImplementationContractObject) { + const args = { useCall: false, data, to: proxyAddress } + const confirmationCb = (network, tx, gasEstimation, continueTxExecution, cancelCb) => { + // continue using original authorization given by user + continueTxExecution(null) + } + const continueCb = (error, continueTxExecution, cancelCb) => { continueTxExecution() } + const promptCb = (okCb, cancelCb) => { okCb() } + const finalCb = (error, txResult, address, returnValue) => { + if (error) { + const log = logBuilder(error) + + return this.call('terminal', 'logHtml', log) + } + return this.call('udapp', 'resolveContractAndAddInstance', newImplementationContractObject, proxyAddress) + } + this.runTx(args, confirmationCb, continueCb, promptCb, finalCb) + } + async getEncodedFunctionHex (args, funABI) { return new Promise((resolve, reject) => { txFormat.encodeFunctionCall(args, funABI, (error, data) => { diff --git a/libs/remix-core-plugin/src/lib/constants/uups.ts b/libs/remix-core-plugin/src/lib/constants/uups.ts index 2fb521a999..977d3769a8 100644 --- a/libs/remix-core-plugin/src/lib/constants/uups.ts +++ b/libs/remix-core-plugin/src/lib/constants/uups.ts @@ -88,4 +88,18 @@ export const UUPSfunAbi = { type: "constructor", outputs: [], stateMutability: "payable" +} + +export const UUPSupgradeAbi = { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" } \ No newline at end of file diff --git a/libs/remix-core-plugin/src/lib/openzeppelin-proxy.ts b/libs/remix-core-plugin/src/lib/openzeppelin-proxy.ts index 2de757f4d2..9f9e8780d5 100644 --- a/libs/remix-core-plugin/src/lib/openzeppelin-proxy.ts +++ b/libs/remix-core-plugin/src/lib/openzeppelin-proxy.ts @@ -1,12 +1,12 @@ import { Plugin } from '@remixproject/engine'; import { ContractABI, ContractAST, DeployOption } from '../types/contract'; -import { UUPS, UUPSABI, UUPSBytecode, UUPSfunAbi } from './constants/uups'; +import { UUPS, UUPSABI, UUPSBytecode, UUPSfunAbi, UUPSupgradeAbi } from './constants/uups'; const proxyProfile = { name: 'openzeppelin-proxy', displayName: 'openzeppelin-proxy', description: 'openzeppelin-proxy', - methods: ['isConcerned', 'execute', 'getDeployOptions'] + methods: ['isConcerned', 'executeUUPSProxy', 'executeUUPSContractUpgrade', 'getDeployOptions', 'getUpgradeOptions'] }; export class OpenZeppelinProxy extends Plugin { blockchain: any @@ -47,7 +47,7 @@ export class OpenZeppelinProxy extends Plugin { return inputs } - async execute(implAddress: string, args: string | string [] = '', initializeABI, implementationContractObject): Promise { + async executeUUPSProxy(implAddress: string, args: string | string [] = '', initializeABI, implementationContractObject): Promise { // deploy the proxy, or use an existing one if (!initializeABI) throw new Error('Cannot deploy proxy: Missing initialize ABI') args = args === '' ? [] : args @@ -56,6 +56,13 @@ export class OpenZeppelinProxy extends Plugin { if (this.kind === 'UUPS') this.deployUUPSProxy(implAddress, _data, implementationContractObject) } + async executeUUPSContractUpgrade (proxyAddress: string, newImplAddress: string, newImplementationContractObject): Promise { + if (!newImplAddress) throw new Error('Cannot upgrade: Missing implementation address') + if (!proxyAddress) throw new Error('Cannot upgrade: Missing proxy address') + + if (this.kind === 'UUPS') this.upgradeUUPSProxy(proxyAddress, newImplAddress, newImplementationContractObject) + } + async deployUUPSProxy (implAddress: string, _data: string, implementationContractObject): Promise { const args = [implAddress, _data] const constructorData = await this.blockchain.getEncodedParams(args, UUPSfunAbi) @@ -74,4 +81,20 @@ export class OpenZeppelinProxy extends Plugin { implementationContractObject.name = proxyName this.blockchain.deployProxy(data, implementationContractObject) } + + async upgradeUUPSProxy (proxyAddress: string, newImplAddress: string, newImplementationContractObject): Promise { + const fnData = await this.blockchain.getEncodedFunctionHex([newImplAddress], UUPSupgradeAbi) + const proxyName = 'ERC1967Proxy' + const data = { + contractABI: UUPSABI, + contractName: proxyName, + funAbi: UUPSupgradeAbi, + funArgs: [newImplAddress], + linkReferences: {}, + dataHex: fnData.replace('0x', '') + } + // re-use implementation contract's ABI for UI display in udapp and change name to proxy name. + newImplementationContractObject.name = proxyName + this.blockchain.upgradeProxy(proxyAddress, data, newImplementationContractObject) + } } diff --git a/libs/remix-ui/run-tab/src/lib/actions/deploy.ts b/libs/remix-ui/run-tab/src/lib/actions/deploy.ts index ce7f5f9567..25847a562b 100644 --- a/libs/remix-ui/run-tab/src/lib/actions/deploy.ts +++ b/libs/remix-ui/run-tab/src/lib/actions/deploy.ts @@ -135,6 +135,7 @@ export const createInstance = async ( args, deployMode: DeployMode[]) => { const isProxyDeployment = (deployMode || []).find(mode => mode === 'Deploy with Proxy') + const isContractUpgrade = (deployMode || []).find(mode => mode === 'Upgrade Contract') const statusCb = (msg: string) => { const log = logBuilder(msg) @@ -160,7 +161,9 @@ export const createInstance = async ( if (isProxyDeployment) { const initABI = contractObject.abi.find(abi => abi.name === 'initialize') - plugin.call('openzeppelin-proxy', 'execute', addressToString(address), args, initABI, contractObject) + plugin.call('openzeppelin-proxy', 'executeUUPSProxy', addressToString(address), args, initABI, contractObject) + } else if (isContractUpgrade) { + plugin.call('openzeppelin-proxy', 'executeUUPSContractUpgrade', args, addressToString(address), contractObject) } } @@ -192,7 +195,7 @@ export const createInstance = async ( return terminalLogger(plugin, log) })) } - deployContract(plugin, selectedContract, !isProxyDeployment ? args : '', contractMetadata, compilerContracts, { + deployContract(plugin, selectedContract, !isProxyDeployment && !isContractUpgrade ? args : '', contractMetadata, compilerContracts, { continueCb: (error, continueTxExecution, cancelCb) => { continueHandler(dispatch, gasEstimationPrompt, error, continueTxExecution, cancelCb) }, diff --git a/libs/remix-ui/run-tab/src/lib/actions/events.ts b/libs/remix-ui/run-tab/src/lib/actions/events.ts index f8d74dc359..dde29290e9 100644 --- a/libs/remix-ui/run-tab/src/lib/actions/events.ts +++ b/libs/remix-ui/run-tab/src/lib/actions/events.ts @@ -108,7 +108,7 @@ const broadcastCompilationResult = async (plugin: RunTab, dispatch: React.Dispat if (isUpgradeable) { const options = await plugin.call('openzeppelin-proxy', 'getDeployOptions', data.contracts[file]) - dispatch(setDeployOptions({ options: [{ title: 'Deploy with Proxy', active: false }], initializeOptions: options })) + dispatch(setDeployOptions({ options: [{ title: 'Deploy with Proxy', active: false }, { title: 'Upgrade Contract', active: false }], initializeOptions: options })) } else dispatch(setDeployOptions({} as any)) dispatch(fetchContractListSuccess({ [file]: contracts })) diff --git a/libs/remix-ui/run-tab/src/lib/components/contractGUI.tsx b/libs/remix-ui/run-tab/src/lib/components/contractGUI.tsx index f8af09cbd1..8a55cbb830 100644 --- a/libs/remix-ui/run-tab/src/lib/components/contractGUI.tsx +++ b/libs/remix-ui/run-tab/src/lib/components/contractGUI.tsx @@ -3,8 +3,7 @@ import React, { useEffect, useRef, useState } from 'react' import * as remixLib from '@remix-project/remix-lib' import { ContractGUIProps } from '../types' import { CopyToClipboard } from '@remix-ui/clipboard' -import { MultiDeployInput } from './multiDeployInput' -import { DeployInput } from './deployInput' +import { shortenAddress } from '@remix-ui/helper' const txFormat = remixLib.execution.txFormat export function ContractGUI (props: ContractGUIProps) { @@ -15,20 +14,15 @@ export function ContractGUI (props: ContractGUIProps) { title: string, content: string, classList: string, - dataId: string, - widthClass: string - }>({ title: '', content: '', classList: '', dataId: '', widthClass: '' }) - const [selectedDeployIndex, setSelectedDeployIndex] = useState(null) - const [showOptions, setShowOptions] = useState(false) - const [hasArgs, setHasArgs] = useState(false) - const [isMultiField, setIsMultiField] = useState(false) - const [deployInputs, setDeployInputs] = useState<{ - internalType?: string, - name: string, - type: string - }[]>([]) - const [deployPlaceholder, setDeployPlaceholder] = useState('') + dataId: string + }>({ title: '', content: '', classList: '', dataId: '' }) + const [toggleDeployProxy, setToggleDeployProxy] = useState(false) + const [toggleUpgradeImp, setToggleUpgradeImp] = useState(false) + const [deployState, setDeployState] = useState<{ deploy: boolean, upgrade: boolean }>({ deploy: false, upgrade: false }) + const [useLastProxy, setUseLastProxy] = useState(false) + const [proxyAddress, setProxyAddress] = useState('') const multiFields = useRef>([]) + const initializeFields = useRef>([]) const basicInputRef = useRef() useEffect(() => { @@ -41,7 +35,7 @@ export function ContractGUI (props: ContractGUIProps) { } setBasicInput('') // we have the reset the fields before reseting the previous references. - if (basicInputRef.current) basicInputRef.current.value = '' + basicInputRef.current.value = '' multiFields.current.filter((el) => el !== null && el !== undefined).forEach((el) => el.value = '') multiFields.current = [] }, [props.title, props.funcABI]) @@ -53,8 +47,7 @@ export function ContractGUI (props: ContractGUIProps) { title: title + ' - call', content: 'call', classList: 'btn-info', - dataId: title + ' - call', - widthClass: props.widthClass + dataId: title + ' - call' }) } else if (props.funcABI.stateMutability === 'payable' || props.funcABI.payable) { // // transact. stateMutability = payable @@ -62,8 +55,7 @@ export function ContractGUI (props: ContractGUIProps) { title: title + ' - transact (payable)', content: 'transact', classList: 'btn-danger', - dataId: title + ' - transact (payable)', - widthClass: props.widthClass + dataId: title + ' - transact (payable)' }) } else { // // transact. stateMutability = nonpayable @@ -71,59 +63,13 @@ export function ContractGUI (props: ContractGUIProps) { title: title + ' - transact (not payable)', content: 'transact', classList: 'btn-warning', - dataId: title + ' - transact (not payable)', - widthClass: props.widthClass + dataId: title + ' - transact (not payable)' }) } }, [props.lookupOnly, props.funcABI, title]) - useEffect(() => { - if (props.deployOption && props.deployOption[selectedDeployIndex]) { - if (props.deployOption[selectedDeployIndex].title === 'Deploy with Proxy') { - if (props.initializerOptions) { - setDeployInputs(props.initializerOptions.inputs.inputs) - setDeployPlaceholder(props.initializerOptions.initializeInputs) - setHasArgs(true) - if (props.initializerOptions.inputs.inputs.length > 1) setIsMultiField(true) - else setIsMultiField(false) - } else { - setDeployInputs([]) - setDeployPlaceholder('') - setHasArgs(false) - setIsMultiField(false) - } - } else { - if (props.funcABI) { - setDeployInputs(props.funcABI.inputs) - setDeployPlaceholder(props.inputs) - setHasArgs(true) - if (props.funcABI.inputs.length > 1) setIsMultiField(true) - else setIsMultiField(false) - } else { - setDeployInputs([]) - setDeployPlaceholder('') - setHasArgs(false) - setIsMultiField(false) - } - } - } else { - if (props.funcABI) { - setDeployInputs(props.funcABI.inputs) - setDeployPlaceholder(props.inputs) - setHasArgs(true) - if (props.funcABI.inputs.length > 1) setIsMultiField(true) - else setIsMultiField(false) - } else { - setDeployInputs([]) - setDeployPlaceholder('') - setHasArgs(false) - setIsMultiField(false) - } - } - }, [selectedDeployIndex, props.funcABI, props.initializerOptions]) - - const getContentOnCTC = (fields: HTMLInputElement[]) => { - const multiString = getMultiValsString(fields) + const getContentOnCTC = () => { + const multiString = getMultiValsString(multiFields.current) // copy-to-clipboard icon is only visible for method requiring input params if (!multiString) { return 'cannot encode empty arguments' @@ -191,8 +137,7 @@ export function ContractGUI (props: ContractGUIProps) { if (inputString) { inputString = inputString.replace(/(^|,\s+|,)(\d+)(\s+,|,|$)/g, '$1"$2"$3') // replace non quoted number by quoted number inputString = inputString.replace(/(^|,\s+|,)(0[xX][0-9a-fA-F]+)(\s+,|,|$)/g, '$1"$2"$3') // replace non quoted hex string by quoted hex string - inputString = JSON.stringify([inputString]) - const inputJSON = JSON.parse(inputString) + const inputJSON = JSON.parse('[' + inputString + ']') const multiInputs = multiFields.current for (let k = 0; k < multiInputs.length; k++) { @@ -204,9 +149,15 @@ export function ContractGUI (props: ContractGUIProps) { } const handleActionClick = () => { - const deployMode = selectedDeployIndex !== null ? [props.deployOption[selectedDeployIndex].title] : [] + if (deployState.deploy) { + const proxyInitializeString = getMultiValsString(initializeFields.current) - props.clickCallBack(props.funcABI.inputs, basicInput, deployMode) + props.clickCallBack(props.initializerOptions.inputs.inputs, proxyInitializeString, ['Deploy with Proxy']) + } else if (deployState.upgrade) { + props.clickCallBack(props.funcABI.inputs, proxyAddress, ['Upgrade Contract']) + } else { + props.clickCallBack(props.funcABI.inputs, basicInput) + } } const handleBasicInput = (e) => { @@ -215,90 +166,193 @@ export function ContractGUI (props: ContractGUIProps) { setBasicInput(value) } - const handleMultiValsSubmit = (fields: HTMLInputElement[]) => { - const valsString = getMultiValsString(fields) - const deployMode = selectedDeployIndex !== null ? [props.deployOption[selectedDeployIndex].title] : [] + const handleExpandMultiClick = () => { + const valsString = getMultiValsString(multiFields.current) if (valsString) { - props.clickCallBack(props.funcABI.inputs, valsString, deployMode) + props.clickCallBack(props.funcABI.inputs, valsString) } else { - props.clickCallBack(props.funcABI.inputs, '', deployMode) + props.clickCallBack(props.funcABI.inputs, '') } } - const setSelectedDeploy = (index: number) => { - setSelectedDeployIndex(index !== selectedDeployIndex ? index : null) - if (basicInputRef.current) basicInputRef.current.value = '' - setBasicInput('') + const handleToggleDeployProxy = () => { + setToggleDeployProxy(!toggleDeployProxy) + } + + const handleDeployProxySelect = (e) => { + const value = e.target.checked + + if (value) setToggleUpgradeImp(false) + setToggleDeployProxy(value) + setDeployState({ upgrade: false, deploy: value }) + } + + const handleToggleUpgradeImp = () => { + setToggleUpgradeImp(!toggleUpgradeImp) } - const toggleOptions = () => { - setShowOptions(!showOptions) + const handleUpgradeImpSelect = (e) => { + const value = e.target.checked + + setToggleUpgradeImp(value) + if (value) setToggleDeployProxy(false) + setDeployState({ deploy: false, upgrade: value }) + } + + const handleUseLastProxySelect = (e) => { + const value = e.target.checked + + setUseLastProxy(value) + setProxyAddress('') + } + + const handleSetProxyAddress = (e) => { + const value = e.target.value + + setProxyAddress(value) } return ( -
- { - props.isDeploy ? !isMultiField ? - : : - <> -
- - 0) || (props.funcABI.type === 'fallback') || (props.funcABI.type === 'receive')) ? 'hidden' : 'visible' }} /> - 0) ? 'hidden' : 'visible' }}> +
0) || (props.funcABI.type === 'fallback') || (props.funcABI.type === 'receive') ? 'udapp_hasArgs' : ''}`}> +
+ + 0) || (props.funcABI.type === 'fallback') || (props.funcABI.type === 'receive')) ? 'hidden' : 'visible' }} /> + 0) ? 'hidden' : 'visible' }}> +
+
+
+
+
{title}
+ +
+
+ {props.funcABI.inputs.map((inp, index) => { + return ( +
+ + { multiFields.current[index] = el }} className="form-control" placeholder={inp.type} title={inp.name} data-id={`multiParamManagerInput${inp.name}`} /> +
) + })} +
+
+ + +
-
-
-
-
{title}
- +
+ { props.deployOption && (props.deployOption || []).length > 0 ? + <> +
+
+ +
- {props.funcABI.inputs.map((inp, index) => { - return ( -
- - { multiFields.current[index] = el }} className="form-control" placeholder={inp.type} title={inp.name} data-id={`multiParamManagerInput${inp.name}`} /> -
) - })} + { + props.initializerOptions && props.initializerOptions.initializeInputs ? + + + : null + }
-
- getContentOnCTC(multiFields.current)} /> - +
+ { + props.initializerOptions && props.initializerOptions.initializeInputs ? +
+
{ + props.initializerOptions.inputs.inputs.map((inp, index) => { + return ( +
+ + { initializeFields.current[index] = el }} style={{ height: 32 }} className="form-control udapp_input" placeholder={inp.type} title={inp.name} /> +
+ )}) + } +
+
: null + } +
+
+ +
+ + +
-
- - } +
+
+
+ + +
+ { + !useLastProxy ? +
+ + +
: + { shortenAddress(proxyAddress) || 'No proxy address available' } + } +
+
+ : null + }
) } diff --git a/libs/remix-ui/run-tab/src/lib/reducers/runTab.ts b/libs/remix-ui/run-tab/src/lib/reducers/runTab.ts index 2a0ff1c855..aadf532380 100644 --- a/libs/remix-ui/run-tab/src/lib/reducers/runTab.ts +++ b/libs/remix-ui/run-tab/src/lib/reducers/runTab.ts @@ -1,6 +1,6 @@ import { CompilerAbstract } from '@remix-project/remix-solidity-ts' import { ContractData } from '@remix-project/core-plugin' -import { DeployMode, DeployOption, DeployOptions } from '../types' +import { DeployMode, DeployOptions } from '../types' import { ADD_DEPLOY_OPTION, ADD_INSTANCE, ADD_PROVIDER, CLEAR_INSTANCES, CLEAR_RECORDER_COUNT, DISPLAY_NOTIFICATION, DISPLAY_POPUP_MESSAGE, FETCH_ACCOUNTS_LIST_FAILED, FETCH_ACCOUNTS_LIST_REQUEST, FETCH_ACCOUNTS_LIST_SUCCESS, FETCH_CONTRACT_LIST_FAILED, FETCH_CONTRACT_LIST_REQUEST, FETCH_CONTRACT_LIST_SUCCESS, FETCH_PROVIDER_LIST_FAILED, FETCH_PROVIDER_LIST_REQUEST, FETCH_PROVIDER_LIST_SUCCESS, HIDE_NOTIFICATION, HIDE_POPUP_MESSAGE, REMOVE_DEPLOY_OPTION, REMOVE_INSTANCE, REMOVE_PROVIDER, RESET_STATE, SET_BASE_FEE_PER_GAS, SET_CONFIRM_SETTINGS, SET_CURRENT_CONTRACT, SET_CURRENT_FILE, SET_DECODED_RESPONSE, SET_DEPLOY_OPTIONS, SET_EXECUTION_ENVIRONMENT, SET_EXTERNAL_WEB3_ENDPOINT, SET_GAS_LIMIT, SET_GAS_PRICE, SET_GAS_PRICE_STATUS, SET_IPFS_CHECKED_STATE, SET_LOAD_TYPE, SET_MATCH_PASSPHRASE, SET_MAX_FEE, SET_MAX_PRIORITY_FEE, SET_NETWORK_NAME, SET_PASSPHRASE, SET_PATH_TO_SCENARIO, SET_PERSONAL_MODE, SET_RECORDER_COUNT, SET_SELECTED_ACCOUNT, SET_SEND_UNIT, SET_SEND_VALUE, SET_TX_FEE_CONTENT } from '../constants' import Web3 from 'web3' diff --git a/libs/remix-ui/run-tab/src/lib/types/index.ts b/libs/remix-ui/run-tab/src/lib/types/index.ts index c304d8815f..0137294bda 100644 --- a/libs/remix-ui/run-tab/src/lib/types/index.ts +++ b/libs/remix-ui/run-tab/src/lib/types/index.ts @@ -219,7 +219,7 @@ export interface Modal { cancelFn: () => void } -export type DeployMode = 'Deploy with Proxy' | 'Upgrade Proxy' +export type DeployMode = 'Deploy with Proxy' | 'Upgrade Contract' export type DeployOption = { initializeInputs: string,