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 1 commit
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
23 changes: 23 additions & 0 deletions contracts/apps/AppStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ contract AppStorage {
bytes32 internal constant KERNEL_POSITION = 0x4172f0f7d2289153072b0a6ca36959e0cbe2efc3afe50fc81636caa96338137b;
bytes32 internal constant APP_ID_POSITION = 0xd625496217aa6a3453eecb9c3489dc5a53e6c67b444329ea2b2cbc9ff547639b;

bytes32 internal constant USED_NONCE_POSITION_BASE = keccak256("aragonOS.appStorage.usedNonce");
bytes32 internal constant VOLATILE_SENDER_POSITION = keccak256("aragonOS.appStorage.volatile.sender");

function kernel() public view returns (IKernel) {
return IKernel(KERNEL_POSITION.getStorageAddress());
}
Expand All @@ -26,11 +29,31 @@ contract AppStorage {
return APP_ID_POSITION.getStorageBytes32();
}

function volatileStorageSender() public view returns (address) {
return VOLATILE_SENDER_POSITION.getStorageAddress();
}

function usedNonce(address _account, uint256 _nonce) public view returns (bool) {
return usedNoncePosition(_account, _nonce).getStorageBool();
}

function setKernel(IKernel _kernel) internal {
KERNEL_POSITION.setStorageAddress(address(_kernel));
}

function setAppId(bytes32 _appId) internal {
APP_ID_POSITION.setStorageBytes32(_appId);
}

function setVolatileStorageSender(address _sender) internal {
VOLATILE_SENDER_POSITION.setStorageAddress(_sender);
}

function setUsedNonce(address _account, uint256 _nonce, bool _used) internal {
return usedNoncePosition(_account, _nonce).setStorageBool(_used);
}

function usedNoncePosition(address _account, uint256 _nonce) internal returns (bytes32) {
return keccak256(abi.encodePacked(USED_NONCE_POSITION_BASE, _account, _nonce));
}
}
10 changes: 7 additions & 3 deletions contracts/apps/AragonApp.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ import "../evmscript/EVMScriptRunner.sol";
// ReentrancyGuard, EVMScriptRunner, and ACLSyntaxSugar are not directly used by this contract, but
// are included so that they are automatically usable by subclassing contracts
contract AragonApp is AppStorage, Autopetrified, VaultRecoverable, ReentrancyGuard, EVMScriptRunner, ACLSyntaxSugar {
string private constant ERROR_AUTH_FAILED = "APP_AUTH_FAILED";
string internal 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;
}
}
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)
);
}
}
61 changes: 61 additions & 0 deletions contracts/relayer/BaseRelayer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
pragma solidity ^0.4.24;

import "../lib/sig/ECDSA.sol";
import "../apps/AragonApp.sol";
import "../common/DepositableStorage.sol";


contract BaseRelayer is AragonApp, DepositableStorage {
using ECDSA for bytes32;

bytes32 public constant OFF_CHAIN_RELAYER_SERVICE_ROLE = keccak256("OFF_CHAIN_RELAYER_SERVICE_ROLE");

uint256 private constant EXTERNAL_TX_COST = 21000;

string private constant ERROR_GAS_REFUND_FAIL = "RELAYER_GAS_REFUND_FAIL";
string private constant ERROR_NONCE_ALREADY_USED = "RELAYER_NONCE_ALREADY_USED";
string private constant ERROR_INVALID_SENDER_SIGNATURE = "RELAYER_INVALID_SENDER_SIGNATURE";

event FundsReceived(address indexed sender, uint256 amount);
event TransactionRelayed(address indexed from, address indexed to, uint256 nonce, bytes calldata);

modifier refundGas() {
uint256 startGas = gasleft();
_;
uint256 refund = EXTERNAL_TX_COST + startGas - gasleft();
require(msg.sender.send(refund), ERROR_GAS_REFUND_FAIL);
}

function () external payable {
emit FundsReceived(msg.sender, msg.value);
}

function initialize() public onlyInit {
initialized();
setDepositable(true);
}

function isNonceUsed(address sender, uint256 nonce) public view returns (bool);

function assertValidTransaction(address from, uint256 nonce, bytes calldata, bytes signature) internal view {
require(!isNonceUsed(from, nonce), ERROR_NONCE_ALREADY_USED);
require(isValidSignature(from, messageHash(calldata, nonce), signature), ERROR_INVALID_SENDER_SIGNATURE);
}

function isValidSignature(address sender, bytes32 hash, bytes signature) internal pure returns (bool) {
address signer = hash.toEthSignedMessageHash().recover(signature);
return sender == signer;
}

function messageHash(bytes calldata, uint256 nonce) internal pure returns (bytes32) {
return keccak256(abi.encodePacked(keccak256(calldata), nonce));
}

function revertForwardingError() internal {
assembly {
let ptr := mload(0x40)
returndatacopy(ptr, 0, returndatasize)
revert(ptr, returndatasize)
}
}
}
24 changes: 24 additions & 0 deletions contracts/relayer/RelayedAragonAppWithParameterizedSender.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
pragma solidity ^0.4.24;

import "../apps/AragonApp.sol";


contract RelayedAragonAppWithParameterizedSender is AragonApp {
bytes32 public constant RELAYER_ROLE = keccak256("RELAYER_ROLE");

modifier relayedAuth(address _sender, bytes32 _role) {
assertRelayer();
require(canPerform(_sender, _role, new uint256[](0)), ERROR_AUTH_FAILED);
_;
}

modifier relayedAuthP(address _sender, bytes32 _role, uint256[] _params) {
assertRelayer();
require(canPerform(_sender, _role, _params), ERROR_AUTH_FAILED);
_;
}

function assertRelayer() private {
require(canPerform(msg.sender, RELAYER_ROLE, new uint256[](0)), ERROR_AUTH_FAILED);
}
}
29 changes: 29 additions & 0 deletions contracts/relayer/RelayedAragonAppWithVolatileSender.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
pragma solidity ^0.4.24;

import "../apps/AragonApp.sol";


contract RelayedAragonAppWithVolatileSender is AragonApp {
bytes32 public constant RELAYER_ROLE = keccak256("RELAYER_ROLE");

function exec(address from, bytes calldata) external auth(RELAYER_ROLE) {
setVolatileStorageSender(from);
bool success = address(this).call(calldata);
if (!success) revertForwardingError();
setVolatileStorageSender(address(0));
}

function sender() internal view returns (address) {
if (msg.sender != address(this)) return msg.sender;
address volatileSender = volatileStorageSender();
return volatileSender != address(0) ? volatileSender : address(this);
}

function revertForwardingError() internal {
assembly {
let ptr := mload(0x40)
returndatacopy(ptr, 0, returndatasize)
revert(ptr, returndatasize)
}
}
}
29 changes: 29 additions & 0 deletions contracts/relayer/RelayerAragonAppWithParameterizedSender.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
pragma solidity ^0.4.24;

import "./BaseRelayer.sol";


contract RelayerAragonAppWithParameterizedSender is BaseRelayer {
modifier relayedAuth(address _sender, bytes32 _role) {
require(canPerform(_sender, _role, new uint256[](0)), ERROR_AUTH_FAILED);
_;
}

modifier relayedAuthP(address _sender, bytes32 _role, uint256[] _params) {
require(canPerform(_sender, _role, _params), ERROR_AUTH_FAILED);
_;
}

function exec(address from, uint256 nonce, bytes calldata, bytes signature) external refundGas auth(OFF_CHAIN_RELAYER_SERVICE_ROLE) {
assertValidTransaction(from, nonce, calldata, signature);

setUsedNonce(from, nonce, true);
bool success = address(this).call(calldata);
if (!success) revertForwardingError();
emit TransactionRelayed(from, address(this), nonce, calldata);
}

function isNonceUsed(address _account, uint256 _nonce) public view returns (bool) {
return usedNonce(_account, _nonce);
}
}
29 changes: 29 additions & 0 deletions contracts/relayer/RelayerAragonAppWithVolatileSender.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
pragma solidity ^0.4.24;

import "./BaseRelayer.sol";


contract RelayerAragonAppWithVolatileSender is BaseRelayer {
function exec(address from, uint256 nonce, bytes calldata, bytes signature) external refundGas auth(OFF_CHAIN_RELAYER_SERVICE_ROLE) {
assertValidTransaction(from, nonce, calldata, signature);

setVolatileStorageSender(from);
setUsedNonce(from, nonce, true);

bool success = address(this).call(calldata);
if (!success) revertForwardingError();

setVolatileStorageSender(address(0));
emit TransactionRelayed(from, address(this), nonce, calldata);
}

function isNonceUsed(address _account, uint256 _nonce) public view returns (bool) {
return usedNonce(_account, _nonce);
}

function sender() internal view returns (address) {
if (msg.sender != address(this)) return msg.sender;
address volatileSender = volatileStorageSender();
return volatileSender != address(0) ? volatileSender : address(this);
}
}
21 changes: 21 additions & 0 deletions contracts/relayer/StandAloneRelayer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
pragma solidity ^0.4.24;

import "./BaseRelayer.sol";


contract StandAloneRelayer is BaseRelayer {
mapping (address => mapping (uint256 => bool)) internal usedNonces;

function relay(address from, address to, uint256 nonce, bytes calldata, bytes signature) external refundGas auth(OFF_CHAIN_RELAYER_SERVICE_ROLE) {
assertValidTransaction(from, nonce, calldata, signature);

usedNonces[from][nonce] = true;
bool success = to.call(calldata);
if (!success) revertForwardingError();
emit TransactionRelayed(from, to, nonce, calldata);
}

function isNonceUsed(address sender, uint256 nonce) public view returns (bool) {
return usedNonces[sender][nonce];
}
}
12 changes: 12 additions & 0 deletions contracts/test/mocks/lib/sig/ECDSAMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
pragma solidity ^0.4.24;

import "../../../../lib/sig/ECDSA.sol";


contract ECDSAMock {
using ECDSA for bytes32;

function recover(bytes32 hash, bytes signature) public pure returns (address) {
return hash.toEthSignedMessageHash().recover(signature);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
pragma solidity 0.4.24;

import "../../../relayer/RelayedAragonAppWithParameterizedSender.sol";


contract RelayedAragonAppWithParameterizedSenderMock is RelayedAragonAppWithParameterizedSender {
bytes32 public constant WRITING_ROLE = keccak256("WRITING_ROLE");

uint256 private x;

function initialize() public onlyInit {
initialized();
x = 42;
}

function read() public view returns (uint256) {
return x;
}

function write(uint256 _x) public authP(WRITING_ROLE, arr(_x)) {
_write(_x);
}

function relayedWrite(address _sender, uint256 _x) public relayedAuthP(_sender, WRITING_ROLE, arr(_x)) {
_write(_x);
}

function _write(uint256 _x) internal {
x = _x;
}
}
Loading