0xweb
Version:
Contract package manager and other web3 tools
368 lines (352 loc) • 14.3 kB
text/typescript
import { $is } from '@dequanto/utils/$is';
import { EvmBytecode } from '../EvmBytecode';
import Opcode from '../interfaces/IOpcode';
import stringify from '../utils/stringify';
const updateCallDataLoad = (item: any, types: any) => {
for (const i in item) {
if (item.hasOwnProperty(i)) {
if (
typeof item[i] === 'object' &&
item[i].name === 'CALLDATALOAD' &&
$is.BigInt(item[i].location)
) {
const argNumber = item[i].location
.subtract(4)
.divide(32)
.toString();
item[i].type = types[argNumber];
}
if (typeof item[i] === 'object') {
updateCallDataLoad(item[i], types);
}
}
}
};
const findReturns = (item: any) => {
const returns = [];
for (const i in item) {
if (item.hasOwnProperty(i)) {
if (
typeof item[i] === 'object' &&
item[i].name === 'RETURN' &&
item[i].items &&
item[i].items.length > 0
) {
returns.push(item[i].items);
}
if (typeof item[i] === 'object') {
const deepReturns: any = findReturns(item[i]);
if (deepReturns.length > 0) {
returns.push(...deepReturns);
}
}
}
}
return returns;
};
export class TopLevelFunction {
readonly name: string;
readonly type?: string;
readonly hash: any;
readonly gasUsed: number;
readonly payable: boolean;
readonly visibility: string;
readonly constant: boolean;
readonly items: any;
readonly returns: any;
constructor(state: EvmBytecode, items: any, hash: any, gasUsed: number) {
this.name = 'Function';
this.hash = hash;
this.gasUsed = gasUsed;
this.items = items;
this.payable = true;
this.visibility = 'public';
this.constant = false;
this.returns = [];
if (
this.items.length > 0 &&
this.items[0] instanceof REQUIRE &&
this.items[0].condition.name === 'ISZERO' &&
this.items[0].condition.item.name === 'CALLVALUE'
) {
this.payable = false;
this.items.shift();
}
if (this.items.length === 1 && this.items[0].name === 'RETURN') {
this.constant = true;
}
if (this.hash in state.store.functionHashes) {
const functionName = (state.store.functionHashes)[this.hash].split('(')[0];
const argumentTypes = (state.store.functionHashes)[this.hash]
.replace(functionName, '')
.substr(1)
.slice(0, -1)
.split(',');
if (
argumentTypes.length > 1 ||
(argumentTypes.length === 1 && argumentTypes[0] !== '')
) {
this.items.forEach((item: any) => updateCallDataLoad(item, argumentTypes));
}
}
const returns: any = [];
this.items.forEach((item: any) => {
const deepReturns = findReturns(item);
if (deepReturns.length > 0) {
returns.push(...deepReturns);
}
});
if (
returns.length > 0 &&
returns.every(
(returnItem: any) =>
returnItem.length === returns[0].length &&
returnItem.map((item: any) => item.type).join('') ===
returns[0].map((item: any) => item.type).join('')
)
) {
returns[0].forEach((item: any) => {
if ($is.BigInt(item)) {
this.returns.push('uint256');
} else if (item.type) {
this.returns.push(item.type);
} else {
this.returns.push('unknown');
}
});
} else if (returns.length > 0) {
this.returns.push('<unknown>');
}
}
}
export class Variable {
readonly name: string;
readonly label: string | false;
readonly types: any;
constructor(label: string | false, types: any) {
this.name = 'Variable';
this.label = label;
this.types = types;
}
}
export class REQUIRE {
readonly name: string;
readonly type?: string;
readonly wrapped: boolean;
readonly condition: any;
constructor(condition: any) {
this.name = 'REQUIRE';
this.wrapped = true;
this.condition = condition;
}
toString() {
return 'require(' + stringify(this.condition) + ');';
}
}
export class JUMPI {
readonly name: string;
readonly type?: string;
readonly wrapped: boolean;
readonly condition: any;
readonly location: any;
readonly valid: boolean;
readonly true?: any;
readonly false?: any;
readonly payable?: boolean;
constructor(condition: any, location: any, ifTrue?: any, ifFalse?: any, skipped?: boolean) {
this.name = 'JUMPI';
this.wrapped = true;
this.condition = condition;
this.location = location;
if (skipped) {
this.valid = true;
} else if (ifTrue && ifFalse) {
this.valid = true;
this.true = ifTrue;
this.false = ifFalse;
if (
this.true.length >= 1 &&
this.true[0] instanceof REQUIRE &&
this.true[0].condition.name === 'ISZERO' &&
this.true[0].condition.item.name === 'CALLVALUE'
) {
this.payable = false;
this.true.shift();
} else {
this.payable = true;
}
} else {
this.valid = false;
}
}
toString() {
if (this.valid && this.true && this.false) {
return stringify(this.condition);
} else if (this.valid) {
return 'if' + stringify(this.condition) + ' goto(' + stringify(this.location) + ');';
} else {
return "revert(\"Bad jump destination\");";
}
}
}
export default (opcode: Opcode, state: EvmBytecode): void => {
const jumpLocation = state.stack.pop();
const jumpCondition = state.stack.pop();
const opcodes = state.getOpcodes();
if (!$is.BigInt(jumpLocation)) {
state.halted = true;
state.instructions.push(new JUMPI(jumpCondition, jumpLocation));
} else {
const jumpLocationData = opcodes.find((o: any) => o.pc === Number(jumpLocation));
if (!jumpLocationData || jumpLocationData.name !== 'JUMPDEST') {
state.halted = true;
state.instructions.push(new JUMPI(jumpCondition, jumpLocation));
} else if ($is.BigInt(jumpCondition)) {
const jumpIndex = opcodes.indexOf(jumpLocationData);
if (
jumpIndex >= 0 &&
jumpCondition !== 0n &&
!(opcode.pc + ':' + Number(jumpLocation) in state.jumps)
) {
state.jumps[opcode.pc + ':' + Number(jumpLocation)] = true;
state.pc = jumpIndex;
}
} else if (
!(opcode.pc + ':' + Number(jumpLocation) in state.jumps) &&
jumpCondition.name === 'SIG'
) {
const jumpIndex = opcodes.indexOf(jumpLocationData);
if (jumpIndex >= 0) {
const functionClone: any = state.clone();
functionClone.pc = jumpIndex;
const functionCloneTree = functionClone.parse();
state.functions[jumpCondition.hash] = new TopLevelFunction(
state,
functionCloneTree,
jumpCondition.hash,
functionClone.gasUsed
);
if (
jumpCondition.hash in state.store.functionHashes &&
functionCloneTree.length === 1 &&
functionCloneTree[0].name === 'RETURN' &&
functionCloneTree[0].items.every((item: any) => item.name === 'MappingLoad')
) {
functionCloneTree[0].items.forEach((item: any) => {
const fullFunction = (state.store.functionHashes)[jumpCondition.hash];
state.mappings[item.location].name = fullFunction.split('(')[0];
if (
item.structlocation &&
!state.mappings[item.location].structs.includes(item.structlocation)
) {
state.mappings[item.location].structs.push(item.structlocation);
}
});
delete state.functions[jumpCondition.hash];
} else if (
jumpCondition.hash in state.store.functionHashes &&
state.functions[jumpCondition.hash].items.length === 1 &&
state.functions[jumpCondition.hash].items[0].name === 'RETURN' &&
state.functions[jumpCondition.hash].items[0].items.length === 1 &&
state.functions[jumpCondition.hash].items[0].items[0].name === 'SLOAD' &&
$is.BigInt(
state.functions[jumpCondition.hash].items[0].items[0].location
)
) {
if (
!(
state.functions[jumpCondition.hash].items[0].items[0].location in
state.variables
)
) {
const fullFunction = (state.store.functionHashes)[jumpCondition.hash];
state.variables[
state.functions[jumpCondition.hash].items[0].items[0].location
] = new Variable(fullFunction.split('(')[0], []);
delete state.functions[jumpCondition.hash];
} else {
const fullFunction = (state.store.functionHashes)[jumpCondition.hash];
state.variables[
state.functions[jumpCondition.hash].items[0].items[0].location
].label = fullFunction.split('(')[0];
delete state.functions[jumpCondition.hash];
}
}
}
} else if (
!(opcode.pc + ':' + Number(jumpLocation) in state.jumps) &&
((jumpCondition.name === 'LT' &&
jumpCondition.left.name === 'CALLDATASIZE' &&
$is.BigInt(jumpCondition.right) &&
jumpCondition.right.equals(4)) ||
(jumpCondition.name === 'ISZERO' && jumpCondition.item.name === 'CALLDATASIZE'))
) {
const jumpIndex = opcodes.indexOf(jumpLocationData);
if (jumpIndex >= 0) {
state.halted = true;
const trueClone: any = state.clone();
trueClone.pc = jumpIndex;
const trueCloneTree = trueClone.parse();
const falseClone = state.clone();
falseClone.pc = state.pc + 1;
const falseCloneTree = falseClone.getInstructions();
if (
trueCloneTree.length > 0 &&
trueCloneTree.length === falseCloneTree.length &&
trueCloneTree[0].name !== 'REVERT' &&
trueCloneTree[0].name !== 'INVALID' &&
trueCloneTree.map((item: any) => stringify(item)).join('') ===
falseCloneTree.map((item: any) => stringify(item)).join('')
) {
state.functions[''] = new TopLevelFunction(
state,
trueCloneTree,
'',
trueCloneTree.gasUsed
);
} else if (
trueCloneTree.length > 0 &&
trueCloneTree[0].name !== 'REVERT' &&
trueCloneTree[0].name !== 'INVALID'
) {
state.instructions.push(
new JUMPI(jumpCondition, jumpLocation, trueCloneTree, falseCloneTree)
);
}
} else {
state.instructions.push(new JUMPI(jumpCondition, jumpLocation));
}
} else if (!(opcode.pc + ':' + Number(jumpLocation) in state.jumps)) {
const jumpIndex = opcodes.indexOf(jumpLocationData);
state.jumps[opcode.pc + ':' + Number(jumpLocation)] = true;
if (jumpIndex >= 0) {
state.halted = true;
const trueClone: any = state.clone();
trueClone.pc = jumpIndex;
const trueCloneTree = trueClone.parse();
const falseClone = state.clone();
falseClone.pc = state.pc + 1;
const falseCloneTree: any = falseClone.getInstructions();
if (
(falseCloneTree.length === 1 &&
'name' in falseCloneTree[0] &&
(falseCloneTree[0].name === 'REVERT' &&
falseCloneTree[0].items &&
falseCloneTree[0].items.length === 0)) ||
falseCloneTree[0].name === 'INVALID'
) {
state.instructions.push(new REQUIRE(jumpCondition));
state.instructions.push(...trueCloneTree);
} else {
state.instructions.push(
new JUMPI(jumpCondition, jumpLocation, trueCloneTree, falseCloneTree)
);
}
} else {
state.instructions.push(new JUMPI(jumpCondition, jumpLocation));
}
} else {
state.instructions.push(new JUMPI(jumpCondition, jumpLocation, null, null, true));
}
}
};