ethereum / EIPs

The Ethereum Improvement Proposal repository

Home Page:https://eips.ethereum.org/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

EIP1035: Transaction execution batching and delegation

k06a opened this issue · comments


eip: EIP1035
title: Transaction execution batching and delegation
author: Anton Bukov (@k06a) k06aaa@gmail.com
discussions-to: k06aaa@gmail.com
status: Draft
type: Core
category: Core
created: 2018-04-25
updated: 2018-04-26
relates: #901

Simple Summary

It will be great to allow trustless delegated execution of the transactions inside EVM. I mean some account can sign target, call data, account nonce, network chain_id and pass them to any other, who will be able to perform this call and pay Ethereum fees like this:

target.authorizedcall(data, account , nonce, chain_id, signature)

This also will allow batching/chaining account transactions and many other features. One more use case: sender of any ERC20 tokens can signed necessary call and pass it to receiver, and receiver (exchange?) will be able to pay fees for this transaction.

Abstract

Abstract implementation of target.authorizedcall(data, account, nonce, chain_id, signature) method should perform:

  1. Check nonce of the account to be valid
  2. Check chain_id to be valid for this network
  3. Check signature of target|data|nonce|chain_id and extract signer
  4. Check signer is the same as account
  5. Perform data call on target with msg.sender == account
  6. Increment account nonce

Motivation

  1. Ethereum fees is a real problem for some new users, who had some (bounty?) tokens and can't even spend this tokens without buying some ether.
  2. Look at #827, #1003 – these protocols are trying to solve the problem of batching/chaining user transactions.

Specification

From solidity side this should looks like function on every smart contract:

function authorizedcall(bytes data, address account, uint256 nonce, uint256 chain_id, bytes signature);

Test Cases

Test cases for an implementation are mandatory for EIPs that are affecting consensus changes. Other EIPs can choose to include links to test cases if applicable.

Implementation

This is working example: https://github.com/bitclave/Feeless

Complete code:

import { ECRecovery } from "zeppelin-solidity/contracts/ECRecovery.sol";


contract Feeless {
    
    address internal msgSender;
    mapping(address => uint256) public nonces;
    
    modifier feeless {
        if (msgSender == address(0)) {
            msgSender = msg.sender;
            _;
            msgSender = address(0);
        } else {
            _;
        }
    }

    function performFeelessTransaction(address sender, bytes data, uint256 nonce, bytes sig) public payable {
        bytes memory prefix = "\x19Ethereum Signed Message:\n32";
        bytes32 hash = keccak256(prefix, keccak256(data, nonce));
        msgSender = ECRecovery.recover(hash, sig);
        require(msgSender == sender);

        require(nonces[msgSender]++ == nonce);
        require(address(this).call.value(msg.value)(data));
        msgSender = address(0);
    }
    
}

Usage:

contract MyToken is StandardToken, Feeless {

    string public constant symbol = "XXX";
    string public constant name = "MyToken";
    uint8 public constant decimals = 18;
    string public constant version = "1.0";

    function transfer(address _to, uint256 _value) public feeless returns (bool) {
        balances[msgSender] = balances[msgSender].sub(_value);
        balances[_to] = balances[_to].add(_value);
        Transfer(msgSender, _to, _value);
        return true;
    }

}

Signing call data:

const target = myToken.options.address;
const nonce = await myToken.methods.nonces(wallet1).call();
const data = await myToken.methods.transfer(wallet2, 5 * 10**18).encodeABI();
const hash = web3.utils.sha3(target + data.substr(2) + web3.utils.toBN(nonce).toString(16,64));
const sig = await web3.eth.accounts.sign(hash, wallet1PrivateKey);

Delegated execution by any other account:

await myToken.performFeelessTransaction(wallet1, target, data, nonce, sig).send({ from: wallet2 });

I would personally prefer this to be an EVM layer change, rather than contract layer. It feels silly that the person authorizing the transaction has to be the same person paying fees for the transaction. Miners care whether they get fees, they don't care about who they get fees from. Users care about other people not being able to use/spend assets under the control of their private keys, they don't care about how the transaction ends up in a block.

I would like to see transactions be two layers, the outer layer is signed by the fee payer and contains feePayerNonce, gasPrice, gasLimit, feePayerSignature, transactorNonce, to, value, data, transactorSignature. It would be even cooler if transactorNonce, to, value, data, transactorSignature was actually an array, for batching.

The above scheme would allow users to pay for "transaction submission services" off-chain via other mechanisms (e.g., monthly subscription, pre-paid, paid with fiat, add-on benefit to other services, etc.). It would also indirectly enable gas payments in non-eth if batching was supported, as the transactor executing the transaction could provide two signed transactions to the feePayer, one that transfers them some tokens and another that does the thing desired, with the nonce of the token transfer being one more than the nonce of the desired action. The feePayer can batch these two transactions together to effectively be paid in tokens, while miners are still paid in ETH. Alternatively, if the transactor is willing to enter into a trusted relationship with the feePayer then they could pre-pay for submissions via something like DAI.

@MicahZoltu the more I think about this proposal the more I see this is the solution for tx batching to replace ERC827 (and ERC1003). Ethereum also can provide default smart contract (0x05?) for batching transactions with the methods like this:

function batchWithRequire(
    address account,
    address[] targets,
    bytes[] datas,
    uint256 firstNonce,
    uint256 chain_id,
    bytes[] sigs) public
{
    for (uint i = 0; i < targets.length; i++) {
        require(targets[i].authorizedcall(datas[i], account, firstNonce + i, chain_id, sigs[i]));
    }
}   

This EIP will help a lot in inter-chain development.

Yay! That would greatly help with off-chain scaling schemes like state channels.

Strongly related to #1077 by @alexvandesande. Have you considered collaborating on this one @k06a?

@PhABC thanks, I'll dig into it!

I agree with @MicahZoltu , I think we should allow any Ethereum transaction to have gas payment delegated, with no need of in contract function. I think this proposal is related to #1228 and #865 .

If someone is looking for ETH, ERC20 batch sender - feel free to use:
https://multisender.app

I would personally prefer this to be an EVM layer change, rather than contract layer. It feels silly that the person authorizing the transaction has to be the same person paying fees for the transaction. Miners care whether they get fees, they don't care about who they get fees from. Users care about other people not being able to use/spend assets under the control of their private keys, they don't care about how the transaction ends up in a block.

I would like to see transactions be two layers, the outer layer is signed by the fee payer and contains feePayerNonce, gasPrice, gasLimit, feePayerSignature, transactorNonce, to, value, data, transactorSignature. It would be even cooler if transactorNonce, to, value, data, transactorSignature was actually an array, for batching.

The above scheme would allow users to pay for "transaction submission services" off-chain via other mechanisms (e.g., monthly subscription, pre-paid, paid with fiat, add-on benefit to other services, etc.). It would also indirectly enable gas payments in non-eth if batching was supported, as the transactor executing the transaction could provide two signed transactions to the feePayer, one that transfers them some tokens and another that does the thing desired, with the nonce of the token transfer being one more than the nonce of the desired action. The feePayer can batch these two transactions together to effectively be paid in tokens, while miners are still paid in ETH. Alternatively, if the transactor is willing to enter into a trusted relationship with the feePayer then they could pre-pay for submissions via something like DAI.

Is this EIP continuing? I am especially interested in details pertaining to the EVM change described above. It would simplify the creation of a retroactive metatransactions upgrade to all existing tokens and contracts. relevant to EIPs: 1776, 965, 985.

@AlexeyAkhunov has been pushing recently for changing the transaction protocol so it can evolve over time. It may be worth considering #1035 (comment) in that design.

@MicahZoltu I stumbled upon this EIP while having similar discussions regarding enabling meta transactions on the Zilliqa blockchain. My overall goal is driving towards subscription-based recurring payments that would drastically benefit from transaction batching. I've linked your comment to justify work breaking out the messaging standard like so. In that regard, I'd like to stay in the loop and contribute as I can to the Ethereum effort. Kudos to you ETH guys for discussing and innovating in the open... Zilliqa really needs an EIP repository.

what if the ERC20 token already existed,Could implement the feeless contract? if can ,how

import { ECRecovery } from "zeppelin-solidity/contracts/ECRecovery.sol";

contract Feeless {

address internal msgSender;
mapping(address => uint256) public nonces;

modifier feeless {
    if (msgSender == address(0)) {
        msgSender = msg.sender;
        _;
        msgSender = address(0);
    } else {
        _;
    }
}

function performFeelessTransaction(address sender, bytes data, uint256 nonce, bytes sig) public payable {
    bytes memory prefix = "\x19Ethereum Signed Message:\n32";
    bytes32 hash = keccak256(prefix, keccak256(data, nonce));
    msgSender = ECRecovery.recover(hash, sig);
    require(msgSender == sender);

    require(nonces[msgSender]++ == nonce);
    require(address(this).call.value(msg.value)(data));
    msgSender = address(0);
}

}


Usage:

contract MyToken is StandardToken, Feeless {

string public constant symbol = "XXX";
string public constant name = "MyToken";
uint8 public constant decimals = 18;
string public constant version = "1.0";

function transfer(address _to, uint256 _value) public feeless returns (bool) {
    balances[msgSender] = balances[msgSender].sub(_value);
    balances[_to] = balances[_to].add(_value);
    Transfer(msgSender, _to, _value);
    return true;
}

}


Signing call data:

const target = myToken.options.address;
const nonce = await myToken.methods.nonces(wallet1).call();
const data = await myToken.methods.transfer(wallet2, 5 * 10**18).encodeABI();
const hash = web3.utils.sha3(target + data.substr(2) + web3.utils.toBN(nonce).toString(16,64));
const sig = await web3.eth.accounts.sign(hash, wallet1PrivateKey);


Delegated execution by any other account:

await myToken.performFeelessTransaction(wallet1, target, data, nonce, sig).send({ from: wallet2 });

If all it takes is just one public address to delegate the transaction, how is the security maintained ?
What is stopping someone from delegating the transaction to random accounts ?

There has been no activity on this issue for two months. It will be closed in a week if no further activity occurs. If you would like to move this EIP forward, please respond to any outstanding feedback or add a comment indicating that you have addressed all required feedback and are ready for a review.

This issue was closed due to inactivity. If you are still pursuing it, feel free to reopen it and respond to any feedback or request a review in a comment.

If anyone is looking to send ETH, tokens, and NFTs in batches, a good option is OGP Bulk Sender