remix-project mirror
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
remix-project/libs/remix-simulator/src/VmProxy.ts

454 lines
16 KiB

import { util } from '@remix-project/remix-lib'
const { toHexPaddedString, formatMemory } = util
import { helpers } from '@remix-project/remix-lib'
const { normalizeHexAddress } = helpers.ui
import { ConsoleLogs, hash } from '@remix-project/remix-lib'
import BN from 'bn.js'
import { isBigNumber } from 'web3-utils'
import { toChecksumAddress, bufferToHex, Address, toBuffer } from '@ethereumjs/util'
import utils from 'web3-utils'
import { ethers } from 'ethers'
import { VMContext } from './vm-context'
import type { StateManager } from '@ethereumjs/statemanager'
import type { InterpreterStep } from '@ethereumjs/evm/dist/interpreter'
import type { AfterTxEvent, VM } from '@ethereumjs/vm'
import type { TypedTransaction } from '@ethereumjs/tx'
export class VmProxy {
vmContext: VMContext
vm: VM
vmTraces
txs
txsReceipt
hhLogs
processingHash
processingAddress
processingIndex
previousDepth
incr
eth
debug
providers
currentProvider
storageCache
sha3Preimages
sha3
toHex
toAscii
fromAscii
fromDecimal
fromWei
toWei
toBigNumber
isAddress
utils
txsMapBlock
blocks
stateCopy: StateManager
flagDoNotRecordEVMSteps: boolean
lastMemoryUpdate: Array<string>
constructor (vmContext: VMContext) {
this.vmContext = vmContext
this.stateCopy
this.vm = null
this.vmTraces = {}
this.txs = {}
this.txsReceipt = {}
this.hhLogs = {}
this.processingHash = null
this.processingAddress = null
this.processingIndex = null
this.previousDepth = 0
this.incr = 0
this.eth = {}
this.debug = {}
this.eth.getCode = (address, cb) => this.getCode(address, cb)
this.eth.getTransaction = (txHash, cb) => this.getTransaction(txHash, cb)
this.eth.getTransactionReceipt = (txHash, cb) => this.getTransactionReceipt(txHash, cb)
this.eth.getTransactionFromBlock = (blockNumber, txIndex, cb) => this.getTransactionFromBlock(blockNumber, txIndex, cb)
this.eth.getBlockNumber = (cb) => this.getBlockNumber(cb)
this.eth.getStorageAt = (address: string, position: string, blockNumber: string, cb) => this.getStorageAt(address, position, blockNumber, cb)
this.debug.traceTransaction = (txHash, options, cb) => this.traceTransaction(txHash, options, cb)
this.debug.storageRangeAt = (blockNumber, txIndex, address, start, maxLength, cb) => this.storageRangeAt(blockNumber, txIndex, address, start, maxLength, cb)
this.debug.preimage = (hashedKey, cb) => this.preimage(hashedKey, cb)
this.providers = { HttpProvider: function (url) {} }
this.currentProvider = { host: 'vm provider' }
this.storageCache = {}
this.sha3Preimages = {}
// util
this.sha3 = (...args) => utils.sha3.apply(this, args)
this.toHex = (...args) => utils.toHex.apply(this, args)
this.toAscii = (...args) => utils.toAscii.apply(this, args)
this.fromAscii = (...args) => utils.fromAscii.apply(this, args)
this.fromDecimal = (...args) => utils.fromDecimal.apply(this, args)
this.fromWei = (...args) => utils.fromWei.apply(this, args)
this.toWei = (...args) => utils.toWei.apply(this, args)
this.toBigNumber = (...args) => utils.toBN.apply(this, args)
this.isAddress = (...args) => utils.isAddress.apply(this, args)
this.utils = utils
this.txsMapBlock = {}
this.blocks = {}
this.lastMemoryUpdate = []
}
setVM (vm) {
if (this.vm === vm) return
this.vm = vm
this.vm.evm.events.on('step', async (data: InterpreterStep) => {
await this.pushTrace(data)
})
this.vm.events.on('afterTx', async (data: AfterTxEvent, resolve: (result?: any) => void) => {
await this.txProcessed(data)
resolve()
})
this.vm.events.on('beforeTx', async (data: TypedTransaction, resolve: (result?: any) => void) => {
await this.txWillProcess(data)
resolve()
})
}
releaseCurrentHash () {
const ret = this.processingHash
this.processingHash = undefined
return ret
}
flagNextAsDoNotRecordEvmSteps () {
this.flagDoNotRecordEVMSteps = true
}
async txWillProcess (data: TypedTransaction) {
if (this.flagDoNotRecordEVMSteps) return
this.lastMemoryUpdate = []
this.stateCopy = await this.vm.stateManager.copy()
this.incr++
this.processingHash = bufferToHex(data.hash())
this.vmTraces[this.processingHash] = {
gas: '0x0',
return: '0x0',
structLogs: []
}
const tx = {}
tx['hash'] = this.processingHash
tx['from'] = toChecksumAddress(data.getSenderAddress().toString())
if (data.to) {
tx['to'] = toChecksumAddress(data.to.toString())
}
this.processingAddress = tx['to']
tx['input'] = bufferToHex(data.data)
tx['gas'] = data.gasLimit.toString(10)
if (data.value) {
tx['value'] = data.value.toString(10)
}
this.txs[this.processingHash] = tx
this.txsReceipt[this.processingHash] = tx
this.storageCache[this.processingHash] = {}
this.storageCache['after_' + this.processingHash] = {}
if (data.to) {
(async (processingHash, processingAccount, processingAddress, self) => {
try {
const storage = await self.stateCopy.dumpStorage(processingAccount)
self.storageCache[processingHash][processingAddress] = storage
} catch (e) {
console.log(e)
}
})(this.processingHash, data.to, tx['to'], this)
}
this.processingIndex = 0
}
async txProcessed (data: AfterTxEvent) {
if (this.flagDoNotRecordEVMSteps) {
this.flagDoNotRecordEVMSteps = false
return
}
const lastOp = this.vmTraces[this.processingHash].structLogs[this.processingIndex - 1]
if (lastOp) {
lastOp.error = lastOp.op !== 'RETURN' && lastOp.op !== 'STOP' && lastOp.op !== 'DESTRUCT'
}
const gasUsed = '0x' + data.totalGasSpent.toString(16)
this.vmTraces[this.processingHash].gas = gasUsed
this.txsReceipt[this.processingHash].gasUsed = gasUsed
const logs = []
for (const l in data.execResult.logs) {
const log = data.execResult.logs[l]
const topics = []
if (log[1].length > 0) {
for (const k in log[1]) {
topics.push('0x' + log[1][k].toString('hex'))
}
} else {
topics.push('0x')
}
logs.push({
address: '0x' + log[0].toString('hex'),
data: '0x' + log[2].toString('hex'),
topics: topics,
rawVMResponse: log
})
}
this.txsReceipt[this.processingHash].logs = logs
this.txsReceipt[this.processingHash].transactionHash = this.processingHash
const status = data.execResult.exceptionError ? 0 : 1
this.txsReceipt[this.processingHash].status = `0x${status}`
const to = this.txs[this.processingHash].to
if (to) {
try {
await (async (processingHash, processingAddress, self) => {
try {
const account = Address.fromString(processingAddress)
const storage = await self.vm.stateManager.dumpStorage(account)
self.storageCache['after_' + processingHash][processingAddress] = storage
} catch (e) {
console.log(e)
}
})(this.processingHash, to, this)
} catch (e) {
console.log(e)
}
}
if (data.createdAddress) {
const address = data.createdAddress.toString()
const checksumedAddress = toChecksumAddress(address)
this.vmTraces[this.processingHash].return = checksumedAddress
this.txsReceipt[this.processingHash].contractAddress = checksumedAddress
} else if (data.execResult.returnValue) {
this.vmTraces[this.processingHash].return = '0x' + data.execResult.returnValue.toString('hex')
} else {
this.vmTraces[this.processingHash].return = '0x'
}
this.processingIndex = null
this.processingAddress = null
this.previousDepth = 0
this.stateCopy = null
}
async pushTrace (data: InterpreterStep) {
if (this.flagDoNotRecordEVMSteps) return
try {
const depth = data.depth + 1 // geth starts the depth from 1
if (!this.processingHash) {
return
}
let previousOpcode
if (this.vmTraces[this.processingHash] && this.vmTraces[this.processingHash].structLogs[this.processingIndex - 1]) {
previousOpcode = this.vmTraces[this.processingHash].structLogs[this.processingIndex - 1]
}
if (this.previousDepth > depth && previousOpcode) {
// returning from context, set error it is not STOP, RETURN
previousOpcode.invalidDepthChange = previousOpcode.op !== 'RETURN' && previousOpcode.op !== 'STOP'
}
const step = {
stack: { ...data.stack },
storage: {},
memory: null,
op: data.opcode.name,
pc: data.pc,
gasCost: data.opcode.fee.toString(),
gas: data.gasLeft.toString(),
depth: depth
}
step.stack.length = Object.keys(data.stack).length
if (previousOpcode && (previousOpcode.op === 'CALLDATACOPY' || previousOpcode.op === 'CODECOPY' || previousOpcode.op === 'EXTCODECOPY' || previousOpcode.op === 'RETURNDATACOPY' || previousOpcode.op === 'MSTORE' || previousOpcode.op === 'MSTORE8')) {
step.memory = new Uint8Array(data.memory)
this.lastMemoryUpdate = step.memory
}
this.vmTraces[this.processingHash].structLogs.push(step)
// Track hardhat console.log call
if (step.op === 'STATICCALL' && toHexPaddedString(step.stack[step.stack.length - 2]) === '0x000000000000000000000000000000000000000000636f6e736f6c652e6c6f67') {
const payloadStart = parseInt(toHexPaddedString(step.stack[step.stack.length - 3]), 16)
const memory = formatMemory(data.memory)
const memoryStr = memory.join('')
let payload = memoryStr.substring(payloadStart * 2, memoryStr.length)
const fnselectorStr = payload.substring(0, 8)
const fnselectorStrInHex = '0x' + fnselectorStr
const fnselector = parseInt(fnselectorStrInHex)
const fnArgs = ConsoleLogs[fnselector]
const iface = new ethers.utils.Interface([`function log${fnArgs} view`])
const functionDesc = iface.getFunction(`log${fnArgs}`)
const sigHash = iface.getSighash(`log${fnArgs}`)
if (fnArgs.includes('uint') && sigHash !== fnselectorStrInHex) {
payload = payload.replace(fnselectorStr, sigHash)
} else {
payload = '0x' + payload
}
let consoleArgs = iface.decodeFunctionData(functionDesc, payload)
consoleArgs = consoleArgs.map((value) => {
if (isBigNumber(value)) {
return value.toString()
}
return value
})
this.hhLogs[this.processingHash] = this.hhLogs[this.processingHash] ? this.hhLogs[this.processingHash] : []
this.hhLogs[this.processingHash].push(consoleArgs)
}
if (step.op === 'CREATE' || step.op === 'CALL') {
if (step.op === 'CREATE') {
this.processingAddress = '(Contract Creation - Step ' + this.processingIndex + ')'
this.storageCache[this.processingHash][this.processingAddress] = {}
} else {
this.processingAddress = normalizeHexAddress(toHexPaddedString(step.stack[step.stack.length - 2]))
this.processingAddress = toChecksumAddress(this.processingAddress)
if (!this.storageCache[this.processingHash][this.processingAddress]) {
(async (processingHash, processingAddress, self) => {
try {
const account = Address.fromString(processingAddress)
const storage = await self.stateCopy.dumpStorage(account)
self.storageCache[processingHash][processingAddress] = storage
} catch (e) {
console.log(e)
}
})(this.processingHash, this.processingAddress, this)
}
}
}
if (previousOpcode && previousOpcode.op === 'SHA3') {
const preimage = this.getSha3Input(previousOpcode.stack, formatMemory(this.lastMemoryUpdate))
const imageHash = toHexPaddedString(step.stack[step.stack.length - 1]).replace('0x', '')
this.sha3Preimages[imageHash] = {
preimage: preimage
}
}
this.processingIndex++
this.previousDepth = depth
} catch (e) {
console.log(e)
}
}
getCode (address, cb) {
address = toChecksumAddress(address)
this.vm.stateManager.getContractCode(Address.fromString(address)).then((result) => {
cb(null, bufferToHex(result))
}).catch((error) => {
cb(error)
})
}
setProvider (provider) {}
traceTransaction (txHash, options, cb) {
if (this.vmTraces[txHash]) {
if (cb) {
cb(null, this.vmTraces[txHash])
}
return this.vmTraces[txHash]
}
if (cb) {
cb('unable to retrieve traces ' + txHash, null)
}
}
getStorageAt (address: string, position: string, blockNumber: string, cb) {
// we don't use the range params here
address = toChecksumAddress(address)
blockNumber = blockNumber === 'latest' ? this.vmContext.latestBlockNumber : blockNumber
const block = this.vmContext.blocks[blockNumber]
const txHash = '0x' + block.transactions[block.transactions.length - 1].hash().toString('hex')
if (this.storageCache['after_' + txHash] && this.storageCache['after_' + txHash][address]) {
const slot = '0x' + hash.keccak(toBuffer(ethers.utils.hexZeroPad(position, 32))).toString('hex')
const storage = this.storageCache['after_' + txHash][address]
return cb(null, storage[slot].value)
}
// Before https://github.com/ethereum/remix-project/pull/1703, it used to throw error as
// 'unable to retrieve storage ' + txIndex + ' ' + address
cb(null, '0x0')
}
storageRangeAt (blockNumber, txIndex, address, start, maxLength, cb) {
// we don't use the range params here
address = toChecksumAddress(address)
const block = this.vmContext.blocks[blockNumber]
const txHash = '0x' + block.transactions[txIndex].hash().toString('hex')
if (this.storageCache[txHash] && this.storageCache[txHash][address]) {
const storage = this.storageCache[txHash][address]
return cb(null, {
storage: JSON.parse(JSON.stringify(storage)),
nextKey: null
})
}
// Before https://github.com/ethereum/remix-project/pull/1703, it used to throw error as
// 'unable to retrieve storage ' + txIndex + ' ' + address
cb(null, { storage: {} })
}
getBlockNumber (cb) { cb(null, 'vm provider') }
getTransaction (txHash, cb) {
if (this.txs[txHash]) {
if (cb) {
cb(null, this.txs[txHash])
}
return this.txs[txHash]
}
if (cb) {
cb('unable to retrieve tx ' + txHash, null)
}
}
getTransactionReceipt (txHash, cb) {
// same as getTransaction but return the created address also
if (this.txsReceipt[txHash]) {
if (cb) {
cb(null, this.txsReceipt[txHash])
}
return this.txsReceipt[txHash]
}
if (cb) {
cb('unable to retrieve txReceipt ' + txHash, null)
}
}
getTransactionFromBlock (blockNumber, txIndex, cb) {
const mes = 'not supposed to be needed by remix in vmmode'
console.log(mes)
if (cb) {
cb(mes, null)
}
}
preimage (hashedKey, cb) {
hashedKey = hashedKey.replace('0x', '')
cb(null, this.sha3Preimages[hashedKey] !== undefined ? this.sha3Preimages[hashedKey].preimage : null)
}
getSha3Input (stack, memory) {
const memoryStart = toHexPaddedString(stack[stack.length - 1])
const memoryLength = toHexPaddedString(stack[stack.length - 2])
const memStartDec = (new BN(memoryStart.replace('0x', ''), 16)).toString(10)
const memoryStartInt = parseInt(memStartDec) * 2
const memLengthDec = (new BN(memoryLength.replace('0x', ''), 16).toString(10))
const memoryLengthInt = parseInt(memLengthDec) * 2
let i = Math.floor(memoryStartInt / 32)
const maxIndex = Math.floor(memoryLengthInt / 32) + i
if (!memory[i]) {
return this.emptyFill(memoryLength)
}
let sha3Input = memory[i].slice(memoryStartInt - 32 * i)
i++
while (i < maxIndex) {
sha3Input += memory[i] ? memory[i] : this.emptyFill(32)
i++
}
if (sha3Input.length < memoryLength) {
const leftSize = memoryLengthInt - sha3Input.length
sha3Input += memory[i] ? memory[i].slice(0, leftSize) : this.emptyFill(leftSize)
}
return sha3Input
}
emptyFill (size) {
return (new Array(size)).join('0')
}
}