diff --git a/build/contracts.js b/build/contracts.js index 59f21634..c93024fb 100644 --- a/build/contracts.js +++ b/build/contracts.js @@ -1259,6 +1259,31 @@ module.exports = { "stateMutability": "nonpayable", "type": "constructor" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "batch", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "bool", + "name": "succeeded", + "type": "bool" + } + ], + "name": "BatchDelegated", + "type": "event" + }, { "anonymous": false, "inputs": [], @@ -1402,6 +1427,11 @@ module.exports = { "name": "account", "type": "address" }, + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, { "internalType": "address[]", "name": "to", @@ -1418,7 +1448,7 @@ module.exports = { "type": "bytes" } ], - "name": "delegateBatchFromAccount", + "name": "delegateBatch", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -1430,6 +1460,11 @@ module.exports = { "name": "account", "type": "address" }, + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, { "internalType": "address[]", "name": "to", @@ -1446,7 +1481,25 @@ module.exports = { "type": "bytes" } ], - "name": "delegateBatchWithoutGasPriceFromAccount", + "name": "delegateBatchWithGasPrice", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "batches", + "type": "bytes[]" + }, + { + "internalType": "bool", + "name": "revertOnFailure", + "type": "bool" + } + ], + "name": "delegateBatches", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -1469,11 +1522,6 @@ module.exports = { "internalType": "bytes[]", "name": "data", "type": "bytes[]" - }, - { - "internalType": "uint256", - "name": "gasPrice", - "type": "uint256" } ], "internalType": "struct Gateway.DelegatedBatch", @@ -1511,14 +1559,19 @@ module.exports = { "internalType": "bytes[]", "name": "data", "type": "bytes[]" + }, + { + "internalType": "uint256", + "name": "gasPrice", + "type": "uint256" } ], - "internalType": "struct Gateway.DelegatedBatchWithoutGasPrice", + "internalType": "struct Gateway.DelegatedBatchWithGasPrice", "name": "delegatedBatch", "type": "tuple" } ], - "name": "hashDelegatedBatchWithoutGasPrice", + "name": "hashDelegatedBatchWithGasPrice", "outputs": [ { "internalType": "bytes32", @@ -1551,7 +1604,7 @@ module.exports = { "constant": true } ], - "byteCodeHash": "0x47d2ba3882eb32577f973869e4cb02fb2331e1dfc7405a081d5453b5fd8c4305", + "byteCodeHash": "0x2a73271e1c51f75479b1071321e1d0fc93189989c1258f77bbe0a0cbe0f27d36", "typedDataDomainName": "Pillar Gateway", "typedDataDomainVersion": "1", "addresses": { diff --git a/package-lock.json b/package-lock.json index 9750a23b..b43a2d76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@etherspot/contracts", - "version": "1.0.0-alpha.30", + "version": "1.0.0-alpha.31", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 37a98688..9a53384f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@etherspot/contracts", - "version": "1.0.0-alpha.30", + "version": "1.0.0-alpha.31", "description": "ETHERspot contracts", "license": "MIT", "main": "./build/index.js", diff --git a/src/gateway/Gateway.sol b/src/gateway/Gateway.sol index 9f298290..2697c4c0 100644 --- a/src/gateway/Gateway.sol +++ b/src/gateway/Gateway.sol @@ -20,21 +20,21 @@ contract Gateway is Initializable, TypedDataContainer { uint256 nonce; address[] to; bytes[] data; - uint256 gasPrice; } - struct DelegatedBatchWithoutGasPrice { + struct DelegatedBatchWithGasPrice { uint256 nonce; address[] to; bytes[] data; + uint256 gasPrice; } bytes32 private constant DELEGATED_BATCH_TYPE_HASH = keccak256( - "DelegatedBatch(uint256 nonce,address[] to,bytes[] data,uint256 gasPrice)" + "DelegatedBatch(uint256 nonce,address[] to,bytes[] data)" ); - bytes32 private constant DELEGATED_BATCH_TYPE_HASH_WITHOUT_GAS_PRICE = keccak256( - "DelegatedBatchWithoutGasPrice(uint256 nonce,address[] to,bytes[] data)" + bytes32 private constant DELEGATED_BATCH_TYPE_HASH_WITH_GAS_PRICE = keccak256( + "DelegatedBatchWithGasPrice(uint256 nonce,address[] to,bytes[] data,uint256 gasPrice)" ); AccountOwnerRegistry public accountOwnerRegistry; @@ -109,24 +109,28 @@ contract Gateway is Initializable, TypedDataContainer { ); } - function delegateBatchFromAccount( + function delegateBatch( address account, + uint256 nonce, address[] memory to, bytes[] memory data, bytes memory senderSignature ) public { + require( + nonce > accountNonce[account] + ); + address sender = _hashPrimaryTypedData( _hashTypedData( - accountNonce[account], + nonce, to, - data, - tx.gasprice + data ) ).recoverAddress(senderSignature); - accountNonce[account] = accountNonce[account].add(1); + accountNonce[account] = nonce; _sendBatch( account, @@ -136,23 +140,29 @@ contract Gateway is Initializable, TypedDataContainer { ); } - function delegateBatchWithoutGasPriceFromAccount( + function delegateBatchWithGasPrice( address account, + uint256 nonce, address[] memory to, bytes[] memory data, bytes memory senderSignature ) public { + require( + nonce > accountNonce[account] + ); + address sender = _hashPrimaryTypedData( _hashTypedData( - accountNonce[account], + nonce, to, - data + data, + tx.gasprice ) ).recoverAddress(senderSignature); - accountNonce[account] = accountNonce[account].add(1); + accountNonce[account] = nonce; _sendBatch( account, @@ -203,14 +213,13 @@ contract Gateway is Initializable, TypedDataContainer { _hashTypedData( delegatedBatch.nonce, delegatedBatch.to, - delegatedBatch.data, - delegatedBatch.gasPrice + delegatedBatch.data ) ); } - function hashDelegatedBatchWithoutGasPrice( - DelegatedBatchWithoutGasPrice memory delegatedBatch + function hashDelegatedBatchWithGasPrice( + DelegatedBatchWithGasPrice memory delegatedBatch ) public view @@ -220,7 +229,8 @@ contract Gateway is Initializable, TypedDataContainer { _hashTypedData( delegatedBatch.nonce, delegatedBatch.to, - delegatedBatch.data + delegatedBatch.data, + delegatedBatch.gasPrice ) ); } @@ -299,7 +309,7 @@ contract Gateway is Initializable, TypedDataContainer { } return keccak256(abi.encode( - DELEGATED_BATCH_TYPE_HASH_WITHOUT_GAS_PRICE, + DELEGATED_BATCH_TYPE_HASH, nonce, keccak256(abi.encodePacked(to)), keccak256(abi.encodePacked(dataHashes)) @@ -323,7 +333,7 @@ contract Gateway is Initializable, TypedDataContainer { } return keccak256(abi.encode( - DELEGATED_BATCH_TYPE_HASH, + DELEGATED_BATCH_TYPE_HASH_WITH_GAS_PRICE, nonce, keccak256(abi.encodePacked(to)), keccak256(abi.encodePacked(dataHashes)), diff --git a/test/gateway/Gateway.js b/test/gateway/Gateway.js index a0318563..bf492c12 100644 --- a/test/gateway/Gateway.js +++ b/test/gateway/Gateway.js @@ -7,6 +7,7 @@ const { buildTypedData, hashTypedData, signTypedData, + getNextNonce, } = require('../utils'); const { GAS_PRICE, @@ -60,25 +61,20 @@ contract('Gateway', (addresses) => { type: 'bytes[]', name: 'data', }, - { - type: 'uint256', - name: 'gasPrice', - }, ], { nonce, to, data, - gasPrice: GAS_PRICE, }, ); - const buildDelegatedBatchWithoutGasPriceTypedData = ( + const buildDelegatedBatchWithGasPriceTypedData = ( nonce, to, data, ) => buildTypedData( gateway.address, - 'DelegatedBatchWithoutGasPrice', [ + 'DelegatedBatchWithGasPrice', [ { type: 'uint256', name: 'nonce', @@ -91,10 +87,15 @@ contract('Gateway', (addresses) => { type: 'bytes[]', name: 'data', }, + { + type: 'uint256', + name: 'gasPrice', + }, ], { nonce, to, data, + gasPrice: GAS_PRICE, }, ); @@ -236,7 +237,7 @@ contract('Gateway', (addresses) => { }); }); - context('delegateBatchFromAccount()', () => { + context('delegateBatch()', () => { const from = addresses.pop(); const account = addresses.pop(); const sender = addresses.pop(); @@ -248,17 +249,17 @@ contract('Gateway', (addresses) => { }); it('expect to send single call', async () => { + const nonce = getNextNonce(); const to = gatewayRecipientMock.address; const data = gatewayRecipientMock.contract.methods.emitContext() .encodeABI(); - const typedData = buildDelegatedBatchTypedData(0, [to], [data]); + const typedData = buildDelegatedBatchTypedData(nonce, [to], [data]); const senderSignature = await signTypedData(typedData, sender); - const output = await gateway.delegateBatchFromAccount(account, [to], [data], senderSignature, { + const output = await gateway.delegateBatch(account, nonce, [to], [data], senderSignature, { from, - gasPrice: GAS_PRICE, }); logGasUsage(output); @@ -272,24 +273,24 @@ contract('Gateway', (addresses) => { }); it('expect to revert on invalid signature', async () => { + const nonce = 0; // invalid nonce const to = gatewayRecipientMock.address; const data = gatewayRecipientMock.contract.methods.emitContext() .encodeABI(); - const typedData = buildDelegatedBatchTypedData(1000, [to], [data]); + const typedData = buildDelegatedBatchTypedData(nonce, [to], [data]); const senderSignature = await signTypedData(typedData, sender); - await expect(gateway.delegateBatchFromAccount(account, [to], [data], senderSignature, { + await expect(gateway.delegateBatch(account, nonce, [to], [data], senderSignature, { from, - gasPrice: GAS_PRICE, })) .rejects .toThrow(/revert/); }); }); - context('delegateBatchWithoutGasPriceFromAccount()', () => { + context('delegateBatchWithGasPrice()', () => { const from = addresses.pop(); const account = addresses.pop(); const sender = addresses.pop(); @@ -301,16 +302,18 @@ contract('Gateway', (addresses) => { }); it('expect to send single call', async () => { + const nonce = getNextNonce(); const to = gatewayRecipientMock.address; const data = gatewayRecipientMock.contract.methods.emitContext() .encodeABI(); - const typedData = buildDelegatedBatchWithoutGasPriceTypedData(0, [to], [data]); + const typedData = buildDelegatedBatchWithGasPriceTypedData(nonce, [to], [data]); const senderSignature = await signTypedData(typedData, sender); - const output = await gateway.delegateBatchWithoutGasPriceFromAccount(account, [to], [data], senderSignature, { + const output = await gateway.delegateBatchWithGasPrice(account, nonce, [to], [data], senderSignature, { from, + gasPrice: GAS_PRICE, }); logGasUsage(output); @@ -328,7 +331,6 @@ contract('Gateway', (addresses) => { const from = addresses.pop(); const account = addresses.pop(); const sender = addresses.pop(); - let nonce = 0; before(async () => { await accountOwnerRegistry.addAccountOwner(sender, { @@ -337,15 +339,16 @@ contract('Gateway', (addresses) => { }); it('expect to send single batch', async () => { + const nonce = getNextNonce(); const to = gatewayRecipientMock.address; const data = gatewayRecipientMock.contract.methods.emitContext() .encodeABI(); - const typedData = buildDelegatedBatchWithoutGasPriceTypedData(nonce, [to], [data]); + const typedData = buildDelegatedBatchTypedData(nonce, [to], [data]); const senderSignature = await signTypedData(typedData, sender); - const batch = gateway.contract.methods.delegateBatchWithoutGasPriceFromAccount(account, [to], [data], senderSignature) + const batch = gateway.contract.methods.delegateBatch(account, nonce, [to], [data], senderSignature) .encodeABI(); const output = await gateway.delegateBatches([batch], false, { @@ -362,8 +365,6 @@ contract('Gateway', (addresses) => { .toBe(batch); expect(logs[0].args.succeeded) .toBeTruthy(); - - nonce += 1; }); it('expect to send multiple batch', async () => { @@ -371,20 +372,19 @@ contract('Gateway', (addresses) => { const batchesCount = 4; for (let index = 0; index < batchesCount; index += 1) { + const nonce = getNextNonce(); const to = gatewayRecipientMock.address; const data = gatewayRecipientMock.contract.methods.emitContext() .encodeABI(); - const typedData = buildDelegatedBatchWithoutGasPriceTypedData(nonce, [to], [data]); + const typedData = buildDelegatedBatchTypedData(nonce, [to], [data]); const senderSignature = await signTypedData(typedData, sender); batches.push( - gateway.contract.methods.delegateBatchWithoutGasPriceFromAccount(account, [to], [data], senderSignature) + gateway.contract.methods.delegateBatch(account, nonce, [to], [data], senderSignature) .encodeABI(), ); - - nonce += 1; } const output = await gateway.delegateBatches(batches, false, { @@ -407,16 +407,16 @@ contract('Gateway', (addresses) => { context('# revertOnFailure flag is set to true', () => { it('expect to revert on batch failure', async () => { + const nonce = 0; // invalid nonce const to = gatewayRecipientMock.address; const data = gatewayRecipientMock.contract.methods.emitContext() .encodeABI(); - // invalid nonce - const typedData = buildDelegatedBatchWithoutGasPriceTypedData(nonce + 10, [to], [data]); + const typedData = buildDelegatedBatchTypedData(nonce, [to], [data]); const senderSignature = await signTypedData(typedData, sender); - const batch = gateway.contract.methods.delegateBatchWithoutGasPriceFromAccount(account, [to], [data], senderSignature) + const batch = gateway.contract.methods.delegateBatch(account, nonce, [to], [data], senderSignature) .encodeABI(); await expect(gateway.delegateBatches([batch], true, { @@ -429,16 +429,17 @@ contract('Gateway', (addresses) => { context('# revertOnFailure flag is set to false', () => { it('expect to emit event on batch failure', async () => { + const nonce = 0; // invalid nonce const to = gatewayRecipientMock.address; const data = gatewayRecipientMock.contract.methods.emitContext() .encodeABI(); // invalid nonce - const typedData = buildDelegatedBatchWithoutGasPriceTypedData(nonce + 10, [to], [data]); + const typedData = buildDelegatedBatchTypedData(nonce, [to], [data]); const senderSignature = await signTypedData(typedData, sender); - const batch = gateway.contract.methods.delegateBatchWithoutGasPriceFromAccount(account, [to], [data], senderSignature) + const batch = gateway.contract.methods.delegateBatch(account, nonce, [to], [data], senderSignature) .encodeABI(); const output = await gateway.delegateBatches([batch], false, { @@ -473,27 +474,27 @@ contract('Gateway', (addresses) => { nonce: 1000, to: [to], data: [data], - gasPrice: GAS_PRICE, })) .resolves .toBe(typedDataHash); }); }); - context('hashDelegatedBatchWithoutGasPrice()', () => { + context('hashDelegatedBatchWithGasPrice()', () => { it('expect to return correct hash', async () => { const to = gatewayRecipientMock.address; const data = gatewayRecipientMock.contract.methods.emitContext() .encodeABI(); - const typedData = buildDelegatedBatchWithoutGasPriceTypedData(2000, [to], [data]); + const typedData = buildDelegatedBatchWithGasPriceTypedData(2000, [to], [data]); const typedDataHash = hashTypedData(typedData); - await expect(gateway.hashDelegatedBatchWithoutGasPrice({ + await expect(gateway.hashDelegatedBatchWithGasPrice({ nonce: 2000, to: [to], data: [data], + gasPrice: GAS_PRICE, })) .resolves .toBe(typedDataHash); diff --git a/test/utils.js b/test/utils.js index 7f0051b4..773ae8b0 100644 --- a/test/utils.js +++ b/test/utils.js @@ -215,6 +215,14 @@ function increaseTime(seconds = 0) { }); } +let currentNonce = 1; + +function getNextNonce() { + currentNonce += 1; + + return currentNonce; +} + module.exports = { concatHex, logGasUsage, @@ -229,4 +237,5 @@ module.exports = { signTypedData, computeCreate2Address, increaseTime, + getNextNonce, };