Skip to content
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
wants to merge 19 commits into
base: next
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions contracts/apps/AppStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import "../kernel/IKernel.sol";
contract AppStorage {
using UnstructuredStorage for bytes32;

/* Hardcoded constants to save gas
bytes32 internal constant KERNEL_POSITION = keccak256("aragonOS.appStorage.kernel");
bytes32 internal constant APP_ID_POSITION = keccak256("aragonOS.appStorage.appId");
/*
* Hardcoded constants to save gas
* bytes32 internal constant KERNEL_POSITION = keccak256("aragonOS.appStorage.kernel");
* bytes32 internal constant APP_ID_POSITION = keccak256("aragonOS.appStorage.appId");
*/
bytes32 internal constant KERNEL_POSITION = 0x4172f0f7d2289153072b0a6ca36959e0cbe2efc3afe50fc81636caa96338137b;
bytes32 internal constant APP_ID_POSITION = 0xd625496217aa6a3453eecb9c3489dc5a53e6c67b444329ea2b2cbc9ff547639b;
Expand Down
8 changes: 6 additions & 2 deletions contracts/apps/AragonApp.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ contract AragonApp is AppStorage, Autopetrified, VaultRecoverable, ReentrancyGua
string private constant ERROR_AUTH_FAILED = "APP_AUTH_FAILED";

modifier auth(bytes32 _role) {
require(canPerform(msg.sender, _role, new uint256[](0)), ERROR_AUTH_FAILED);
require(canPerform(sender(), _role, new uint256[](0)), ERROR_AUTH_FAILED);
_;
}

modifier authP(bytes32 _role, uint256[] _params) {
require(canPerform(msg.sender, _role, _params), ERROR_AUTH_FAILED);
require(canPerform(sender(), _role, _params), ERROR_AUTH_FAILED);
_;
}

Expand Down Expand Up @@ -65,4 +65,8 @@ contract AragonApp is AppStorage, Autopetrified, VaultRecoverable, ReentrancyGua
// Funds recovery via a vault is only available when used with a kernel
return kernel().getRecoveryVault(); // if kernel is not set, it will revert
}

function sender() internal view returns (address) {
return msg.sender;
}
}
53 changes: 53 additions & 0 deletions contracts/common/MemoryHelpers.sol
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))
}
}
}
2 changes: 2 additions & 0 deletions contracts/kernel/IKernel.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
pragma solidity ^0.4.24;

import "../acl/IACL.sol";
import "../relayer/IRelayer.sol";
import "../common/IVaultRecoverable.sol";


Expand All @@ -16,6 +17,7 @@ interface IKernelEvents {
// This should be an interface, but interfaces can't inherit yet :(
contract IKernel is IKernelEvents, IVaultRecoverable {
function acl() public view returns (IACL);
function relayer() public view returns (IRelayer);
function hasPermission(address who, address where, bytes32 what, bytes how) public view returns (bool);

function setApp(bytes32 namespace, bytes32 appId, address app) public;
Expand Down
10 changes: 10 additions & 0 deletions contracts/kernel/Kernel.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import "./KernelConstants.sol";
import "./KernelStorage.sol";
import "../acl/IACL.sol";
import "../acl/ACLSyntaxSugar.sol";
import "../relayer/IRelayer.sol";
import "../common/ConversionHelpers.sol";
import "../common/IsContract.sol";
import "../common/Petrifiable.sol";
Expand Down Expand Up @@ -169,6 +170,7 @@ contract Kernel is IKernel, KernelStorage, KernelAppIds, KernelNamespaceConstant
function APP_ADDR_NAMESPACE() external pure returns (bytes32) { return KERNEL_APP_ADDR_NAMESPACE; }
function KERNEL_APP_ID() external pure returns (bytes32) { return KERNEL_CORE_APP_ID; }
function DEFAULT_ACL_APP_ID() external pure returns (bytes32) { return KERNEL_DEFAULT_ACL_APP_ID; }
function DEFAULT_RELAYER_APP_ID() external pure returns (bytes32) { return KERNEL_DEFAULT_RELAYER_APP_ID; }
/* solium-enable function-order, mixedcase */

/**
Expand Down Expand Up @@ -197,6 +199,14 @@ contract Kernel is IKernel, KernelStorage, KernelAppIds, KernelNamespaceConstant
return IACL(getApp(KERNEL_APP_ADDR_NAMESPACE, KERNEL_DEFAULT_ACL_APP_ID));
}

/**
* @dev Get the installed Relayer app
* @return Relayer app
*/
function relayer() public view returns (IRelayer) {
return IRelayer(getApp(KERNEL_APP_ADDR_NAMESPACE, KERNEL_DEFAULT_RELAYER_APP_ID));
}

/**
* @dev Function called by apps to check ACL on kernel or to check permission status
* @param _who Sender of the original call
Expand Down
2 changes: 2 additions & 0 deletions contracts/kernel/KernelConstants.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ contract KernelAppIds {
bytes32 internal constant KERNEL_CORE_APP_ID = apmNamehash("kernel");
bytes32 internal constant KERNEL_DEFAULT_ACL_APP_ID = apmNamehash("acl");
bytes32 internal constant KERNEL_DEFAULT_VAULT_APP_ID = apmNamehash("vault");
bytes32 internal constant KERNEL_DEFAULT_VAULT_APP_ID = apmNamehash("relayer");
*/
bytes32 internal constant KERNEL_CORE_APP_ID = 0x3b4bf6bf3ad5000ecf0f989d5befde585c6860fea3e574a4fab4c49d1c177d9c;
bytes32 internal constant KERNEL_DEFAULT_ACL_APP_ID = 0xe3262375f45a6e2026b7e7b18c2b807434f2508fe1a2a3dfb493c7df8f4aad6a;
bytes32 internal constant KERNEL_DEFAULT_VAULT_APP_ID = 0x7e852e0fcfce6551c13800f1e7476f982525c2b5277ba14b24339c68416336d1;
bytes32 internal constant KERNEL_DEFAULT_RELAYER_APP_ID = 0x7641595d1a2007abf0fe95c31d0b7a822954acbf6fb0cbe3bd1161d9dec9e1d3;
}


Expand Down
69 changes: 69 additions & 0 deletions contracts/lib/sig/ECDSA.sol
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)
);
}
}
6 changes: 6 additions & 0 deletions contracts/relayer/IRelayer.sol
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;
}
34 changes: 34 additions & 0 deletions contracts/relayer/RelayedAragonApp.sol
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();
}
}
126 changes: 126 additions & 0 deletions contracts/relayer/Relayer.sol
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;
Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explained above

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)
}
}
}
}
1 change: 1 addition & 0 deletions contracts/test/mocks/common/KeccakConstants.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ contract KeccakConstants {
bytes32 public constant KERNEL_APP_ID = keccak256(abi.encodePacked(APM_NODE, keccak256("kernel")));
bytes32 public constant DEFAULT_ACL_APP_ID = keccak256(abi.encodePacked(APM_NODE, keccak256("acl")));
bytes32 public constant DEFAULT_VAULT_APP_ID = keccak256(abi.encodePacked(APM_NODE, keccak256("vault")));
bytes32 public constant DEFAULT_RELAYER_APP_ID = keccak256(abi.encodePacked(APM_NODE, keccak256("relayer")));

// ACL
bytes32 public constant CREATE_PERMISSIONS_ROLE = keccak256(abi.encodePacked("CREATE_PERMISSIONS_ROLE"));
Expand Down
Loading