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.
475 lines
14 KiB
475 lines
14 KiB
const web3Utils = require("web3-utils");
|
|
|
|
class Injector {
|
|
constructor(viaIR){
|
|
this.viaIR = viaIR;
|
|
this.hashCounter = 0;
|
|
this.modifierCounter = 0;
|
|
this.modifiers = {};
|
|
}
|
|
|
|
_split(contract, injectionPoint){
|
|
return {
|
|
start: contract.instrumented.slice(0, injectionPoint),
|
|
end: contract.instrumented.slice(injectionPoint)
|
|
}
|
|
}
|
|
|
|
_getInjectable(id, hash, type){
|
|
switch(type){
|
|
case 'and-true':
|
|
return ` && ${this._getTrueMethodIdentifier(id)}(${hash}))`;
|
|
case 'or-false':
|
|
return ` || ${this._getFalseMethodIdentifier(id)}(${hash}))`;
|
|
case 'modifier':
|
|
return ` ${this._getModifierIdentifier(id)} `;
|
|
default:
|
|
return (this.viaIR)
|
|
? `${this._getAbiEncodeStatementHash(hash)} /* ${type} */ \n`
|
|
: `${this._getDefaultMethodIdentifier(id)}(${hash}); /* ${type} */ \n`;
|
|
}
|
|
}
|
|
|
|
_getHash(id) {
|
|
this.hashCounter++;
|
|
return web3Utils.keccak256(`${id}:${this.hashCounter}`).slice(0,18);
|
|
}
|
|
|
|
// Method returns void
|
|
_getDefaultMethodIdentifier(id){
|
|
return `c_${web3Utils.keccak256(id).slice(2,10)}`
|
|
}
|
|
|
|
// Method returns boolean: true
|
|
_getTrueMethodIdentifier(id){
|
|
return `c_true${web3Utils.keccak256(id).slice(2,10)}`
|
|
}
|
|
|
|
// Method returns boolean: false
|
|
_getFalseMethodIdentifier(id){
|
|
return `c_false${web3Utils.keccak256(id).slice(2,10)}`
|
|
}
|
|
|
|
_getModifierIdentifier(id){
|
|
return `c_mod${web3Utils.keccak256(id).slice(2,10)}`
|
|
}
|
|
|
|
// Way to get hash on the stack with viaIR (which seems to ignore abi.encode (v0.8.24))
|
|
// If you visiting solidity-coverage & understand what's happening here, please do not mention
|
|
// this obscure and irrelevant optimization opportunity in the issues at ethereum/solidity
|
|
_getAbiEncodeStatementHash(hash){
|
|
return `abi.encode(${hash}); `
|
|
}
|
|
|
|
_getAbiEncodeStatementVar(hash){
|
|
return `abi.encode(c__${hash}); `
|
|
}
|
|
|
|
_getInjectionComponents(contract, injectionPoint, id, type){
|
|
const { start, end } = this._split(contract, injectionPoint);
|
|
const hash = this._getHash(id)
|
|
const injectable = this._getInjectable(id, hash, type);
|
|
|
|
return {
|
|
start: start,
|
|
end: end,
|
|
hash: hash,
|
|
injectable: injectable
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates an instrumentation fn definition for contract scoped methods.
|
|
* Declared once per contract.
|
|
* @param {String} id
|
|
* @return {String}
|
|
*/
|
|
_getDefaultMethodDefinition(id){
|
|
const hash = web3Utils.keccak256(id).slice(2,10);
|
|
const method = this._getDefaultMethodIdentifier(id);
|
|
|
|
return (this.viaIR)
|
|
? ``
|
|
: `\nfunction ${method}(bytes8 c__${hash}) internal pure {}\n`;
|
|
}
|
|
|
|
/**
|
|
* Generates an instrumentation fn definition for file scoped methods.
|
|
* Declared once per file. (Has no visibility modifier)
|
|
* @param {String} id
|
|
* @return {String}
|
|
*/
|
|
_getFileScopedHashMethodDefinition(id, contract){
|
|
const hash = web3Utils.keccak256(id).slice(2,10);
|
|
const method = this._getDefaultMethodIdentifier(id);
|
|
const abi = this._getAbiEncodeStatementVar(hash);
|
|
|
|
return (this.viaIR)
|
|
? `\nfunction ${method}(bytes8 c__${hash}) pure { ${abi} }\n`
|
|
: `\nfunction ${method}(bytes8 c__${hash}) pure {}\n`;
|
|
}
|
|
|
|
/**
|
|
* Generates a solidity statement injection defining a method
|
|
* *which returns boolean true* to pass instrumentation hash to.
|
|
* @param {String} fileName
|
|
* @return {String} ex: bytes32[1] memory _sc_82e0891
|
|
*/
|
|
_getTrueMethodDefinition(id){
|
|
const hash = web3Utils.keccak256(id).slice(2,10);
|
|
const method = this._getTrueMethodIdentifier(id);
|
|
const abi = this._getAbiEncodeStatementVar(hash);
|
|
|
|
return (this.viaIR)
|
|
? `function ${method}(bytes8 c__${hash}) internal pure returns (bool){ ${abi} return true; }\n`
|
|
: `function ${method}(bytes8 c__${hash}) internal pure returns (bool){ return true; }\n`;
|
|
}
|
|
|
|
/**
|
|
* Generates a solidity statement injection defining a method
|
|
* *which returns boolean true* to pass instrumentation hash to.
|
|
* Declared once per file. (Has no visibility modifier)
|
|
* @param {String} fileName
|
|
* @return {String} ex: bytes32[1] memory _sc_82e0891
|
|
*/
|
|
_getFileScopeTrueMethodDefinition(id){
|
|
const hash = web3Utils.keccak256(id).slice(2,10);
|
|
const method = this._getTrueMethodIdentifier(id);
|
|
const abi = this._getAbiEncodeStatementVar(hash);
|
|
|
|
return (this.viaIR)
|
|
? `function ${method}(bytes8 c__${hash}) pure returns (bool){ ${abi} return true; }\n`
|
|
: `function ${method}(bytes8 c__${hash}) pure returns (bool){ return true; }\n`;
|
|
}
|
|
|
|
/**
|
|
* Generates a solidity statement injection defining a method
|
|
* *which returns boolean false* to pass instrumentation hash to.
|
|
* @param {String} fileName
|
|
* @return {String} ex: bytes32[1] memory _sc_82e0891
|
|
*/
|
|
_getFalseMethodDefinition(id){
|
|
const hash = web3Utils.keccak256(id).slice(2,10);
|
|
const method = this._getFalseMethodIdentifier(id);
|
|
const abi = this._getAbiEncodeStatementVar(hash);
|
|
|
|
return (this.viaIR)
|
|
? `function ${method}(bytes8 c__${hash}) internal pure returns (bool){ ${abi} return false; }\n`
|
|
: `function ${method}(bytes8 c__${hash}) internal pure returns (bool){ return false; }\n`;
|
|
}
|
|
|
|
/**
|
|
* Generates a solidity statement injection defining a method
|
|
* *which returns boolean false* to pass instrumentation hash to.
|
|
* Declared once per file. (Has no visibility modifier)
|
|
* @param {String} fileName
|
|
* @return {String} ex: bytes32[1] memory _sc_82e0891
|
|
*/
|
|
_getFileScopedFalseMethodDefinition(id){
|
|
const hash = web3Utils.keccak256(id).slice(2,10);
|
|
const method = this._getFalseMethodIdentifier(id);
|
|
const abi = this._getAbiEncodeStatementVar(hash);
|
|
|
|
return (this.viaIR)
|
|
? `function ${method}(bytes8 c__${hash}) pure returns (bool){ ${abi} return false; }\n`
|
|
: `function ${method}(bytes8 c__${hash}) pure returns (bool){ return false; }\n`;
|
|
}
|
|
|
|
_getModifierDefinitions(contractId, instrumentation){
|
|
let injection = '';
|
|
|
|
if (this.modifiers[contractId]){
|
|
|
|
for (const item of this.modifiers[contractId]){
|
|
injection += `modifier ${this._getModifierIdentifier(item.modifierId)}{ `;
|
|
|
|
let hash = this._getHash(item.modifierId);
|
|
let comment = `modifier-${item.condition}`;
|
|
let injectable = this._getInjectable(item.contractId, hash, comment);
|
|
|
|
// Process modifiers in the same step as `require` stmts in coverage.js
|
|
let type = (item.condition === 'pre') ? 'requirePre' : 'requirePost';
|
|
|
|
instrumentation[hash] = {
|
|
id: item.branchId,
|
|
type: type,
|
|
contractPath: item.fileName,
|
|
hits: 0
|
|
}
|
|
|
|
injection += `${injectable} _; }\n`;
|
|
}
|
|
}
|
|
|
|
return injection;
|
|
}
|
|
|
|
_cacheModifier(injection){
|
|
if (!this.modifiers[injection.contractId]) {
|
|
this.modifiers[injection.contractId] = [];
|
|
}
|
|
|
|
this.modifiers[injection.contractId].push(injection);
|
|
}
|
|
|
|
resetModifierMapping(){
|
|
this.modifiers = {};
|
|
}
|
|
|
|
injectLine(contract, fileName, injectionPoint, injection, instrumentation){
|
|
const type = 'line';
|
|
const { start, end } = this._split(contract, injectionPoint);
|
|
const id = `${fileName}:${injection.contractName}`;
|
|
|
|
const newLines = start.match(/\n/g);
|
|
const linecount = ( newLines || []).length + 1;
|
|
contract.runnableLines.push(linecount);
|
|
|
|
const hash = this._getHash(id)
|
|
const injectable = this._getInjectable(id, hash, type);
|
|
|
|
instrumentation[hash] = {
|
|
id: linecount,
|
|
type: type,
|
|
contractPath: fileName,
|
|
hits: 0
|
|
}
|
|
|
|
contract.instrumented = `${start}${injectable}${end}`;
|
|
}
|
|
|
|
injectStatement(contract, fileName, injectionPoint, injection, instrumentation) {
|
|
const type = 'statement';
|
|
const id = `${fileName}:${injection.contractName}`;
|
|
|
|
const {
|
|
start,
|
|
end,
|
|
hash,
|
|
injectable
|
|
} = this._getInjectionComponents(contract, injectionPoint, id, type);
|
|
|
|
instrumentation[hash] = {
|
|
id: injection.statementId,
|
|
type: type,
|
|
contractPath: fileName,
|
|
hits: 0
|
|
}
|
|
|
|
contract.instrumented = `${start}${injectable}${end}`;
|
|
};
|
|
|
|
injectFunction(contract, fileName, injectionPoint, injection, instrumentation){
|
|
const type = 'function';
|
|
const id = `${fileName}:${injection.contractName}`;
|
|
|
|
const {
|
|
start,
|
|
end,
|
|
hash,
|
|
injectable
|
|
} = this._getInjectionComponents(contract, injectionPoint, id, type);
|
|
|
|
instrumentation[hash] = {
|
|
id: injection.fnId,
|
|
type: type,
|
|
contractPath: fileName,
|
|
hits: 0
|
|
}
|
|
|
|
contract.instrumented = `${start}${injectable}${end}`;
|
|
}
|
|
|
|
injectBranch(contract, fileName, injectionPoint, injection, instrumentation){
|
|
const type = 'branch';
|
|
const id = `${fileName}:${injection.contractName}`;
|
|
|
|
const {
|
|
start,
|
|
end,
|
|
hash,
|
|
injectable
|
|
} = this._getInjectionComponents(contract, injectionPoint, id, type);
|
|
|
|
instrumentation[hash] = {
|
|
id: injection.branchId,
|
|
locationIdx: injection.locationIdx,
|
|
type: type,
|
|
contractPath: fileName,
|
|
hits: 0
|
|
}
|
|
|
|
contract.instrumented = `${start}${injectable}${end}`;
|
|
}
|
|
|
|
injectEmptyBranch(contract, fileName, injectionPoint, injection, instrumentation) {
|
|
const type = 'branch';
|
|
const id = `${fileName}:${injection.contractName}`;
|
|
|
|
const {
|
|
start,
|
|
end,
|
|
hash,
|
|
injectable
|
|
} = this._getInjectionComponents(contract, injectionPoint, id, type);
|
|
|
|
instrumentation[hash] = {
|
|
id: injection.branchId,
|
|
locationIdx: injection.locationIdx,
|
|
type: type,
|
|
contractPath: fileName,
|
|
hits: 0
|
|
}
|
|
|
|
contract.instrumented = `${start}else { ${injectable}}${end}`;
|
|
}
|
|
|
|
injectRequirePre(contract, fileName, injectionPoint, injection, instrumentation) {
|
|
const type = 'requirePre';
|
|
const id = `${fileName}:${injection.contractName}`;
|
|
|
|
const {
|
|
start,
|
|
end,
|
|
hash,
|
|
injectable
|
|
} = this._getInjectionComponents(contract, injectionPoint, id, type);
|
|
|
|
instrumentation[hash] = {
|
|
id: injection.branchId,
|
|
type: type,
|
|
contractPath: fileName,
|
|
hits: 0
|
|
}
|
|
|
|
contract.instrumented = `${start}${injectable}${end}`;
|
|
}
|
|
|
|
injectRequirePost(contract, fileName, injectionPoint, injection, instrumentation) {
|
|
const type = 'requirePost';
|
|
const id = `${fileName}:${injection.contractName}`;
|
|
|
|
const {
|
|
start,
|
|
end,
|
|
hash,
|
|
injectable
|
|
} = this._getInjectionComponents(contract, injectionPoint, id, type);
|
|
|
|
instrumentation[hash] = {
|
|
id: injection.branchId,
|
|
type: type,
|
|
contractPath: fileName,
|
|
hits: 0
|
|
}
|
|
|
|
contract.instrumented = `${start}${injectable}${end}`;
|
|
}
|
|
|
|
injectHashMethod(contract, fileName, injectionPoint, injection, instrumentation){
|
|
const start = contract.instrumented.slice(0, injectionPoint);
|
|
const end = contract.instrumented.slice(injectionPoint);
|
|
const id = `${fileName}:${injection.contractName}`;
|
|
|
|
const methodDefinition = (injection.isFileScoped)
|
|
? this._getFileScopedHashMethodDefinition(id)
|
|
: this._getDefaultMethodDefinition(id);
|
|
|
|
const trueMethodDefinition = (injection.isFileScoped)
|
|
? this._getFileScopeTrueMethodDefinition(id)
|
|
: this._getTrueMethodDefinition(id);
|
|
|
|
const falseMethodDefinition = (injection.isFileScoped)
|
|
? this._getFileScopedFalseMethodDefinition(id)
|
|
: this._getFalseMethodDefinition(id);
|
|
|
|
const modifierDefinition = (injection.isFileScoped)
|
|
? ""
|
|
: this._getModifierDefinitions(id, instrumentation);
|
|
|
|
contract.instrumented = `${start}` +
|
|
`${methodDefinition}` +
|
|
`${trueMethodDefinition}` +
|
|
`${falseMethodDefinition}` +
|
|
`${modifierDefinition}` +
|
|
`${end}`;
|
|
}
|
|
|
|
injectOpenParen(contract, fileName, injectionPoint, injection, instrumentation){
|
|
const start = contract.instrumented.slice(0, injectionPoint);
|
|
const end = contract.instrumented.slice(injectionPoint);
|
|
contract.instrumented = `${start}(${end}`;
|
|
}
|
|
|
|
injectAndTrue(contract, fileName, injectionPoint, injection, instrumentation){
|
|
const type = 'and-true';
|
|
const id = `${fileName}:${injection.contractName}`;
|
|
|
|
const {
|
|
start,
|
|
end,
|
|
hash,
|
|
injectable
|
|
} = this._getInjectionComponents(contract, injectionPoint, id, type);
|
|
|
|
instrumentation[hash] = {
|
|
id: injection.branchId,
|
|
locationIdx: injection.locationIdx,
|
|
type: type,
|
|
contractPath: fileName,
|
|
hits: 0
|
|
}
|
|
|
|
contract.instrumented = `${start}${injectable}${end}`;
|
|
}
|
|
|
|
injectOrFalse(contract, fileName, injectionPoint, injection, instrumentation){
|
|
const type = 'or-false';
|
|
const id = `${fileName}:${injection.contractName}`;
|
|
|
|
const {
|
|
start,
|
|
end,
|
|
hash,
|
|
injectable
|
|
} = this._getInjectionComponents(contract, injectionPoint, id, type);
|
|
|
|
instrumentation[hash] = {
|
|
id: injection.branchId,
|
|
locationIdx: injection.locationIdx,
|
|
type: type,
|
|
contractPath: fileName,
|
|
hits: 0
|
|
}
|
|
|
|
contract.instrumented = `${start}${injectable}${end}`;
|
|
}
|
|
|
|
injectModifier(contract, fileName, injectionPoint, injection, instrumentation){
|
|
this.modifierCounter++;
|
|
|
|
const type = 'modifier';
|
|
const contractId = `${fileName}:${injection.contractName}`;
|
|
const modifierId = `${fileName}:${injection.contractName}:` +
|
|
`${injection.modifierName}:${injection.fnId}:` +
|
|
`${injection.condition}:${this.modifierCounter}`;
|
|
|
|
const {
|
|
start,
|
|
end,
|
|
hash,
|
|
injectable
|
|
} = this._getInjectionComponents(contract, injectionPoint, modifierId, type);
|
|
|
|
this._cacheModifier({
|
|
contractId,
|
|
modifierId,
|
|
fileName,
|
|
...injection
|
|
});
|
|
|
|
contract.instrumented = `${start}${injectable}${end}`;
|
|
}
|
|
};
|
|
|
|
module.exports = Injector;
|
|
|