diff --git a/packages/evm/src/Bytes32.ts b/packages/evm/src/Bytes32.ts index 15451f3..a66ea76 100644 --- a/packages/evm/src/Bytes32.ts +++ b/packages/evm/src/Bytes32.ts @@ -1,5 +1,6 @@ import BN from 'bn.js' import { Byte } from './Byte' +import { Address } from './Address' const TWO_POW256 = new BN('1' + '0'.repeat(64), 16) const MAX_256 = new BN('f'.repeat(64), 16) @@ -23,6 +24,10 @@ export class Bytes32 { return new Bytes32(new BN(value).toTwos(256)) } + static fromAddress (value: Address) { + return Bytes32.fromHex(value) + } + static fromHex (value: string) { return new Bytes32(new BN(value, 16)) } @@ -43,6 +48,11 @@ export class Bytes32 { return this.value.toString(16, 64) } + toAddress () { + const hex = this.toHex() + return hex.substring(24) as Address + } + toBytes () { return this.value.toArray(undefined, 32) as Byte[] } diff --git a/packages/evm/src/opcodes/create.ts b/packages/evm/src/opcodes/create.ts new file mode 100644 index 0000000..aca396c --- /dev/null +++ b/packages/evm/src/opcodes/create.ts @@ -0,0 +1,54 @@ +import { ExecutionContext } from '../ExecutionContext' +import { Bytes32 } from '../Bytes32' +import { getContractAddress } from '../getContractAddress' +import { executeCode } from '../executeCode' + +export function opCREATE (ctx: ExecutionContext) { + const value = ctx.stack.pop() + const memoryOffset = ctx.stack.pop().toUnsignedNumber() + const memoryBytes = ctx.stack.pop().toUnsignedNumber() + + const gasLeft = ctx.message.gasLimit - ctx.gasUsed + const gasLimit = allButOne64th(gasLeft) + const initCode = ctx.memory.getBytes(memoryOffset, memoryBytes) + + const nonce = ctx.state.getNonce(ctx.message.account) + const balance = ctx.state.getBalance(ctx.message.account) + + const contract = getContractAddress(ctx.message.account, nonce) + + if (balance.lt(value)) { + ctx.stack.push(Bytes32.ZERO) + return + } + + ctx.state.setNonce(ctx.message.account, nonce + 1) + ctx.state.setBalance(ctx.message.account, balance.sub(value)) + + const result = executeCode({ + account: contract, + callDepth: ctx.message.callDepth + 1, + sender: ctx.message.account, + origin: ctx.message.origin, + gasLimit, + gasPrice: ctx.message.gasPrice, + code: initCode, + data: [], + enableStateModifications: ctx.message.enableStateModifications, + value, + }, ctx.state.clone()) + + if (result.type === 'ExecutionSuccess') { + ctx.stack.push(Bytes32.fromAddress(contract)) + ctx.state = result.state + + ctx.useGas(result.gasUsed) + ctx.refund(result.gasRefund) + } else { + ctx.stack.push(Bytes32.ZERO) + } +} + +function allButOne64th (value: number) { + return value - Math.floor(value / 64) +} diff --git a/packages/evm/src/opcodes/index.ts b/packages/evm/src/opcodes/index.ts index d6d3c06..6c38bff 100644 --- a/packages/evm/src/opcodes/index.ts +++ b/packages/evm/src/opcodes/index.ts @@ -40,6 +40,7 @@ import { opMSIZE, opMLOAD, opMSTORE, opMSTORE8 } from './memory' import { opSSTORE, opSLOAD } from './storage' import { Byte } from '../Byte' import { opCODESIZE, opCODECOPY } from './code' +import { opCREATE } from './create' export { opUnreachable } from './invalid' export { makeOpPUSH } from './stack' @@ -122,6 +123,7 @@ const OP_CODES: Record = { 0x9d: makeOpSWAP(14), 0x9e: makeOpSWAP(15), 0x9f: makeOpSWAP(16), + 0xf0: opCREATE, 0xf3: opRETURN, 0xfd: opREVERT, } diff --git a/packages/evm/test/Bytes32.test.ts b/packages/evm/test/Bytes32.test.ts index 3013c2d..ef5c56b 100644 --- a/packages/evm/test/Bytes32.test.ts +++ b/packages/evm/test/Bytes32.test.ts @@ -3,6 +3,7 @@ import { Bytes32 } from '../src/Bytes32' import { TestCases } from './opcodes/bytes32/cases' import { TestCase } from './opcodes/bytes32/cases/helpers' import { Byte } from '../src/Byte' +import { Address } from '../src/Address' describe('Bytes32', () => { runTestCases('add', TestCases.ADD) @@ -29,6 +30,21 @@ describe('Bytes32', () => { runTestCases('shr', invert(TestCases.SHR)) runTestCases('sar', invert(TestCases.SAR)) + describe('to and from Address', () => { + it('toAddress ignores first bytes', () => { + const hex = 'ab'.repeat(16) + 'cd'.repeat(16) + const value = Bytes32.fromHex(hex) + expect(value.toAddress()).to.equal('ab'.repeat(4) + 'cd'.repeat(16)) + }) + + it('fromAddress works like from hex', () => { + const address = 'ab'.repeat(20) as Address + const a = Bytes32.fromAddress(address) + const b = Bytes32.fromHex(address) + expect(a.eq(b)).to.equal(true) + }) + }) + describe('to and from number', () => { it('fromNumber works for positive numbers', () => { const a = Bytes32.fromNumber(42) diff --git a/packages/evm/test/helpers/executeAssembly.ts b/packages/evm/test/helpers/executeAssembly.ts index fc822a9..cb0485f 100644 --- a/packages/evm/test/helpers/executeAssembly.ts +++ b/packages/evm/test/helpers/executeAssembly.ts @@ -30,7 +30,10 @@ export function executeAssembly ( } export function assemblyToBytecode (code: string): Byte[] { - const instructions = code.trim().split(/\s+/) + const instructions = code + .replace(/\/\/.*/g, ' ') // remove comments + .trim() + .split(/\s+/) const result: Byte[] = [] for (const instruction of instructions) { const opcode = OPCODES[instruction] as Byte diff --git a/packages/evm/test/opcodes/create.test.ts b/packages/evm/test/opcodes/create.test.ts new file mode 100644 index 0000000..d96d79e --- /dev/null +++ b/packages/evm/test/opcodes/create.test.ts @@ -0,0 +1,83 @@ +import { expect } from 'chai' +import { executeAssembly } from "../helpers" +import { Address } from "../../src/Address" +import { State } from "../../src/State" +import { getContractAddress } from '../../src/getContractAddress' +import { Bytes32 } from '../../src/Bytes32' + +describe('CREATE opcode', () => { + const assembly = ` + PUSH1 05 // size of the code + PUSH1 12 // code offset + PUSH1 00 // memory offset of the code + CODECOPY + + PUSH1 05 // size of the code + PUSH1 00 // memory offset of the code + PUSH1 69 // value passed + CREATE + + PUSH1 00 + SSTORE // save the address of the created contract + STOP + + // code of the contract + PUSH1 01 + PUSH1 00 + SSTORE // save 1 at address 0 in the storage of the new contract + ` + const account = 'abcd'.repeat(10) as Address + + it('results in the creation of a new contract', () => { + const state = new State() + state.setNonce(account, 42) + state.setBalance(account, Bytes32.fromNumber(0x100)) + + const result = executeAssembly(assembly, { account }, state) + + if (result.type !== 'ExecutionSuccess') { + expect(result.type).to.equal('ExecutionSuccess') + } else { + // increments nonce + expect(result.state.getNonce(account)).to.equal(43) + + // subtracts balance + const balance = result.state.getBalance(account) + expect(balance.eq(Bytes32.fromNumber(0x100 - 0x69))).to.equal(true) + + // returns correct address + const expectedAddress = getContractAddress(account, 42) + const actualAddress = result.state + .getStorage(account, Bytes32.ZERO) + .toAddress() + expect(actualAddress).to.equal(expectedAddress) + + // actually runs the contract code + const stored = result.state.getStorage(actualAddress, Bytes32.ZERO) + expect(stored.eq(Bytes32.ONE)).to.equal(true) + } + }) + + it('account does not have the balance', () => { + const state = new State() + state.setNonce(account, 42) + state.setBalance(account, Bytes32.fromNumber(0x68)) + + const result = executeAssembly(assembly, { account }, state) + + if (result.type !== 'ExecutionSuccess') { + expect(result.type).to.equal('ExecutionSuccess') + } else { + // does not increment the nonce + expect(result.state.getNonce(account)).to.equal(42) + + // does not subtract the balance + const balance = result.state.getBalance(account) + expect(balance.eq(Bytes32.fromNumber(0x68))).to.equal(true) + + // returns zero + const returnValue = result.state.getStorage(account, Bytes32.ZERO) + expect(returnValue).to.equal(Bytes32.ZERO) + } + }) +})