-
Notifications
You must be signed in to change notification settings - Fork 245
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Meta Txs: Adding support for meta transactions in aragon apps (Part 1) #526
Draft
facuspagnuolo
wants to merge
19
commits into
next
Choose a base branch
from
meta-txs
base: next
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from 14 commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
061b437
meta-txs: measure gas ovearload for different alternatives
facuspagnuolo db8c7ff
meta-txs: optimize nonce tracking costs
facuspagnuolo fd420ff
meta-txs: pick relayed app using volatile storage approach
facuspagnuolo 87030a0
meta-txs: use depositable proxies functionality from relayer
facuspagnuolo ee34afa
meta-txs: implement shared interface between relayer and apps
facuspagnuolo 3c6a500
meta-txs: optimize off chain service authorization
facuspagnuolo a576413
meta-txs: use events helpers
facuspagnuolo d83a209
meta-txs: optimize volatile sender using calldata
facuspagnuolo ada4788
meta-txs: add solidity tests
facuspagnuolo 464ee8f
meta-txs: implement gas refunds quotas
facuspagnuolo d528882
meta-txs: fix linting rules
facuspagnuolo ec06318
meta-txs: skip gas test for coverage measure
facuspagnuolo 3d2b814
meta-txs: fix kernel tests
facuspagnuolo dcd3db7
meta-txs: increase coverage
facuspagnuolo 306d35c
meta-txs: multiple enhancements
facuspagnuolo f1e30eb
meta-txs: implement whitelist of allowed senders
facuspagnuolo f092d3b
meta-txs: support EIP712
facuspagnuolo 3b91caa
meta-txs: ignore relayer contracts for coverage measure
facuspagnuolo a439ac5
meta-txs: store senders information using a struct
facuspagnuolo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
pragma solidity ^0.4.24; | ||
|
||
|
||
library MemoryHelpers { | ||
|
||
function append(bytes memory self, address addr) internal pure returns (bytes memory) { | ||
// alloc required encoded data size | ||
uint256 dataSize = self.length; | ||
uint256 appendedDataSize = dataSize + 32; | ||
bytes memory appendedData = new bytes(appendedDataSize); | ||
|
||
// copy data | ||
uint256 inputPointer; | ||
uint256 outputPointer; | ||
assembly { | ||
inputPointer := add(self, 0x20) | ||
outputPointer := add(appendedData, 0x20) | ||
} | ||
memcpy(outputPointer, inputPointer, dataSize); | ||
|
||
// append address | ||
assembly { | ||
let signerPointer := add(add(appendedData, 0x20), dataSize) | ||
mstore(signerPointer, addr) | ||
} | ||
|
||
return appendedData; | ||
} | ||
|
||
// From https://github.com/Arachnid/solidity-stringutils/blob/master/src/strings.sol | ||
function memcpy(uint256 output, uint256 input, uint256 length) internal pure { | ||
uint256 len = length; | ||
uint256 dest = output; | ||
uint256 src = input; | ||
|
||
// Copy word-length chunks while possible | ||
for (; len >= 32; len -= 32) { | ||
assembly { | ||
mstore(dest, mload(src)) | ||
} | ||
dest += 32; | ||
src += 32; | ||
} | ||
|
||
// Copy remaining bytes | ||
uint256 mask = 256 ** (32 - len) - 1; | ||
assembly { | ||
let srcpart := and(mload(src), not(mask)) | ||
let destpart := and(mload(dest), mask) | ||
mstore(dest, or(destpart, srcpart)) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
pragma solidity ^0.4.24; | ||
|
||
|
||
/** | ||
* @title Elliptic curve signature operations | ||
* @dev Based on https://github.com/OpenZeppelin/openzeppelin-solidity/blob/v2.0.0/contracts/cryptography/ECDSA.sol | ||
*/ | ||
library ECDSA { | ||
|
||
/** | ||
* @dev Recover signer address from a message by using their signature | ||
* @param hash bytes32 message, the hash is the signed message. What is recovered is the signer address. | ||
* @param signature bytes signature, the signature is generated using web3.eth.sign() | ||
*/ | ||
function recover(bytes32 hash, bytes signature) | ||
internal | ||
pure | ||
returns (address) | ||
{ | ||
bytes32 r; | ||
bytes32 s; | ||
uint8 v; | ||
|
||
// Check the signature length | ||
if (signature.length != 65) { | ||
return (address(0)); | ||
} | ||
|
||
// Divide the signature in r, s and v variables | ||
// ecrecover takes the signature parameters, and the only way to get them | ||
// currently is to use assembly. | ||
// solium-disable-next-line security/no-inline-assembly | ||
assembly { | ||
r := mload(add(signature, 0x20)) | ||
s := mload(add(signature, 0x40)) | ||
v := byte(0, mload(add(signature, 0x60))) | ||
} | ||
|
||
// Version of signature should be 27 or 28, but 0 and 1 are also possible versions | ||
if (v < 27) { | ||
v += 27; | ||
} | ||
|
||
// If the version is correct return the signer address | ||
if (v != 27 && v != 28) { | ||
return (address(0)); | ||
} else { | ||
// solium-disable-next-line arg-overflow | ||
return ecrecover(hash, v, r, s); | ||
} | ||
} | ||
|
||
/** | ||
* toEthSignedMessageHash | ||
* @dev prefix a bytes32 value with "\x19Ethereum Signed Message:" | ||
* and hash the result | ||
*/ | ||
function toEthSignedMessageHash(bytes32 hash) | ||
internal | ||
pure | ||
returns (bytes32) | ||
{ | ||
// 32 is the length in bytes of hash, | ||
// enforced by the type signature above | ||
return keccak256( | ||
abi.encodePacked("\x19Ethereum Signed Message:\n32", hash) | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
pragma solidity ^0.4.24; | ||
|
||
|
||
contract IRelayer { | ||
function relay(address from, address to, uint256 nonce, bytes data, uint256 gasRefund, uint256 gasPrice, bytes signature) external; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
pragma solidity ^0.4.24; | ||
|
||
import "./IRelayer.sol"; | ||
import "../apps/AragonApp.sol"; | ||
|
||
|
||
contract RelayedAragonApp is AragonApp { | ||
|
||
function sender() internal view returns (address) { | ||
address relayer = address(_relayer()); | ||
if (msg.sender != relayer) { | ||
return msg.sender; | ||
} | ||
|
||
address signer = _decodeSigner(); | ||
return signer != address(0) ? signer : relayer; | ||
} | ||
|
||
function _decodeSigner() internal returns (address signer) { | ||
// Note that calldatasize includes one word more than the original calldata array, due to the address of the | ||
// signer that is being appended at the end of it. Thus, we are loading the last word of the calldata array to | ||
// fetch the actual signed of the relayed call | ||
assembly { | ||
let ptr := mload(0x40) | ||
mstore(0x40, add(ptr, 0x20)) | ||
calldatacopy(ptr, sub(calldatasize, 0x20), 0x20) | ||
signer := mload(ptr) | ||
} | ||
} | ||
|
||
function _relayer() internal returns (IRelayer) { | ||
return kernel().relayer(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
pragma solidity ^0.4.24; | ||
|
||
import "./IRelayer.sol"; | ||
import "./RelayedAragonApp.sol"; | ||
import "../lib/sig/ECDSA.sol"; | ||
import "../lib/math/SafeMath.sol"; | ||
import "../apps/AragonApp.sol"; | ||
import "../common/IsContract.sol"; | ||
import "../common/TimeHelpers.sol"; | ||
import "../common/MemoryHelpers.sol"; | ||
import "../common/DepositableStorage.sol"; | ||
|
||
|
||
contract Relayer is IRelayer, AragonApp, DepositableStorage { | ||
using ECDSA for bytes32; | ||
using SafeMath for uint256; | ||
using MemoryHelpers for bytes; | ||
|
||
bytes32 public constant ALLOW_OFF_CHAIN_SERVICE_ROLE = keccak256("ALLOW_OFF_CHAIN_SERVICE_ROLE"); | ||
bytes32 public constant DISALLOW_OFF_CHAIN_SERVICE_ROLE = keccak256("DISALLOW_OFF_CHAIN_SERVICE_ROLE"); | ||
|
||
string private constant ERROR_GAS_REFUND_FAIL = "RELAYER_GAS_REFUND_FAIL"; | ||
string private constant ERROR_GAS_QUOTA_EXCEEDED = "RELAYER_GAS_QUOTA_EXCEEDED"; | ||
string private constant ERROR_NONCE_ALREADY_USED = "RELAYER_NONCE_ALREADY_USED"; | ||
string private constant ERROR_SERVICE_NOT_ALLOWED = "RELAYER_SERVICE_NOT_ALLOWED"; | ||
string private constant ERROR_INVALID_SENDER_SIGNATURE = "RELAYER_INVALID_SENDER_SIGNATURE"; | ||
|
||
event ServiceAllowed(address indexed service); | ||
event ServiceDisallowed(address indexed service); | ||
event TransactionRelayed(address from, address to, uint256 nonce, bytes calldata); | ||
|
||
uint256 public startDate; | ||
uint256 public monthlyRefundQuota; | ||
mapping (address => bool) internal allowedServices; | ||
mapping (address => uint256) internal totalRefunds; | ||
mapping (address => uint256) internal lastUsedNonce; | ||
|
||
modifier onlyAllowedServices() { | ||
require(isServiceAllowed(msg.sender), ERROR_SERVICE_NOT_ALLOWED); | ||
_; | ||
} | ||
|
||
function initialize(uint256 _monthlyRefundQuota) external onlyInit { | ||
initialized(); | ||
startDate = getTimestamp(); | ||
monthlyRefundQuota = _monthlyRefundQuota; | ||
setDepositable(true); | ||
} | ||
|
||
function relay(address from, address to, uint256 nonce, bytes data, uint256 gasRefund, uint256 gasPrice, bytes signature) | ||
external | ||
onlyAllowedServices | ||
{ | ||
uint256 refund = gasRefund.mul(gasPrice); | ||
require(canRefund(from, refund), ERROR_GAS_QUOTA_EXCEEDED); | ||
require(!isNonceUsed(from, nonce), ERROR_NONCE_ALREADY_USED); | ||
require(isValidSignature(from, messageHash(to, nonce, data, gasRefund, gasPrice), signature), ERROR_INVALID_SENDER_SIGNATURE); | ||
|
||
totalRefunds[from] = totalRefunds[from].add(refund); | ||
lastUsedNonce[from] = nonce; | ||
|
||
relayCall(from, to, data); | ||
emit TransactionRelayed(from, to, nonce, data); | ||
|
||
/* solium-disable security/no-send */ | ||
require(msg.sender.send(refund), ERROR_GAS_REFUND_FAIL); | ||
} | ||
|
||
function allowService(address service) external authP(ALLOW_OFF_CHAIN_SERVICE_ROLE, arr(service)) { | ||
allowedServices[service] = true; | ||
emit ServiceAllowed(service); | ||
} | ||
|
||
function disallowService(address service) external authP(DISALLOW_OFF_CHAIN_SERVICE_ROLE, arr(service)) { | ||
allowedServices[service] = false; | ||
emit ServiceDisallowed(service); | ||
} | ||
|
||
function allowRecoverability(address token) public view returns (bool) { | ||
// does not allow to recover ETH | ||
return token != ETH; | ||
} | ||
|
||
function isServiceAllowed(address service) public view returns (bool) { | ||
return allowedServices[service]; | ||
} | ||
|
||
function getLastUsedNonce(address sender) public view returns (uint256) { | ||
return lastUsedNonce[sender]; | ||
} | ||
|
||
function getTotalRefunds(address sender) public view returns (uint256) { | ||
return totalRefunds[sender]; | ||
} | ||
|
||
function isNonceUsed(address sender, uint256 nonce) public view returns (bool) { | ||
return getLastUsedNonce(sender) >= nonce; | ||
} | ||
|
||
function canRefund(address sender, uint256 refund) public view returns (bool) { | ||
uint256 monthsSinceStart = (getTimestamp().sub(startDate) / (30 days)) + 1; | ||
uint256 maxRefunds = monthsSinceStart.mul(monthlyRefundQuota); | ||
return getTotalRefunds(sender).add(refund) <= maxRefunds; | ||
facuspagnuolo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
function isValidSignature(address sender, bytes32 hash, bytes signature) internal pure returns (bool) { | ||
address signer = hash.toEthSignedMessageHash().recover(signature); | ||
return sender == signer; | ||
} | ||
|
||
function messageHash(address to, uint256 nonce, bytes data, uint256 gasRefund, uint256 gasPrice) internal pure returns (bytes32) { | ||
return keccak256(abi.encodePacked(to, nonce, keccak256(data), gasRefund, gasPrice)); | ||
facuspagnuolo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
function relayCall(address from, address to, bytes data) internal { | ||
bytes memory encodedSignerData = data.append(from); | ||
assembly { | ||
let success := call(gas, to, 0, add(encodedSignerData, 0x20), mload(encodedSignerData), 0, 0) | ||
switch success case 0 { | ||
let ptr := mload(0x40) | ||
returndatacopy(ptr, 0, returndatasize) | ||
revert(ptr, returndatasize) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this be an ACL role too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Explained above