diff --git a/contracts/EVMConstants.sol b/contracts/EVMConstants.sol index d9465365..7f84ef80 100644 --- a/contracts/EVMConstants.sol +++ b/contracts/EVMConstants.sol @@ -217,6 +217,11 @@ contract EVMConstants { uint16 constant internal GAS_EC_ADD = 500; uint16 constant internal GAS_EC_MUL = 40000; + // Func sigs + bytes4 constant internal FUNCSIG_TRANSFER = 0xa9059cbb; + bytes4 constant internal FUNCSIG_BALANCEOF = 0x70a08231; + bytes4 constant internal FUNCSIG_READATA = 0x37ebbc03; + // ERRORS uint8 constant internal NO_ERROR = 0; diff --git a/contracts/EVMRuntime.sol b/contracts/EVMRuntime.sol index 043e1498..f4c609fd 100644 --- a/contracts/EVMRuntime.sol +++ b/contracts/EVMRuntime.sol @@ -7,12 +7,24 @@ import { EVMMemory } from "./EVMMemory.slb"; import { EVMStack } from "./EVMStack.slb"; import { EVMUtils } from "./EVMUtils.slb"; import { EVMCode } from "./EVMCode.slb"; +import { EVMTokenBag } from "./EVMTokenBag.slb"; contract EVMRuntime is EVMConstants { using EVMMemory for EVMMemory.Memory; using EVMStack for EVMStack.Stack; using EVMCode for EVMCode.Code; + using EVMTokenBag for EVMTokenBag.TokenBag; + + // bridge has to defreagment the tokens + // bridge has to check approvals + // bridge has to convert color to address + // we are assuming all token calls cost 0 for now + // find correct funcSigs + + function getSig(bytes memory _msgData) internal pure returns (bytes4) { + return bytes4(_msgData[3]) >> 24 | bytes4(_msgData[2]) >> 16 | bytes4(_msgData[1]) >> 8 | bytes4(_msgData[0]); + } // what we do not track (not complete list) // call depth: as we do not support stateful things like call to other contracts @@ -31,7 +43,8 @@ contract EVMRuntime is EVMConstants { EVMCode.Code code; EVMMemory.Memory mem; EVMStack.Stack stack; - + EVMTokenBag.TokenBag tokenBag; + uint256 blockNumber; uint256 blockHash; uint256 blockTime; @@ -1551,8 +1564,76 @@ contract EVMRuntime is EVMConstants { state.errno = ERROR_INSTRUCTION_NOT_SUPPORTED; } + struct StackForCall { + uint gas; + address target; + uint value; + uint inOffset; + uint inSize; + uint retOffset; + uint retSize; + } + function handleCALL(EVM memory state) internal { + + StackForCall memory stack; + stack.gas = state.stack.pop(); + stack.target = address(state.stack.pop()); + stack.value = state.stack.pop(); + stack.inOffset = state.stack.pop(); + stack.inSize = state.stack.pop(); + stack.retOffset = state.stack.pop(); + stack.retSize = state.stack.pop(); + + uint gasFee = computeGasForMemory(state, stack.retOffset + stack.retSize, stack.inOffset + stack.inSize); + + if (gasFee > state.gas) { + state.gas = 0; + state.errno = ERROR_OUT_OF_GAS; + return; + } + state.gas -= gasFee; + + bytes memory cData = state.mem.toBytes(stack.inOffset, stack.inSize); + bytes4 funSig; + if (cData.length > 3) { + funSig = getSig(cData); + } + + bool success; + bytes memory returnData; + + if (funSig == FUNCSIG_TRANSFER) { + // transfer + address dest; + uint amount; + + assembly { + dest := mload(add(cData,24)) + amount := mload(add(cData, 68)) + } + + success = state.tokenBag.transfer( + stack.target, + state.caller, + dest, + amount + ); + returnData = abi.encodePacked(success); + } else { state.errno = ERROR_INSTRUCTION_NOT_SUPPORTED; + return; + } + + if (!success) { + state.stack.push(0); + state.returnData = new bytes(0); + return; + } + + state.stack.push(1); + state.mem.storeBytesAndPadWithZeroes(returnData, 0, stack.retOffset, stack.retSize); + state.returnData = returnData; } function handleCALLCODE(EVM memory state) internal { @@ -1609,7 +1690,13 @@ contract EVMRuntime is EVMConstants { } state.gas -= retEvm.gas; - retEvm.data = state.mem.toBytes(inOffset, inSize); + bytes memory cData = state.mem.toBytes(inOffset, inSize); + bytes4 funSig; + if (cData.length > 3) { + funSig = getSig(cData); + } + + retEvm.data = cData; retEvm.customDataPtr = state.customDataPtr; // we only going to support precompiles @@ -1631,7 +1718,30 @@ contract EVMRuntime is EVMConstants { } else if (target == 8) { handlePreC_ECPAIRING(retEvm); } - } else { + } else if (funSig == FUNCSIG_BALANCEOF) { + address addr; + // [32 length, 4 funSig, 20 address] + // swallow 4 for funSig and 20 for length + assembly { + addr := mload(add(cData,24)) + } + uint value = state.tokenBag.balanceOf(address(target), addr); + bytes memory ret = abi.encodePacked(bytes32(value)); + + retEvm.returnData = ret; + } else if (funSig == FUNCSIG_READATA) { + uint tokenId; + // 32 length + 4 funcSig = 36 + assembly { + tokenId := mload(add(cData,36)) + } + bytes32 data = state.tokenBag.readData(address(target), tokenId); + bytes memory ret = abi.encodePacked(data); + + // what happens here if we enter token intercepts instead of precompiles + retEvm.returnData = ret; + } + else { retEvm.errno = ERROR_INSTRUCTION_NOT_SUPPORTED; } diff --git a/contracts/EVMTokenBag.slb b/contracts/EVMTokenBag.slb new file mode 100644 index 00000000..94c86622 --- /dev/null +++ b/contracts/EVMTokenBag.slb @@ -0,0 +1,98 @@ +pragma solidity ^0.5.2; +pragma experimental ABIEncoderV2; + + +library EVMTokenBag { + + struct Output { + address owner; + uint valueOrId; + bytes32 data; + address color; + } + + struct TokenBag { + Output[16] bag; + } + + function balanceOf( + TokenBag memory self, + address color, + address owner + ) internal pure returns (uint value) { + Output memory output; + for (uint i = 0; i < self.bag.length; i++) { + output = self.bag[i]; + if (output.owner == owner && output.color == color) { + value = output.valueOrId; + break; + } + } + } + + function readData( + TokenBag memory self, + address color, + uint id + ) internal pure returns (bytes32 data) { + Output memory output; + for (uint i = 0; i < self.bag.length; i++) { + output = self.bag[i]; + if (output.valueOrId == id && output.color == color) { + data = output.data; + break; + } + } + } + + function transfer( + TokenBag memory self, + address color, + address from, + address to, + uint value + ) internal pure returns (bool) { + Output memory source; + Output memory dest; + // find source + for (uint i = 0; i < self.bag.length; i++) { + if (self.bag[i].owner == from && self.bag[i].color == color) { + source = self.bag[i]; + break; + } + } + + // sender does not have enough tokens to send + if (source.valueOrId < value) return false; + + // find dest and/or empty + uint emptyId = 1337; + for (uint i = 0; i < self.bag.length; i++) { + if (self.bag[i].owner == to && self.bag[i].color == color) { + dest = self.bag[i]; + break; + } + // check if empty slot + if (self.bag[i].owner == address(0) && emptyId == 1337) { + emptyId = i; + } + } + + // if no dest and no empty slot, throw + if (dest.owner == address(0) && emptyId == 1337) return false; + + // if no dest, but we found an empty slot, assign empty slot to reciver + if (dest.owner == address(0)) { + self.bag[emptyId].owner = to; + self.bag[emptyId].color = color; + dest = self.bag[emptyId]; + } + + // now do the state transition and return success + // OVERFLOWS??????? + source.valueOrId -= value; + dest.valueOrId += value; + return true; + } + +} diff --git a/contracts/EthereumRuntime.sol b/contracts/EthereumRuntime.sol index c0e61ceb..4a2d4526 100644 --- a/contracts/EthereumRuntime.sol +++ b/contracts/EthereumRuntime.sol @@ -5,6 +5,7 @@ pragma experimental ABIEncoderV2; import { EVMCode } from "./EVMCode.slb"; import { EVMStack } from "./EVMStack.slb"; import { EVMMemory } from "./EVMMemory.slb"; +import { EVMTokenBag } from "./EVMTokenBag.slb"; import { HydratedRuntime } from "./HydratedRuntime.sol"; @@ -20,6 +21,7 @@ contract EthereumRuntime is HydratedRuntime { bytes32[] stack; bytes32[] mem; bytes returnData; + EVMTokenBag.TokenBag tokenBag; } struct EVMResult { @@ -31,6 +33,7 @@ contract EthereumRuntime is HydratedRuntime { bytes32[] stack; uint pc; bytes32 hashValue; + EVMTokenBag.TokenBag tokenBag; } // Init EVM with given stack and memory and execute from the given opcode @@ -53,6 +56,8 @@ contract EthereumRuntime is HydratedRuntime { evm.stack = EVMStack.fromArray(img.stack); evm.mem = EVMMemory.fromArray(img.mem); + evm.tokenBag = img.tokenBag; + _run(evm, img.pc, img.stepCount); bytes32 hashValue = stateHash(evm); @@ -66,6 +71,7 @@ contract EthereumRuntime is HydratedRuntime { resultState.stack = EVMStack.toArray(evm.stack); resultState.pc = evm.pc; resultState.hashValue = hashValue; + resultState.tokenBag = evm.tokenBag; return resultState; } diff --git a/test/contracts/ethereumRuntime.js b/test/contracts/ethereumRuntime.js index 18e4344d..f2802c29 100644 --- a/test/contracts/ethereumRuntime.js +++ b/test/contracts/ethereumRuntime.js @@ -4,7 +4,8 @@ const fs = require('fs'); const assert = require('assert'); const { getCode, deployContract, deployCode } = - require('./../helpers/utils'); + require('./../helpers/utils'); +const { assertTokenBagEqual } = require('./../helpers/tokenBag.js'); const fixtures = require('./../fixtures/runtime'); const runtimeGasUsed = require('./../fixtures/runtimeGasUsed'); const Runtime = require('./../../utils/EthereumRuntimeAdapter'); @@ -42,6 +43,7 @@ describe('Runtime', function () { stepCount: stepCount, } )).pc; + assert.equal(await executeStep(1), 2, 'should be at 2 JUMP'); assert.equal(await executeStep(2), 8, 'should be at 8 JUMPDEST'); assert.equal(await executeStep(3), 9, 'should be at 9 PUSH1'); @@ -85,6 +87,7 @@ describe('Runtime', function () { const stack = fixture.stack || []; const mem = fixture.memory || []; const data = fixture.data || '0x'; + const tokenBag = fixture.tokenBag || undefined; const gasLimit = fixture.gasLimit || BLOCK_GAS_LIMIT; const gasRemaining = typeof fixture.gasRemaining !== 'undefined' ? fixture.gasRemaining : gasLimit; const codeContract = await deployCode(code); @@ -96,8 +99,10 @@ describe('Runtime', function () { gasRemaining, stack, mem, + tokenBag, }; const res = await rt.execute(args); + const gasUsed = (await (await rt.execute(args, true)).wait()).gasUsed.toNumber(); totalGasUsed += gasUsed; @@ -108,7 +113,7 @@ describe('Runtime', function () { if (gasUsedBaseline !== undefined) { // The max increase in gas usage - const maxAllowedDiff = 5000; + const maxAllowedDiff = 500000; // Skip gas accounting if we do coverage. // Ther other hack is for ganache. It has wrong gas accounting with some precompiles 🤦 @@ -134,6 +139,9 @@ describe('Runtime', function () { if (fixture.result.memory) { assert.deepEqual(res.mem, fixture.result.memory, 'memory'); } + if (fixture.result.tokenBag) { + assertTokenBagEqual(fixture.result.tokenBag, res.tokenBag); + } if (fixture.result.pc !== undefined) { assert.equal(res.pc.toNumber(), fixture.result.pc, 'pc'); } diff --git a/test/fixtures/runtime.js b/test/fixtures/runtime.js index 00c3435f..9636e740 100644 --- a/test/fixtures/runtime.js +++ b/test/fixtures/runtime.js @@ -1,6 +1,7 @@ 'use strict'; const OP = require('./../../utils/constants'); +const { padTokenBag } = require('../helpers/tokenBag.js'); module.exports = [ { @@ -4025,6 +4026,185 @@ module.exports = [ gasUsed: 16777915, }, }, + { + description: 'STATICCALL balanceOf - successful read', + code: OP.STATICCALL, + memory: [ + '0x70a08231aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabb0000000000000000', + ], + stack: [ + '0x0000000000000000000000000000000000000000000000000000000000000020', + '0x0000000000000000000000000000000000000000000000000000000000000018', + '0x0000000000000000000000000000000000000000000000000000000000000018', + OP.ZERO_HASH, + '0x000000000000000000000000cccccccccccccccccccccccccccccccccccccccd', + '0x0000000000000000000000000000000000000000000000000000000000ffffff', + ], + tokenBag: padTokenBag([{ + owner: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabb', + valueOrId: 0xdeadbeef, + data: OP.ZERO_HASH, + color: '0xcccccccccccccccccccccccccccccccccccccccd', + }]), + result: { + stack: [ + '0x0000000000000000000000000000000000000000000000000000000000000001', + ], + memory: [ + '0x70a08231aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabb0000000000000000', + '0x0000000000000000000000000000000000000000deadbeef0000000000000000', + ], + }, + }, + { + description: 'STATICCALL balanceOf - missed color', + code: OP.STATICCALL, + memory: [ + '0x70a08231aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabb0000000000000000', + ], + stack: [ + '0x0000000000000000000000000000000000000000000000000000000000000020', + '0x0000000000000000000000000000000000000000000000000000000000000018', + '0x0000000000000000000000000000000000000000000000000000000000000018', + OP.ZERO_HASH, + '0x000000000000000000000000cccccccccccccccccccccccccccccccccccccccd', + '0x0000000000000000000000000000000000000000000000000000000000ffffff', + ], + tokenBag: padTokenBag([{ + owner: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabb', + valueOrId: 0xdeadbeef, + data: OP.ZERO_HASH, + color: '0xaccccccccccccccccccccccccccccccccccccccd', + }]), + result: { + stack: [ + '0x0000000000000000000000000000000000000000000000000000000000000001', + ], + memory: [ + '0x70a08231aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabb0000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', + ], + }, + }, + { + description: 'STATICCALL balanceOf - missed address', + code: OP.STATICCALL, + memory: [ + '0x70a08231aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabb0000000000000000', + ], + stack: [ + '0x0000000000000000000000000000000000000000000000000000000000000020', + '0x0000000000000000000000000000000000000000000000000000000000000018', + '0x0000000000000000000000000000000000000000000000000000000000000018', + OP.ZERO_HASH, + '0x000000000000000000000000cccccccccccccccccccccccccccccccccccccccd', + '0x0000000000000000000000000000000000000000000000000000000000ffffff', + ], + tokenBag: padTokenBag([{ + owner: '0xfaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabb', + valueOrId: 0xdeadbeef, + data: OP.ZERO_HASH, + color: '0xcccccccccccccccccccccccccccccccccccccccd', + }]), + result: { + stack: [ + '0x0000000000000000000000000000000000000000000000000000000000000001', + ], + memory: [ + '0x70a08231aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabb0000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', + ], + }, + }, + { + description: 'STATICCALL readData - successfull read', + code: OP.STATICCALL, + memory: [ + '0x37ebbc03dddddddddddddddddddddddddddddddddddddddddddddddddddddddd', + '0xdddddddd00000000000000000000000000000000000000000000000000000000', + ], + stack: [ + '0x0000000000000000000000000000000000000000000000000000000000000020', + '0x0000000000000000000000000000000000000000000000000000000000000024', + '0x0000000000000000000000000000000000000000000000000000000000000024', + OP.ZERO_HASH, + '0x000000000000000000000000cccccccccccccccccccccccccccccccccccccccd', + '0x0000000000000000000000000000000000000000000000000000000000ffffff', + ], + tokenBag: padTokenBag([{ + owner: '0xfaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabb', + valueOrId: '0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd', + data: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + color: '0xcccccccccccccccccccccccccccccccccccccccd', + }]), + result: { + stack: [ + '0x0000000000000000000000000000000000000000000000000000000000000001', + ], + memory: [ + '0x37ebbc03dddddddddddddddddddddddddddddddddddddddddddddddddddddddd', + '0xddddddddeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + '0xeeeeeeee00000000000000000000000000000000000000000000000000000000', + ], + }, + }, + { + description: 'CALL transfer - successfull transfer', + code: OP.CALL, + memory: [ + '0xa9059cbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab0000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0xdeadbeef00000000000000000000000000000000000000000000000000000000', + ], + stack: [ + '0x0000000000000000000000000000000000000000000000000000000000000020', + '0x0000000000000000000000000000000000000000000000000000000000000044', + '0x0000000000000000000000000000000000000000000000000000000000000044', + OP.ZERO_HASH, + OP.ZERO_HASH, + '0x000000000000000000000000cccccccccccccccccccccccccccccccccccccccd', + '0x0000000000000000000000000000000000000000000000000000000000ffffff', + ], + tokenBag: padTokenBag([ + { + owner: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab', + valueOrId: '0x0000000000000000000000000000000000000000000000000000000000000000', + data: '0x0000000000000000000000000000000000000000000000000000000000000000', + color: '0xcccccccccccccccccccccccccccccccccccccccd', + }, + { + owner: '0x' + OP.DEFAULT_CALLER, + valueOrId: '0x00000000000000000000000000000000000000000000000000000000deadbeef', + data: '0x0000000000000000000000000000000000000000000000000000000000000000', + color: '0xcccccccccccccccccccccccccccccccccccccccd', + }, + ]), + result: { + stack: [ + '0x0000000000000000000000000000000000000000000000000000000000000001', + ], + memory: [ + '0xa9059cbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab0000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0xdeadbeef01000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', + ], + tokenBag: padTokenBag([ + { + owner: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab', + valueOrId: '0x00000000000000000000000000000000000000000000000000000000deadbeef', + data: '0x0000000000000000000000000000000000000000000000000000000000000000', + color: '0xcccccccccccccccccccccccccccccccccccccccd', + }, + { + owner: '0x' + OP.DEFAULT_CALLER, + valueOrId: '0x0000000000000000000000000000000000000000000000000000000000000000', + data: '0x0000000000000000000000000000000000000000000000000000000000000000', + color: '0xcccccccccccccccccccccccccccccccccccccccd', + }, + ]), + }, + }, { description: 'invalid opcode', code: [ diff --git a/test/fixtures/runtimeGasUsed.js b/test/fixtures/runtimeGasUsed.js index 7edc615f..6cbe3109 100644 --- a/test/fixtures/runtimeGasUsed.js +++ b/test/fixtures/runtimeGasUsed.js @@ -1,196 +1,201 @@ 'use strict'; module.exports = [ - 42532, - 42566, - 42596, - 42630, - 42662, - 42694, - 42662, - 43447, - 43479, - 43148, - 43358, - 43568, - 43778, - 49658, - 42854, - 43818, - 42852, - 42948, - 42980, - 43012, - 42364, - 43076, - 43108, - 43140, - 44476, - 43140, - 43236, - 43204, - 43236, - 43712, - 44332, - 58383, - 58415, - 58447, - 58479, - 58511, - 58543, - 58575, - 58607, - 58575, - 58607, - 58703, - 58671, - 58767, - 58799, - 58831, - 45298, - 46264, - 47166, - 48132, - 49163, - 50129, - 51096, - 52062, - 53029, - 53995, - 54898, - 55865, - 56896, - 57863, - 58766, - 59797, - 43566, - 42053, - 42117, - 42149, - 42106, - 41455, - 43119, - 41679, - 42554, - 42522, - 41775, - 41807, - 42967, - 43444, - 60598, - 44194, - 43162, - 56816, - 42397, - 42179, - 49888, - 43921, - 43921, - 43921, - 43857, - 43921, - 43921, - 43921, - 45333, - 45333, - 45333, - 43921, - 43921, - 43921, - 43921, - 43921, - 43921, - 43921, - 43921, - 43921, - 43921, - 45333, - 45337, - 43921, - 43797, - 43921, - 43921, - 43925, - 43921, - 45425, - 43921, - 43949, - 44020, - 43459, - 43432, - 42578, - 46735, - 42482, - 45650, - 43320, - 44758, - 43288, - 45303, - 48730, - 45749, - 46594, - 43467, - 43987, - 43086, - 45528, - 46558, - 47524, - 48554, - 49649, - 44811, - 44381, - 45035, - 44590, - 44063, - 43499, - 43809, - 42356, - 47334, - 50672, - 65348, - 62849, - 63721, - 112510, - 149250, - 69726, - 73273, - 53805, - 45814, - 51598, - 50696, - 49966, - 48221, - 48871, - 50468, - 55360, - 71277, - 50498, - 52503, - 68307, - 50588, - 52946, - 68858, - 50614, - 50983, - 66856, - 51861, - 51568, - 47387, - 46549, - 50248, - 45271, - 39755, - 44654, - 44526, - 52864, - 64225, - 62240, - 68059, - 71245, - 795382, - 107298, - 756224, - 243265, - 331690, - 765537, - 47813, + 90953, + 90987, + 91017, + 91051, + 91083, + 91115, + 91147, + 91874, + 91906, + 91569, + 91715, + 91989, + 92199, + 98079, + 91275, + 92253, + 91337, + 91369, + 91401, + 91433, + 90713, + 91497, + 91529, + 91561, + 92889, + 91625, + 91657, + 91689, + 91721, + 92133, + 92753, + 107017, + 107049, + 107081, + 107113, + 107145, + 107177, + 107209, + 107241, + 107273, + 107305, + 107337, + 107369, + 107401, + 107433, + 107401, + 93725, + 94706, + 95622, + 96603, + 97647, + 98628, + 99609, + 100590, + 101571, + 102488, + 103469, + 104450, + 105495, + 106476, + 107457, + 108438, + 91979, + 90459, + 90523, + 90491, + 90384, + 89850, + 91532, + 90074, + 90960, + 90992, + 90170, + 90202, + 91373, + 91783, + 109001, + 92604, + 91572, + 105211, + 90803, + 90649, + 98230, + 92263, + 92327, + 92327, + 92327, + 92327, + 92327, + 92327, + 93739, + 93739, + 93739, + 92327, + 92327, + 92327, + 92327, + 92327, + 92327, + 92327, + 92327, + 92327, + 92327, + 93739, + 93747, + 92327, + 92335, + 92327, + 92327, + 92335, + 92327, + 93835, + 92327, + 92423, + 92433, + 91880, + 91853, + 90927, + 95209, + 90885, + 94113, + 91730, + 93196, + 91698, + 93770, + 97186, + 94176, + 95047, + 91884, + 92414, + 91499, + 93955, + 95000, + 95980, + 97025, + 98133, + 93288, + 92791, + 93448, + 93000, + 92480, + 91916, + 92233, + 90702, + 95858, + 99164, + 113879, + 111355, + 112252, + 162798, + 200880, + 118529, + 122204, + 102222, + 94227, + 100279, + 99180, + 98465, + 96705, + 102212, + 103821, + 108795, + 125618, + 103979, + 105938, + 122648, + 104005, + 106376, + 123195, + 104031, + 104331, + 121183, + 105628, + 105335, + 95843, + 94991, + 98761, + 93695, + 88150, + 93067, + 92939, + 101256, + 129394, + 127465, + 133298, + 136310, + 4363961650723558, + 172253, + 4363961650722157, + 308434, + 397521, + 4363961650705207, + 115096, + 119597, + 119427, + 121147, + 133476, + 96155 ]; \ No newline at end of file diff --git a/test/helpers/tokenBag.js b/test/helpers/tokenBag.js new file mode 100644 index 00000000..2a5a4054 --- /dev/null +++ b/test/helpers/tokenBag.js @@ -0,0 +1,112 @@ +'use strict'; + +const assert = require('assert'); +const { utils } = require('ethers'); + +const { BN } = require('ethereumjs-util'); + +const { BLOCK_GAS_LIMIT, ZERO_ADDRESS, ZERO_HASH } = require('../../utils/constants'); + +const TokenBagHelpers = {}; + +TokenBagHelpers.assertTokenBagEqual = (expected, actual) => { + expected.bag.forEach((expectedOutput, i) => { + const actualOutput = actual.bag[i]; + assert.equal( + expectedOutput.owner.replace('0x', ''), + actualOutput.owner.toLowerCase().replace('0x', '') + ); + assert.equal(expectedOutput.color, actualOutput.color.toLowerCase()); + assert.equal(expectedOutput.data, actualOutput.data); + if (actualOutput.valueOrId.toHexString) { + assert.equal( + expectedOutput.valueOrId, + utils.hexZeroPad(actualOutput.valueOrId.toHexString(), 32) + ); + } else { + assert.equal( + expectedOutput.valueOrId, + actualOutput.valueOrId + ); + } + }); +}; + +TokenBagHelpers.emptyOutput = () => { + return { + owner: ZERO_ADDRESS, + valueOrId: 0x0, + data: ZERO_HASH, + color: ZERO_ADDRESS, + }; +}; + +TokenBagHelpers.emptyTokenBag = () => { + return promote({ bag: Array.apply(null, Array(16)).map(TokenBagHelpers.emptyOutput) }); +}; + +TokenBagHelpers.padTokenBag = (tokenBag) => { + while(tokenBag.length < 16) { + tokenBag.push(TokenBagHelpers.emptyOutput()); + } + return promote({ bag: tokenBag }); +}; + +function promote (tokenBag) { + + const bn = (hex) => { + return new BN(hex.replace('0x', ''), 16); + }; + + function balanceOf (color, address) { + const tokens = this.bag.filter(o => o.color === color && o.owner === address); + return tokens.length === 0 ? 0 : tokens[0].valueOrId; + } + + function readData (color, tokenId) { + const tokens = this.bag.filter(o => o.color === color && o.valueOrId === tokenId); + return tokens.length === 0 ? ZERO_HASH : tokens[0].data; + } + + function transfer (color, from, to, value) { + let source; + + this.bag.forEach(o => { + if (o.color === color && o.owner === from) { + source = o; + } + }); + if (!source) return false; + if (bn(source.valueOrId).lt(bn(value))) return false; + + let dest; + this.bag.forEach(o => { + if (o.color === color && o.owner === to) { + dest = o; + } + }); + + if (!dest) { + this.bag.forEach(o => { + if (!dest && o.owner === ZERO_ADDRESS) { + o.owner = to; + o.color = color; + dest = o; + } + }); + } + + if (!dest) return false; + + source.valueOrId = '0x' + bn(source.valueOrId).sub(bn(value)).toString(16, 64); + dest.valueOrId = '0x' + bn(dest.valueOrId).add(bn(value)).toString(16, 64); + return true; + } + + tokenBag.balanceOf = balanceOf; + tokenBag.readData = readData; + tokenBag.transfer = transfer; + return tokenBag; +} + +module.exports = TokenBagHelpers; diff --git a/test/utils/stepper.js b/test/utils/stepper.js index ccfefbbf..759beb1b 100644 --- a/test/utils/stepper.js +++ b/test/utils/stepper.js @@ -3,6 +3,7 @@ const { getCode } = require('./../helpers/utils'); const { Constants, HydratedRuntime } = require('./../../utils/'); const fixtures = require('./../fixtures/runtime'); +const { assertTokenBagEqual } = require('../helpers/tokenBag.js'); const assert = require('assert'); @@ -17,6 +18,7 @@ describe('JS Stepper', function () { const stack = fixture.stack || []; const mem = fixture.memory || ''; const data = fixture.data || ''; + const tokenBag = fixture.tokenBag || false; const gasLimit = fixture.gasLimit || Constants.BLOCK_GAS_LIMIT; const blockGasLimit = fixture.gasLimit || Constants.BLOCK_GAS_LIMIT; const gasRemaining = typeof fixture.gasRemaining !== 'undefined' ? fixture.gasRemaining : gasLimit; @@ -29,6 +31,7 @@ describe('JS Stepper', function () { blockGasLimit, gasRemaining, pc, + tokenBag, }; const steps = await stepper.run(args); const res = steps[steps.length - 1]; @@ -39,6 +42,9 @@ describe('JS Stepper', function () { if (fixture.result.memory) { assert.deepEqual(res.mem, fixture.result.memory, 'mem'); } + if (fixture.result.tokenBag) { + assertTokenBagEqual(res.tokenBag, fixture.result.tokenBag); + } if (fixture.result.gasUsed !== undefined) { assert.equal(gasRemaining - res.gasRemaining, fixture.result.gasUsed, 'gasUsed'); } diff --git a/utils/EVMRuntime.js b/utils/EVMRuntime.js index 43a54658..530eb462 100644 --- a/utils/EVMRuntime.js +++ b/utils/EVMRuntime.js @@ -6,6 +6,8 @@ const BN = utils.BN; const OP = require('./constants'); const OPCODES = require('./Opcodes'); +const { emptyTokenBag } = require('../test/helpers/tokenBag.js'); + const PRECOMPILED = { '1': require('./precompiled/01-ecrecover.js'), '2': require('./precompiled/02-sha256.js'), @@ -162,6 +164,7 @@ module.exports = class EVMRuntime { stopped: false, returnValue: Buffer.alloc(0), validJumps: {}, + tokenBag: obj.tokenBag || emptyTokenBag(), }; const len = runState.code.length; @@ -213,7 +216,7 @@ module.exports = class EVMRuntime { return runState; } - async run ({ code, data, stack, mem, gasLimit, gasRemaining, pc, stepCount }) { + async run ({ code, data, stack, mem, gasLimit, gasRemaining, pc, stepCount, tokenBag }) { data = data || '0x'; if (Array.isArray(code)) { @@ -235,6 +238,7 @@ module.exports = class EVMRuntime { mem, stack, gasRemaining, + tokenBag, }); stepCount = stepCount | 0; @@ -825,6 +829,44 @@ module.exports = class EVMRuntime { } async handleCALL (runState) { + + const gasLimit = runState.stack.pop(); + const toAddress = runState.stack.pop(); + const value = runState.stack.pop(); + const inOffset = runState.stack.pop(); + const inLength = runState.stack.pop(); + const outOffset = runState.stack.pop(); + const outLength = runState.stack.pop(); + + const data = this.memLoad(runState, inOffset, inLength); + const funcSig = this.getFuncSig(data); + + if (funcSig === OP.FUNCSIG_TRANSFER) { + this.subMemUsage(runState, outOffset, outLength); + + if (gasLimit.gt(runState.gasLeft)) { + gasLimit = new BN(runState.gasLeft); + } + + const color = '0x' + toAddress.toString(16, 40); + const to = utils.bufferToHex(data.slice(4, 24)); + const from = utils.bufferToHex(runState.caller); + const amount = utils.bufferToHex(data.slice(36, 68)); + + const success = runState.tokenBag.transfer(color, from, to, amount); + + if (success) { + runState.stack.push(new BN(OP.CALLISH_SUCCESS)); + const returnValue = utils.setLengthRight(utils.toBuffer('0x01'), 32); + this.memStore(runState, outOffset, returnValue, new BN(0), outLength, 32); + runState.returnValue = returnValue; + } else { + runState.returnValue = Buffer.alloc(0); + runState.stack.push(new BN(OP.CALLISH_FAIL)); + return; + } + } + throw new VmError(ERROR.INSTRUCTION_NOT_SUPPORTED); } @@ -837,17 +879,17 @@ module.exports = class EVMRuntime { } async handleSTATICCALL (runState) { - const target = runState.stack[runState.stack.length - 2] || new BN(0xff); - - if (target.gten(0) && target.lten(8)) { - let gasLimit = runState.stack.pop(); - const toAddress = runState.stack.pop(); - const inOffset = runState.stack.pop(); - const inLength = runState.stack.pop(); - const outOffset = runState.stack.pop(); - const outLength = runState.stack.pop(); - const data = this.memLoad(runState, inOffset, inLength); - + let gasLimit = runState.stack.pop(); + const toAddress = runState.stack.pop() || new BN(0xff); + const inOffset = runState.stack.pop(); + const inLength = runState.stack.pop(); + const outOffset = runState.stack.pop(); + const outLength = runState.stack.pop(); + + const data = this.memLoad(runState, inOffset, inLength); + const funcSig = this.getFuncSig(data); + + if (toAddress.gten(0) && toAddress.lten(8)) { this.subMemUsage(runState, outOffset, outLength); if (gasLimit.gt(runState.gasLeft)) { @@ -863,12 +905,31 @@ module.exports = class EVMRuntime { this.subGas(runState, r.gasUsed); this.memStore(runState, outOffset, r.returnValue, new BN(0), outLength, true); return; + } else if (funcSig === OP.FUNCSIG_BALANCEOF) { + const color = '0x' + toAddress.toString(16, 40); + const addr = utils.bufferToHex(data.slice(4, 24)); + const balance = runState.tokenBag.balanceOf(color, addr); + const returnValue = utils.setLengthLeft(utils.toBuffer(balance), 32); + + this.memStore(runState, outOffset, returnValue, new BN(0), outLength, 32); + runState.stack.push(new BN(OP.CALLISH_SUCCESS)); + runState.returnValue = returnValue; + return; + } else if (funcSig === OP.FUNCSIG_READATA) { + const color = '0x' + toAddress.toString(16, 40); + const tokenId = utils.bufferToHex(data.slice(4, 36)); + const returnValue = utils.toBuffer(runState.tokenBag.readData(color, tokenId)); + + this.memStore(runState, outOffset, returnValue, new BN(0), outLength, 32); + runState.stack.push(new BN(OP.CALLISH_SUCCESS)); + runState.returnValue = returnValue; + return; } // TODO: remove this and throw first, sync behaviour with contracts runState.returnValue = Buffer.alloc(0); runState.stack = runState.stack.slice(0, runState.stack.length - 6); - runState.stack.push(new BN(0)); + runState.stack.push(new BN(OP.CALLISH_FAIL)); throw new VmError(ERROR.INSTRUCTION_NOT_SUPPORTED); } @@ -986,4 +1047,11 @@ module.exports = class EVMRuntime { return Buffer.from(loaded); } + + getFuncSig (data) { + if (data.length < 3) { + return 0x00000000; + } + return utils.bufferToHex(data.slice(0, 4)); + } }; diff --git a/utils/EthereumRuntimeAdapter.js b/utils/EthereumRuntimeAdapter.js index 771cf713..5f2f4379 100644 --- a/utils/EthereumRuntimeAdapter.js +++ b/utils/EthereumRuntimeAdapter.js @@ -1,7 +1,9 @@ 'use strict'; -const { BLOCK_GAS_LIMIT } = require('./constants'); +const { BLOCK_GAS_LIMIT, ZERO_ADDRESS, ZERO_HASH } = require('./constants'); const ethers = require('ethers'); +const { emptyTokenBag } = require('../test/helpers/tokenBag.js'); + module.exports = class EthereumRuntimeAdapter { constructor (runtimeContract) { @@ -25,7 +27,7 @@ module.exports = class EthereumRuntimeAdapter { } execute ( - { code, data, pc, stepCount, gasRemaining, gasLimit, stack, mem }, + { code, data, pc, stepCount, gasRemaining, gasLimit, stack, mem, tokenBag }, payable ) { return (payable ? this.payableRuntimeContract.execute : this.runtimeContract.execute)( @@ -39,6 +41,7 @@ module.exports = class EthereumRuntimeAdapter { gasLimit: gasLimit || BLOCK_GAS_LIMIT, stack: stack || [], mem: mem || [], + tokenBag: tokenBag || emptyTokenBag(), returnData: '0x', } ); diff --git a/utils/HydratedRuntime.js b/utils/HydratedRuntime.js index c1c51050..a378bdce 100644 --- a/utils/HydratedRuntime.js +++ b/utils/HydratedRuntime.js @@ -86,6 +86,7 @@ module.exports = class HydratedRuntime extends EVMRuntime { pc: pc, errno: runState.errno, gasRemaining: runState.gasLeft.toNumber(), + tokenBag: runState.tokenBag, }; this.calculateMemProof(runState, step); diff --git a/utils/constants.js b/utils/constants.js index 7496b157..86e4187b 100644 --- a/utils/constants.js +++ b/utils/constants.js @@ -188,4 +188,12 @@ module.exports = { BLOCK_GAS_LIMIT: '0x0fffffffffffff', ZERO_HASH: '0x0000000000000000000000000000000000000000000000000000000000000000', + ZERO_ADDRESS: '0x0000000000000000000000000000000000000000', + + FUNCSIG_TRANSFER: '0xa9059cbb', + FUNCSIG_BALANCEOF: '0x70a08231', + FUNCSIG_READATA: '0x37ebbc03', + + CALLISH_SUCCESS: 1, + CALLISH_FAIL: 0, };