diff --git a/gas-relayer/config/config.json b/gas-relayer/config/config.json index 672cf24..944c45a 100644 --- a/gas-relayer/config/config.json +++ b/gas-relayer/config/config.json @@ -12,7 +12,7 @@ "port": 8545 }, "blockchain": { - "account": "0x1847ab5a71eaa95315c3fc2d3dfb53b7e6e8f313" + "account": "0x47B9F15c410E2f254E5e065ca515379b090c63e1" }, "whisper": { "symKey": "0xd0d905c1c62b810b787141430417caf2b3f54cffadb395b7bb39fdeb8f17266b", @@ -51,7 +51,7 @@ "IdentityGasRelay": { "abiFile": "../abi/IdentityGasRelay.json", "isIdentity": true, - "factoryAddress": "0x6202a2b202Ccf859fd93Ecee33C2D20f20462836", + "factoryAddress": "0x9AA17f02d8f6Bc2553d06c60f99Bcc97Cd5E5A89", "kernelVerification": "isKernel(bytes32)", "allowedFunctions": [ { diff --git a/gas-relayer/src/contract-settings.js b/gas-relayer/src/contract-settings.js index 822857c..5a4e338 100644 --- a/gas-relayer/src/contract-settings.js +++ b/gas-relayer/src/contract-settings.js @@ -4,6 +4,7 @@ class ContractSettings { this.tokens = config.tokens; this.topics = []; this.contracts = config.contracts; + this.config = config; this.web3 = web3; this.events = eventEmitter; @@ -51,7 +52,7 @@ class ContractSettings { this.pendingToLoad--; if(this.pendingToLoad == 0) this.events.emit("setup:complete", this); }) - .catch(function(err){ + .catch((err) => { console.error("Invalid contract for " + contractName); console.error(err); process.exit(); @@ -92,7 +93,8 @@ class ContractSettings { // Obtaining strategy if(this.contracts[topicName].strategy){ - this.contracts[topicName].strategy = require(this.contracts[topicName].strategy); + const strategy = require(this.contracts[topicName].strategy); + this.contracts[topicName].strategy = new strategy(this.web3, this.config, this, this.contracts[topicName]); } this._obtainContractBytecode(topicName); diff --git a/gas-relayer/src/message-processor.js b/gas-relayer/src/message-processor.js index 3b5af47..380f629 100644 --- a/gas-relayer/src/message-processor.js +++ b/gas-relayer/src/message-processor.js @@ -1,8 +1,3 @@ -const md5 = require('md5'); -const erc20ABI = require('../abi/ERC20Token.json'); -const ganache = require("ganache-cli"); -const Web3 = require('web3'); - class MessageProcessor { constructor(config, settings, web3, kId){ @@ -26,10 +21,10 @@ class MessageProcessor { } } - async _validateInput(message, input){ + async _validateInput(message){ const contract = this.settings.getContractByTopic(message.topic); - if(!/^0x[0-9a-f]{40}$/i.test(input.address)){ + if(!/^0x[0-9a-f]{40}$/i.test(message.input.address)){ this._reply('Invalid address', message); return false; } @@ -39,14 +34,14 @@ class MessageProcessor { return false; } - if(!contract.functionSignatures.includes(input.functionName)){ + if(!contract.functionSignatures.includes(message.input.functionName)){ this._reply('Function not allowed', message); return false; } // Get code from address and compare it against the contract code if(!contract.isIdentity){ - const code = this.web3.utils.soliditySha3(await this.web3.eth.getCode(input.address)); + const code = this.web3.utils.soliditySha3(await this.web3.eth.getCode(message.input.address)); if(code != contract.code){ this._reply('Invalid contract code', message); return false; @@ -55,19 +50,6 @@ class MessageProcessor { return true; } - async _validateInstance(message, input){ - const contract = this.settings.getContractByTopic(message.topic); - const instanceCodeHash = this.web3.utils.soliditySha3(await this.web3.eth.getCode(input.address)); - const kernelVerifSignature = this.web3.utils.soliditySha3(contract.kernelVerification).slice(0, 10); - if(instanceCodeHash == null) return false; - - let verificationResult = await this.web3.eth.call({ - to: contract.factoryAddress, - data: kernelVerifSignature + instanceCodeHash.slice(2)}); - - return this.web3.eth.abi.decodeParameter('bool', verificationResult);; - } - _extractInput(message){ let obj = { address: null, @@ -77,7 +59,8 @@ class MessageProcessor { }; try { - let parsedObj = JSON.parse(this.web3.utils.toAscii(message)); + let parsedObj = JSON.parse(this.web3.utils.toAscii(message.payload)); + obj.address = parsedObj.address; obj.functionName = parsedObj.encodedFunctionCall.slice(0, 10); obj.functionParameters = "0x" + parsedObj.encodedFunctionCall.slice(10); @@ -86,109 +69,61 @@ class MessageProcessor { console.error("Couldn't parse " + message); } - return obj; - } - - _obtainParametersFunc(contract, input){ - const parameterList = this.web3.eth.abi.decodeParameters(contract.allowedFunctions[input.functionName].inputs, input.functionParameters); - return function(parameterName){ - return parameterList[parameterName]; - } + message.input = obj; } + /* _getFactor(input, contract, gasToken){ if(contract.allowedFunctions[input.functionName].isToken){ return this.web3.utils.toBN(this.settings.getToken(gasToken).pricePlugin.getFactor()); } else { return this.web3.utils.toBN(1); } - } - - async getBalance(token, input, gasToken){ - // Determining balances of token used - if(token.symbol == "ETH"){ - return new this.web3.utils.BN(await this.web3.eth.getBalance(input.address)); - } else { - const Token = new this.web3.eth.Contract(erc20ABI.abi); - Token.options.address = gasToken; - return new this.web3.utils.BN(await Token.methods.balanceOf(input.address).call()); - } - } - - async _estimateGas(input, gasLimit){ - let web3Sim = new Web3(ganache.provider({ - fork: `${this.config.node.ganache.protocol}://${this.config.node.ganache.host}:${this.config.node.ganache.port}`, - locked: false, - gasLimit: 10000000 - })); - - let simAccounts = await web3Sim.eth.getAccounts(); - - let simulatedReceipt = await web3Sim.eth.sendTransaction({ - from: simAccounts[0], - to: input.address, - value: 0, - data: input.payload, - gasLimit: gasLimit * 0.95 // 95% of current chain latest gas block limit - - }); - - return web3Sim.utils.toBN(simulatedReceipt.gasUsed); - } + } */ async process(error, message){ if(error){ console.error(error); } else { - - let input = this._extractInput(message.payload); + this._extractInput(message); const contract = this.settings.getContractByTopic(message.topic); - console.info("Processing request to: %s, %s", input.address, input.functionName); + console.info("Processing request to: %s, %s", message.input.address, message.input.functionName); - if(!await this._validateInput(message, input)) return; // TODO Log + if(!await this._validateInput(message)) return; // TODO Log if(contract.strategy){ - let validationResult = contract.strategy(message, input) - if(validationResult.success){ + let validationResult = await contract.strategy.execute(message); + if(!validationResult.success){ return this._reply(validationResult.message, message); } } - const latestBlock = await this.web3.eth.getBlock("latest"); - let estimatedGas = 0; - try { - estimatedGas = await this._estimateGas(input, latestBlock.gasLimit); - if(gasLimit.lt(estimatedGas)) { - return this._reply("Gas limit below estimated gas", message); - } - } catch(exc){ - if(exc.message.indexOf("revert") > -1) - return this._reply("Transaction will revert", message); - } - let p = { from: this.config.node.blockchain.account, - to: input.address, + to: message.input.address, value: 0, - data: input.payload, - gas: gasLimit.toString(), + data: message.input.payload, gasPrice: this.config.gasPrice }; - this.web3.eth.sendTransaction(p) - .then((receipt) => { - return this._reply("Transaction mined;" - + receipt.transactionHash - + ';' - + JSON.stringify(receipt) - , message); - }).catch((err) => { - this._reply("Couldn't mine transaction: " + err.message, message); - // TODO log this? - console.error(err); - }); + this.web3.eth.estimateGas(p) + .then((estimatedGas) => { + p.gas = parseInt(estimatedGas * 1.1) + return this.web3.eth.sendTransaction(p); + }) + .then((receipt) => { + return this._reply("Transaction mined;" + + receipt.transactionHash + + ';' + + JSON.stringify(receipt) + , message); + }).catch((err) => { + this._reply("Couldn't mine transaction: " + err.message, message); + // TODO log this? + console.error(err); + }); } } } diff --git a/gas-relayer/src/strategy/IdentityGasRelay.js b/gas-relayer/src/strategy/IdentityGasRelay.js index 48cef5d..ecbf30a 100644 --- a/gas-relayer/src/strategy/IdentityGasRelay.js +++ b/gas-relayer/src/strategy/IdentityGasRelay.js @@ -1,55 +1,122 @@ -const identityStrategy = () => { +const erc20ABI = require('../../abi/ERC20Token.json'); +const ganache = require("ganache-cli"); +const Web3 = require('web3'); +class IdentityStrategy { - -/* -// TODO extract this. Determine strategy depending on contract - - if(contract.isIdentity){ - let validInstance = await this._validateInstance(message, input); - if(!validInstance){ - return this._reply("Invalid identity instance", message); - } - } - - const params = this._obtainParametersFunc(contract, input); - - const token = this.settings.getToken(params('_gasToken')); - if(token == undefined) - return this._reply("Token not allowed", message); - - const gasPrice = this.web3.utils.toBN(params('_gasPrice')); - const gasLimit = this.web3.utils.toBN(params('_gasLimit')); - - // Determine if enough balance for baseToken - if(contract.allowedFunctions[input.functionName].isToken){ - const Token = new this.web3.eth.Contract(erc20ABI); - Token.options.address = params('_baseToken'); - const baseToken = new this.web3.utils.BN(await Token.methods.balanceOf(input.address).call()); - if(balance.lt(this.web3.utils.BN(params('_value')))){ - this._reply("Identity has not enough balance for specified value", message); - return; - } - } - - const gasToken = params('_gasToken'); - const balance = await this.getBalance(token, input, gasToken); - - if(balance.lt(this.web3.utils.toBN(gasPrice.mul(gasLimit)))) { - this._reply("Identity has not enough tokens for gasPrice*gasLimit", message); - return; - } - - - */ - - - - - return { - success: true, - message: "Test" + constructor(web3, config, settings, contract){ + this.web3 = web3; + this.settings = settings; + this.contract = contract; + this.config = config; } + + _obtainParametersFunc(message){ + const parameterList = this.web3.eth.abi.decodeParameters(this.contract.allowedFunctions[message.input.functionName].inputs, message.input.functionParameters); + return function(parameterName){ + return parameterList[parameterName]; + } + } + + async _validateInstance(message){ + const instanceCodeHash = this.web3.utils.soliditySha3(await this.web3.eth.getCode(message.input.address)); + const kernelVerifSignature = this.web3.utils.soliditySha3(this.contract.kernelVerification).slice(0, 10); + if(instanceCodeHash == null) return false; + + let verificationResult = await this.web3.eth.call({ + to: this.contract.factoryAddress, + data: kernelVerifSignature + instanceCodeHash.slice(2)}); + + return this.web3.eth.abi.decodeParameter('bool', verificationResult);; + } + + async getBalance(token, message, gasToken){ + // Determining balances of token used + if(token.symbol == "ETH"){ + return new this.web3.utils.BN(await this.web3.eth.getBalance(message.input.address)); + } else { + const Token = new this.web3.eth.Contract(erc20ABI.abi); + Token.options.address = gasToken; + return new this.web3.utils.BN(await Token.methods.balanceOf(message.input.address).call()); + } + } + + async _estimateGas(message, gasLimit){ + let web3Sim = new Web3(ganache.provider({ + fork: `${this.config.node.ganache.protocol}://${this.config.node.ganache.host}:${this.config.node.ganache.port}`, + locked: false, + gasLimit: 10000000 + })); + + let simAccounts = await web3Sim.eth.getAccounts(); + + let simulatedReceipt = await web3Sim.eth.sendTransaction({ + from: simAccounts[0], + to: message.input.address, + value: 0, + data: message.input.payload, + gasLimit: gasLimit * 0.95 // 95% of current chain latest gas block limit + + }); + + return web3Sim.utils.toBN(simulatedReceipt.gasUsed); + } + + async execute(message){ + + if(this.contract.isIdentity){ + let validInstance = await this._validateInstance(message); + if(!validInstance){ + return { success: false, message: "Invalid identity instance" }; + } + } + + const params = this._obtainParametersFunc(message); + + // Verifying if token is allowed + const token = this.settings.getToken(params('_gasToken')); + if(token == undefined) + return { success: false, message: "Token not allowed" }; + + + // Determine if enough balance for baseToken + const gasPrice = this.web3.utils.toBN(params('_gasPrice')); + const gasLimit = this.web3.utils.toBN(params('_gasLimit')); + if(this.contract.allowedFunctions[message.input.functionName].isToken){ + const Token = new this.web3.eth.Contract(erc20ABI); + Token.options.address = params('_baseToken'); + const baseToken = new this.web3.utils.BN(await Token.methods.balanceOf(message.input.address).call()); + if(balance.lt(this.web3.utils.BN(params('_value')))){ + return { success: false, message: "Identity has not enough balance for specified value" }; + } + } + + // gasPrice * limit calculation + const gasToken = params('_gasToken'); + const balance = await this.getBalance(token, message, gasToken); + if(balance.lt(this.web3.utils.toBN(gasPrice.mul(gasLimit)))) { + return { success: false, message: "Identity has not enough tokens for gasPrice*gasLimit"}; + } + + + const latestBlock = await this.web3.eth.getBlock("latest"); + let estimatedGas = 0; + try { + estimatedGas = await this._estimateGas(message, latestBlock.gasLimit); + if(gasLimit.lt(estimatedGas)) { + return { success: false, message: "Gas limit below estimated gas" }; + } + } catch(exc){ + if(exc.message.indexOf("revert") > -1) + return { success: false, message: "Transaction will revert" }; + } + + return { + success: true, + message: "Test" + }; + } + } -module.exports = identityStrategy; \ No newline at end of file +module.exports = IdentityStrategy; \ No newline at end of file