diff --git a/lib/evm/eei.ts b/lib/evm/eei.ts index 991d306469..f2d429626d 100644 --- a/lib/evm/eei.ts +++ b/lib/evm/eei.ts @@ -93,6 +93,18 @@ export default class EEI { this._result.gasRefund.iadd(amount) } + /** + * Reduces amount of gas to be refunded by a positive value. + * @param amount - Amount to subtract from gas refunds + */ + subRefund(amount: BN): void { + this._result.gasRefund.isub(amount) + if (this._result.gasRefund.ltn(0)) { + this._result.gasRefund = new BN(0) + trap(ERROR.REFUND_EXHAUSTED) + } + } + /** * Returns address of currently executing account. */ diff --git a/lib/evm/opFns.ts b/lib/evm/opFns.ts index 9c7901b748..5bdb839b0c 100644 --- a/lib/evm/opFns.ts +++ b/lib/evm/opFns.ts @@ -912,7 +912,10 @@ function getContractStorage(runState: RunState, address: Buffer, key: Buffer) { } runState.stateManager.getContractStorage(address, key, (err: Error, current: Buffer) => { if (err) return cb(err, null) - if (runState._common.hardfork() === 'constantinople') { + if ( + runState._common.hardfork() === 'constantinople' || + runState._common.gteHardfork('istanbul') + ) { runState.stateManager.getOriginalContractStorage( address, key, @@ -956,9 +959,8 @@ function updateSstoreGas(runState: RunState, found: any, value: Buffer) { // If original value is not 0 if (current.length === 0) { // If current value is 0 (also means that new value is not 0), remove 15000 gas from refund counter. We can prove that refund counter will never go below 0. - // TODO: Remove usage of private attr - runState.eei._result.gasRefund = runState.eei._result.gasRefund.subn( - runState._common.param('gasPrices', 'netSstoreClearRefund'), + runState.eei._result.gasRefund.isub( + new BN(runState._common.param('gasPrices', 'netSstoreClearRefund')), ) } else if (value.length === 0) { // If new value is 0 (also means that current value is not 0), add 15000 gas to refund counter. @@ -978,6 +980,69 @@ function updateSstoreGas(runState: RunState, found: any, value: Buffer) { } } return runState.eei.useGas(new BN(runState._common.param('gasPrices', 'netSstoreDirtyGas'))) + } else if (runState._common.gteHardfork('istanbul')) { + // EIP-2200 + const original = found.original + const current = found.current + // Fail if not enough gas is left + if ( + runState.eei.getGasLeft().lten(runState._common.param('gasPrices', 'sstoreSentryGasEIP2200')) + ) { + trap(ERROR.OUT_OF_GAS) + } + + // Noop + if (current.equals(value)) { + return runState.eei.useGas( + new BN(runState._common.param('gasPrices', 'sstoreNoopGasEIP2200')), + ) + } + if (original.equals(current)) { + // Create slot + if (original.length === 0) { + return runState.eei.useGas( + new BN(runState._common.param('gasPrices', 'sstoreInitGasEIP2200')), + ) + } + // Delete slot + if (value.length === 0) { + runState.eei.refundGas( + new BN(runState._common.param('gasPrices', 'sstoreClearRefundEIP2200')), + ) + } + // Write existing slot + return runState.eei.useGas( + new BN(runState._common.param('gasPrices', 'sstoreCleanGasEIP2200')), + ) + } + if (original.length > 0) { + if (current.length === 0) { + // Recreate slot + runState.eei.subRefund( + new BN(runState._common.param('gasPrices', 'sstoreClearRefundEIP2200')), + ) + } else if (value.length === 0) { + // Delete slot + runState.eei.refundGas( + new BN(runState._common.param('gasPrices', 'sstoreClearRefundEIP2200')), + ) + } + } + if (original.equals(value)) { + if (original.length === 0) { + // Reset to original non-existent slot + runState.eei.refundGas( + new BN(runState._common.param('gasPrices', 'sstoreInitRefundEIP2200')), + ) + } else { + // Reset to original existing slot + runState.eei.refundGas( + new BN(runState._common.param('gasPrices', 'sstoreCleanRefundEIP2200')), + ) + } + } + // Dirty update + return runState.eei.useGas(new BN(runState._common.param('gasPrices', 'sstoreDirtyGasEIP2200'))) } else { if (value.length === 0 && !found.length) { runState.eei.useGas(new BN(runState._common.param('gasPrices', 'sstoreReset'))) diff --git a/lib/exceptions.ts b/lib/exceptions.ts index e939c13c30..f7669b81ca 100644 --- a/lib/exceptions.ts +++ b/lib/exceptions.ts @@ -10,6 +10,7 @@ export enum ERROR { INTERNAL_ERROR = 'internal error', CREATE_COLLISION = 'create collision', STOP = 'stop', + REFUND_EXHAUSTED = 'refund exhausted', } export class VmError { diff --git a/tests/api/istanbul/eip-2200.js b/tests/api/istanbul/eip-2200.js new file mode 100644 index 0000000000..840c3bf1a6 --- /dev/null +++ b/tests/api/istanbul/eip-2200.js @@ -0,0 +1,68 @@ +const tape = require('tape') +const BN = require('bn.js') +const Common = require('ethereumjs-common').default +const VM = require('../../../dist/index').default +const PStateManager = require('../../../dist/state/promisified').default +const { ERROR } = require('../../../dist/exceptions') +const { createAccount } = require('../utils') + +const testCases = [ + { original: new BN(0), code: '60006000556000600055', used: 1612, refund: 0 }, // 0 -> 0 -> 0 + { original: new BN(0), code: '60006000556001600055', used: 20812, refund: 0 }, // 0 -> 0 -> 1 + { original: new BN(0), code: '60016000556000600055', used: 20812, refund: 19200 }, // 0 -> 1 -> 0 + { original: new BN(0), code: '60016000556002600055', used: 20812, refund: 0 }, // 0 -> 1 -> 2 + { original: new BN(0), code: '60016000556001600055', used: 20812, refund: 0 }, // 0 -> 1 -> 1 + { original: new BN(1), code: '60006000556000600055', used: 5812, refund: 15000 }, // 1 -> 0 -> 0 + { original: new BN(1), code: '60006000556001600055', used: 5812, refund: 4200 }, // 1 -> 0 -> 1 + { original: new BN(1), code: '60006000556002600055', used: 5812, refund: 0 }, // 1 -> 0 -> 2 + { original: new BN(1), code: '60026000556000600055', used: 5812, refund: 15000 }, // 1 -> 2 -> 0 + { original: new BN(1), code: '60026000556003600055', used: 5812, refund: 0 }, // 1 -> 2 -> 3 + { original: new BN(1), code: '60026000556001600055', used: 5812, refund: 4200 }, // 1 -> 2 -> 1 + { original: new BN(1), code: '60026000556002600055', used: 5812, refund: 0 }, // 1 -> 2 -> 2 + { original: new BN(1), code: '60016000556000600055', used: 5812, refund: 15000 }, // 1 -> 1 -> 0 + { original: new BN(1), code: '60016000556002600055', used: 5812, refund: 0 }, // 1 -> 1 -> 2 + { original: new BN(1), code: '60016000556001600055', used: 1612, refund: 0 }, // 1 -> 1 -> 1 + { original: new BN(0), code: '600160005560006000556001600055', used: 40818, refund: 19200 }, // 0 -> 1 -> 0 -> 1 + { original: new BN(1), code: '600060005560016000556000600055', used: 10818, refund: 19200 }, // 1 -> 0 -> 1 -> 0 + { original: new BN(1), gas: new BN(2306), code: '6001600055', used: 2306, refund: 0, err: ERROR.OUT_OF_GAS }, // 1 -> 1 (2300 sentry + 2xPUSH) + { original: new BN(1), gas: new BN(2307), code: '6001600055', used: 806, refund: 0 } // 1 -> 1 (2301 sentry + 2xPUSH) +] + +tape('Istanbul: EIP-2200: net-metering SSTORE', async (t) => { + const caller = Buffer.from('0000000000000000000000000000000000000000', 'hex') + const addr = Buffer.from('00000000000000000000000000000000000000ff', 'hex') + const key = new BN(0).toArrayLike(Buffer, 'be', 32) + for (const testCase of testCases) { + const common = new Common('mainnet', 'istanbul') + const vm = new VM({ common }) + const state = new PStateManager(vm.stateManager) + + const account = createAccount('00', '00') + await state.putAccount(addr, account) + await state.putContractCode(addr, Buffer.from(testCase.code, 'hex')) + if (!testCase.original.isZero()) { + await state.putContractStorage(addr, key, testCase.original) + } + + const runCallArgs = { + caller, + gasLimit: testCase.gas ? testCase.gas : new BN(0xffffffffff), + to: addr + } + + try { + const res = await vm.runCall(runCallArgs) + if (testCase.err) { + t.equal(res.execResult.exceptionError.error, testCase.err) + } else { + t.assert(res.execResult.exceptionError === undefined) + } + t.assert(new BN(testCase.used).eq(res.gasUsed)) + t.assert(new BN(testCase.refund).eq(res.execResult.gasRefund)) + } catch (e) { + t.fail(e.message) + } + } + + t.end() +})