diff --git a/apps/remix-ide-e2e/src/tests/transactionExecution.spec.ts b/apps/remix-ide-e2e/src/tests/transactionExecution.spec.ts index 17fb82c644..be8c0b936e 100644 --- a/apps/remix-ide-e2e/src/tests/transactionExecution.spec.ts +++ b/apps/remix-ide-e2e/src/tests/transactionExecution.spec.ts @@ -137,6 +137,21 @@ module.exports = { .selectAccount('0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c') // this account will be used for this test suite .click('#runTabView button[class^="instanceButton"]') .waitForElementPresent('.instance:nth-of-type(2)') + }, + + 'Should Compile and Deploy a contract which define a custom error, the error should be logged in the terminal': function (browser: NightwatchBrowser) { + browser.testContracts('customError.sol', sources[4]['customError.sol'], ['C']) + .clickLaunchIcon('udapp') + .selectAccount('0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c') // this account will be used for this test suite + .click('#runTabView button[class^="instanceButton"]') + .waitForElementPresent('.instance:nth-of-type(3)') + .click('.instance:nth-of-type(3) > div > button') + .clickFunction('g - transact (not payable)') + .journalLastChildIncludes('Error provided by the contract:') + .journalLastChildIncludes('CustomError') + .journalLastChildIncludes('Parameters:') + .journalLastChildIncludes('2,3,error_string_2') + .journalLastChildIncludes('Debug the transaction to get more information.') .end() } } @@ -218,5 +233,23 @@ contract C { event Test(function() external); }` } + }, + // https://github.com/ethereum/remix-project/issues/1152 + { + 'customError.sol': { + content: `// SPDX-License-Identifier: GPL-3.0 + + pragma solidity ^0.8.4; + + error CustomError(uint a, uint b, string c); + contract C { + function f() public pure { + revert CustomError(2, 3, "error_string"); + } + function g() public { + revert CustomError(2, 3, "error_string_2"); + } + }` + } } ] diff --git a/apps/remix-ide/src/app/udapp/run-tab.js b/apps/remix-ide/src/app/udapp/run-tab.js index cc0d2f6ab6..b966287690 100644 --- a/apps/remix-ide/src/app/udapp/run-tab.js +++ b/apps/remix-ide/src/app/udapp/run-tab.js @@ -41,7 +41,7 @@ export class RunTab extends ViewPlugin { this.blockchain = blockchain this.fileManager = fileManager this.editor = editor - this.logCallback = (msg) => { mainView.getTerminal().logHtml(msg) } + this.logCallback = (msg) => { mainView.getTerminal().logHtml(yo`
${msg}`) } this.filePanel = filePanel this.compilersArtefacts = compilersArtefacts this.networkModule = networkModule diff --git a/apps/remix-ide/src/app/ui/universal-dapp-ui.js b/apps/remix-ide/src/app/ui/universal-dapp-ui.js index 0af660c010..17fe7058c8 100644 --- a/apps/remix-ide/src/app/ui/universal-dapp-ui.js +++ b/apps/remix-ide/src/app/ui/universal-dapp-ui.js @@ -253,7 +253,7 @@ UniversalDAppUI.prototype.runTransaction = function (lookupOnly, args, valArr, i const params = args.funABI.type !== 'fallback' ? inputsValues : '' this.blockchain.runOrCallContractMethod( args.contractName, - args.contractAbi, + args.contractABI, args.funABI, inputsValues, args.address, diff --git a/apps/remix-ide/src/blockchain/blockchain.js b/apps/remix-ide/src/blockchain/blockchain.js index 27f9be195d..a5afab15c5 100644 --- a/apps/remix-ide/src/blockchain/blockchain.js +++ b/apps/remix-ide/src/blockchain/blockchain.js @@ -262,6 +262,10 @@ class Blockchain { } if (funABI.type === 'fallback') data.dataHex = value + if (data) { + data.contractName = contractName + data.contractABI = contractAbi + } const useCall = funABI.stateMutability === 'view' || funABI.stateMutability === 'pure' this.runTx({ to: address, data, useCall }, confirmationCb, continueCb, promptCb, (error, txResult, _address, returnValue) => { if (error) { @@ -477,7 +481,7 @@ class Blockchain { if (execResult) { // if it's not the VM, we don't have return value. We only have the transaction, and it does not contain the return value. returnValue = (execResult && isVM) ? execResult.returnValue : txResult - const vmError = txExecution.checkVMError(execResult) + const vmError = txExecution.checkVMError(execResult, args.data.contractABI) if (vmError.error) { return cb(vmError.message) } diff --git a/libs/remix-lib/src/execution/txExecution.ts b/libs/remix-lib/src/execution/txExecution.ts index 8eb1c927dd..639151c781 100644 --- a/libs/remix-lib/src/execution/txExecution.ts +++ b/libs/remix-lib/src/execution/txExecution.ts @@ -1,5 +1,6 @@ 'use strict' import { ethers } from 'ethers' +import { getFunctionFragment } from './txHelper' /** * deploy the given contract @@ -56,7 +57,7 @@ export function callFunction (from, to, data, value, gasLimit, funAbi, txRunner, * @param {Object} execResult - execution result given by the VM * @return {Object} - { error: true/false, message: DOMNode } */ -export function checkVMError (execResult) { +export function checkVMError (execResult, abi) { const errorCode = { OUT_OF_GAS: 'out of gas', STACK_UNDERFLOW: 'stack underflow', @@ -88,19 +89,48 @@ export function checkVMError (execResult) { ret.error = true } else if (exceptionError === errorCode.REVERT) { const returnData = execResult.returnValue - // It is the hash of Error(string) - if (returnData && (returnData.slice(0, 4).toString('hex') === '08c379a0')) { - const abiCoder = new ethers.utils.AbiCoder() - const reason = abiCoder.decode(['string'], returnData.slice(4))[0] - msg = `\tThe transaction has been reverted to the initial state.\nReason provided by the contract: "${reason}".` - } else { - msg = '\tThe transaction has been reverted to the initial state.\nNote: The called function should be payable if you send value and the value you send should be less than your current balance.' + const returnDataHex = returnData.slice(0, 4).toString('hex') + let customError + if (abi) { + let decodedCustomErrorInputs + for (const item of abi) { + if (item.type === 'error') { + // ethers doesn't crash anymore if "error" type is specified, but it doesn't extract the errors. see: + // https://github.com/ethers-io/ethers.js/commit/bd05aed070ac9e1421a3e2bff2ceea150bedf9b7 + // we need here to fake the type, so the "getSighash" function works properly + const fn = getFunctionFragment({ ...item, type: 'function', stateMutability: 'nonpayable' }) + if (!fn) continue + const sign = fn.getSighash(item.name) + if (!sign) continue + if (returnDataHex === sign.replace('0x', '')) { + customError = item.name + decodedCustomErrorInputs = fn.decodeFunctionData(fn.getFunction(item.name), returnData) + break + } + } + } + if (decodedCustomErrorInputs) { + msg = '\tThe transaction has been reverted to the initial state.\nError provided by the contract:' + msg += `\n${customError}` + msg += '\nParameters:' + msg += `\n${decodedCustomErrorInputs}` + } + } + if (!customError) { + // It is the hash of Error(string) + if (returnData && (returnDataHex === '08c379a0')) { + const abiCoder = new ethers.utils.AbiCoder() + const reason = abiCoder.decode(['string'], returnData.slice(4))[0] + msg = `\tThe transaction has been reverted to the initial state.\nReason provided by the contract: "${reason}".` + } else { + msg = '\tThe transaction has been reverted to the initial state.\nNote: The called function should be payable if you send value and the value you send should be less than your current balance.' + } } ret.error = true } else if (exceptionError === errorCode.STATIC_STATE_CHANGE) { msg = '\tState changes is not allowed in Static Call context\n' ret.error = true } - ret.message = `${error}${exceptionError}${msg}\tDebug the transaction to get more information.` + ret.message = `${error}\n${exceptionError}\n${msg}\nDebug the transaction to get more information.` return ret } diff --git a/libs/remix-lib/src/execution/txHelper.ts b/libs/remix-lib/src/execution/txHelper.ts index 1ee42e14bb..099e57868b 100644 --- a/libs/remix-lib/src/execution/txHelper.ts +++ b/libs/remix-lib/src/execution/txHelper.ts @@ -38,6 +38,11 @@ export function encodeFunctionId (funABI) { return abi.getSighash(funABI.name) } +export function getFunctionFragment (funABI): ethers.utils.Interface { + if (funABI.type === 'fallback' || funABI.type === 'receive') return null + return new ethers.utils.Interface([funABI]) +} + export function sortAbiFunction (contractabi) { // Check if function is constant (introduced with Solidity 0.6.0) const isConstant = ({ stateMutability }) => stateMutability === 'view' || stateMutability === 'pure' diff --git a/libs/remix-url-resolver/tests/test.ts b/libs/remix-url-resolver/tests/test.ts index 37d9410bf6..f47144e6be 100644 --- a/libs/remix-url-resolver/tests/test.ts +++ b/libs/remix-url-resolver/tests/test.ts @@ -59,7 +59,7 @@ describe('testRunner', () => { // Test github import describe('test getting github imports', () => { const urlResolver = new RemixURLResolver() - const fileName: string = 'github.com/MathCody/solidity-examples/greeter/greeter.sol' + const fileName: string = 'github.com/ethential/solidity-examples/solidity-features-check/greeter.sol' let results: object = {} before(done => { @@ -78,8 +78,8 @@ describe('testRunner', () => { }) it('should return contract content of given github path', () => { const expt: object = { - cleanUrl: 'MathCody/solidity-examples/greeter/greeter.sol', - content: 'pragma solidity >=0.5.0 <0.6.0;\nimport \"../mortal/mortal.sol\";\n\ncontract Greeter is Mortal {\n /* Define variable greeting of the type string */\n string greeting;\n\n /* This runs when the contract is executed */\n constructor(string memory _greeting) public {\n greeting = _greeting;\n }\n\n /* Main function */\n function greet() public view returns (string memory) {\n return greeting;\n }\n}', + cleanUrl: 'ethential/solidity-examples/solidity-features-check/greeter.sol', + content: 'pragma solidity >=0.7.0;\nimport \"./mortal.sol\";\n// SPDX-License-Identifier: GPL-3.0\n\ncontract Greeter is Mortal {\n /* Define variable greeting of the type string */\n string greeting;\n\n /* This runs when the contract is executed */\n constructor(string memory _greeting) {\n greeting = _greeting;\n }\n\n /* Main function */\n function greet() public view returns (string memory) {\n return greeting;\n }\n}\n\n// 0x37aA58B2cE3Bb9576EEBCD51315070eA8806b7c4\n', type: 'github' } assert.deepEqual(results, expt) @@ -268,6 +268,7 @@ describe('testRunner', () => { }) // Test SWARM imports + /* describe('test getting SWARM imports', () => { const urlResolver = new RemixURLResolver() const fileName = 'bzz-raw://a728627437140f2b0b46c1bcfb0de2126d18b40e9b61c3e31bd96abebf714619' @@ -297,6 +298,7 @@ describe('testRunner', () => { assert.deepEqual(results, expt) }) }) + */ }) }) })