-
Notifications
You must be signed in to change notification settings - Fork 5.2k
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
Send by Signature (ERC777 extension) - transfer without paying for gas #965
Comments
I find this pattern in general a very good one for inviting people to use certain blockchain application where its users don't necessarily process the ether funds hence raising the bar of adaptation. My few comments:
Maybe the contract could be generalize into a PowerOfAttorny contract? |
I'm a strong advocate for this pattern as well. I find it useful for another payment-related core use case: the merchant generates a one-time address (OTA) and associates it with an invoice/order. The buyer transfers tokens to OTA as a means of paying the invoice. Now the merchant wants to sweep the tokens out of OTA, but OTA cannot pay for the gas since it has no ether. So the merchant needs to send yet another transaction to fund OTA with just enough gas so it can sweep the tokens out. The cheque model solves this problem by allowing the merchant to call I believe this use case will prove to be important for any token that is geared towards payments/means-of-exchange. It makes life easier for the services that need to accept payments denominated in that token, as they won't need to go through the extra-steps of sending additional transactions only for funding the one-time accounts with the gas that is later required to sweep the tokens out of those accounts. @chompomonim how should we move forward for bringing this proposal on the table for the #777 standard? |
@meronym this is very interesting and valid use case! I like it very much. I'm now thinking to start some kind of repo for motivations use-cases, example code and so on around Do you think we should start promoting this pattern to become part of ERC777? |
Two issues with the code
const hexData = [
_to.slice(2),
_data,
leftPad((_value).toString(16), 64, 0),
leftPad((_nonce).toString(16), 64, 0)
].join('') order of parameters is wrong. |
Let's imagine a situation where an issuer signs a series of cheques for one person (for example 100 cheques). When a person wants to withdraw all his collected cheques, he will have to send each cheque to the blockchain and pay for each transaction. This consumes a lot of gas. What if he could withdraw all his collected cheques with one tx? A little change in the code can do that. mapping(address => mapping(address => uint256)) public paidChequeSum;
function sendByCheque(address _to, uint256 _value, bytes _data, uint8 v, bytes32 r, bytes32 s) public returns (bool) {
require(_to != address(this));
// Check if signature is valid, remember last running sum
bytes memory prefix = "\x19Ethereum Signed Message:\n32";
bytes32 hash = keccak256(prefix, keccak256(_to, _value, _data));
address signer = ecrecover(hash, v, r, s);
require(signer != address(0));
uint256 amount = _value.sub(paidChequeSum[signer][_to]);
require(amount > 0);
if (amount > balances[signer]) {
amount = balances[signer];
}
// Increase already paid amount
paidChequeSum[signer][_to] = paidChequeSum[signer][_to].add(amount);
doSend(signer, _to, amount, _data, msg.sender, "", false);
return true;
}
Of course there are some drawbacks:
For both drawbacks the solution may be "Use the other |
One more issue with the code is that we need to pass the signer address too. |
This seems similar to #865, except it extends ERC777 rather than ERC20. An important feature in #865 is the ability to offer a fee (in tokens), to compensate the operator who submits the transaction. This fee could also be 0, but having the capability increases the applicable use-cases (e.g. an application wants to help it's users make token transfers, but not perpetually pay the cost of transactions). |
@syakunin thanks for your comments. Trying to address all of them.
Good point. Fixed.
Good point. Having possibility to skip some cheques in terms of saving gas (while taking some risks as well) is quite good idea. I was almost ready to accept your suggestion but I came up with even better, more in spirit of ERC777, idea ---
Interesting... You wanna say, that ecrecover can recover into not proper |
@ptrwtts thanks for pointing into #865. Idea of offering a fee (to motivate oracles) sounds very interesting. I also wanted to accept this proposal, but it pushed me to rethink of how it should work to be in spirit of ERC777 and not ERC20. In next comment I'm going to add update of this proposal. |
This is update for original proposal and if nobody will find any critical issues with it, I'm going switch original #965 description into this one. Preamble
ProblemThe problem of tokens spending is that token owner have to have ETH in his account to pay for gas. So it's impossible to have pure token account. Even in ERC777 (#777) where you can have operator which can manage your tokens (and paying for gas), there still is same problem of lack of gas to initiate SolutionAdd possibility to authorize operator by cheque. User could sign permission and send to operator via any wanted channels. Operator would send tx with signed permission into blockchain by himself so user would not need to pay for gas. Possible function's implementation: contract MyToken is ERC777 {
using ECRecovery for bytes32;
mapping (address => mapping (uint256 => bool)) private usedNonces;
function authorizeOperatorByCheque(address _operator, uint256 _nonce, bytes _sig) public returns (bool) {
require(_operator != address(this));
// Getting signer address
address signer = keccak256(_operator, _nonce).toEthSignedMessageHash().recover(_sig);
require (signer != address(0));
// Setting nonce to protect against repeating authorization in future
require (!usedNonces[signer][_nonce]);
usedNonces[signer][_nonce] = true;
// Authorizing operator
require(_operator != signer);
isOperatorFor[_operator][signer] = true;
emit AuthorizedOperator(_operator, signer);
return true;
} On user's (wallet) side cheque creation could look like: const leftPad = require('left-pad')
const hexData = [
_operator.slice(2),
leftPad((_nonce).toString(16), 64, 0)
].join('')
const msg = web3.sha3(hexData, { encoding: 'hex' }).slice(2)
const signature = web3.eth.sign(accounts[0], msg).slice(2)
const r = '0x' + signature.slice(0,64);
const s = '0x' + signature.slice(64, 128);
const v = Number.parseInt(signature.slice(128, 130), 16) + 27; Later transaction could look like: await token.sendByCheque(_operator, _nonce, v, r, s) Operator as smart contract which is accepting chequesIf user needs more control and don't want allow operator dispose of all his balance, there could be smart contract which do send tokens as users operator but only by accepting cheques. Such smart contract could look like this: contract ChequeBouncer {
using ECRecovery for bytes32;
function signerOfCheque(address _to, uint256 _amount, uint256 _fee, bytes _data) private returns (address) {
return keccak256(_to, _amount, _fee, _data).toEthSignedMessageHash().recover(_sig);
}
}
contract ChequeOperator is ChequeBouncer {
ERC777Token public token;
mapping(address => mapping(address => uint256)) public paidChequeSum;
constructor(address _token) public {
token = ERC777Token(_token);
}
function sendByCheque(address _to, uint256 _amount, uint256 _fee, bytes _data, bytes _sig) public returns (bool) {
require(_to != address(this));
// Check if signature is valid, remember last running sum
address signer = signerOfCheque(_to, _amount, _fee, _data, _sig);
require(signer != address(0));
uint256 amount = _amount.sub(paidChequeSum[signer][_to]).sub(_fee);
require(amount > 0);
if (amount > token.balances[signer]) {
amount = token.balances[signer].sub(_fee);
}
// Increase already paid amount
paidChequeSum[signer][_to] = paidChequeSum[signer][_to].add(amount);
// Send tokens
token.operatorSend(signer, _to, amount, _data, "");
token.operatorSend(signer, msg.sender, _fee, "", "");
return true;
}
} Use caseI think this kind of cheques could potentially be widely used. Example use-case: Imagine situation where shop gives discount points for client in a form of tokens. Also client downloads special app which is not only loyalty app but also is some kind of wallet and stores private key. Later, when client will want to use such points, without depositing some amount of gwei into his 'token wallet', he will not be able to transfer tokens. Meanwhile this token wallet could sign cheque and transfer it to shop back. Then shop (or some another entity) using this cheque could transfer tokens while paying for gas by himself. |
This functionality could/should be implemented as a This sort of "verify function call parameters" feature is pending as part of OpenZeppelin/openzeppelin-contracts#973 |
@shrugs If I understand correctly, with the bouncer model everyone can send txs, but a selected few bouncers can sign the allowance. In the model proposed here, everyone can sign a cheque (to allow spending on their behalf), and anybody can redeem (in practice there are use cases when only a few addresses will redeem - see my example above with one-time addresses - but that's not explicitly constrained by the model). The |
Sure! I should have elaborated more, sorry. The model would definitely be different than the "off chain whitelist" approach that Signature Bouncer takes, but the concept of "sign a permission for taking a future action with certain arguments" fits in well. Perhaps a It would be something like contract ChequeBouncer {
using ECRecovery for bytes32;
// only verify signer, this contract, and amount. this hash where you'd add a nonce
function signerOfCheque(uint256 _amount, bytes _sig)
private
returns (address)
{
return keccak256(
address(this),
_amount,
)
.toEthSignedMessageHash()
.recover(_sig);
}
}
contract MyToken is ERC777, ChequeBouncer {
// allows anyone with signature to collect _amount tokens from signer and send them to _to
function sendByCheque(address _to, uint256 _amount, bytes _sig)
public
{
address signer = signerOfCheque(_amount, _sig);
require(signer != address(0));
doSend(signer, _to, _amount);
}
} The "gassless tx with Operator" pattern is also a good use-case for Signature Bouncer. Imagine a Proxy contract that allows anyone to call it with a meta transaction, but only if that metatransaction is signed by the owner (bouncer) of that proxy. (although if you'd like to guarantee the checks by making the committed funds unspendable until cashed, issuing that commitment to the cheque on-chain is necessary) |
I'd include the I'm not sure I understand the meta-transaction example, can you get into a bit more details on how it'd work? |
@shrugs thanks for mentioning Anyway now it's important to set interface for @meronym, @syakunin, @ptrwtts have you looked on |
@chompomonim In general I like the Now the question is whether we should include a |
Good point. In your case it's better to have From other side if it's ERC777 send and wallet which supports it, you could not generate a lot of addresses, but ask people to set some data while transferring tokens. Then you'll not need to generate a lot of separate addresses. Unfortunately we're not in that world yet... |
Agreed. The point of one-time addresses is legacy support for clients (i.e. wallets or custodian exchange accounts) that don't allow for customization of the token transfer data. I expect that client/wallet support for ERC777 will take quite a bit to mature, and an intermediate solution (such as |
After some considerations I see that there is one more fix needed for If we're using To solve this problem we could add some kind of contract ChequeOperator is ChequeBouncer {
ERC777Token public token;
struct Agreement {
uint256 totalPaid;
address payer;
address beneficiary;
bytes data;
}
mapping(bytes32 => Agreement) public agreements;
constructor(address _token) public {
token = ERC777Token(_token);
}
function createAgreement(bytes32 _id, address _payer, address _beneficiary, bytes _data) {
require(_beneficiary != address(0));
require(_payer != address(0));
require(agreements[_id].beneficiary == address(0));
agreements[_id] = Agreement({
totalPaid: 0,
payer: _payer,
beneficiary: _beneficiary,
data: _data
});
}
function sendByCheque(bytes32 _agreementId, uint256 _amount, uint256 _fee, bytes _sig) public returns (bool) {
// Check if agreement exists
Agreement storage agreement = agreements[_agreementId];
require(agreement.beneficiary != address(0));
// Check if signature is valid, remember last running sum
address signer = signerOfCheque(_agreementId, _amount, _fee, _sig);
require(signer == agreement.payer);
// Calculate amount of tokens to be send
uint256 amount = _amount.sub(agreement.totalPaid).sub(_fee);
require(amount > 0);
// If signer has less tokens that asked to transfer, we can transfer as much as he has already
// and rest tokens can be transferred via same cheque but in another tx
// when signer will top up his balance.
if (amount > token.balances[signer]) {
amount = token.balances[signer].sub(_fee);
}
// Increase already paid amount
agreement.totalPaid = agreement.totalPaid.add(amount);
// Send tokens
token.operatorSend(signer, agreement.beneficiary, amount, agreement.data, "");
if (_fee > 0) {
token.operatorSend(signer, msg.sender, _fee, "", "");
}
return true;
}
} I like this idea but it's good only for frequent token sends among two sides for similar purpose (agreement). Also it could be implemented in many way. Token standard don't need to have any knowledge about that. It's just my suggestion. It's not very good for @meronym usecase. So let's have in function authorizeOperatorBySignature(address _operator, uint256 _nonce, bytes _sig) public;
function sendBySignature(address _to, uint256 _amount, bytes _data, uint256 _nonce, bytes _sig) public; I also propose to change There are no fee in |
Could you fix or delete code in #965 (comment) and #964 to remove security issue? Someone might think it's a good code and use it. |
@mg6maciej thanks for pinging me. After ERC777 got default operators I was willing to fix this proposal to use default operator for I deleted description from outdated #964 and updated main description of this issue. Does it solves your security concerns? |
Well done @chompomonim ! There are a few typos in the method definitions and subsequent calls: |
@meronym thanks for pointing typos. I just fixed them. |
After some thoughts and discussion around potential infrastructure build around this standard (some kind of nodes which would take cheques, get fee in tokens and persist them into blockchain), I see that more changes are needed for this proposal.
|
Another idea is that ERC965 could work well with ERC20 kind of tokens if they would have default operators there. Does anyone know if there is already any ERC for #777 type of operators but for ERC20? |
I think this should be augmented to be compatible with https://eips.ethereum.org/EIPS/eip-712 |
As far as I can see, there are currently 2 proposals for tokens that allow gasless token transfers:
Are there more proposals? |
There is also #1776 |
Hey everyone, I'm new to this, but this feature is very important to a use case for my project. What is involved in getting this proposal or similar proposals for gas delegation moving and implemented, and how long should I expect this to take? I'm most likely willing to put significant effort into helping this along come summer, I just need some guidance. |
The main problem is that there is needed additional standard for wallets and services which would agree on accepting tokens as fee and send transaction into Ethereum networks with your signatures. If you need that only for your own token, then you can use such techniques already today, you don't need to wait until any finalisations of one or another standard. But if you'd like to have such features in major wallets, then it's really not going to happen any time soon. |
@chompomonim ok, I see. can be made work (generically) in metamask (based app/sites), but not "for any wallet". fwiw, this gasless stuff is important IMO for UX though - practically (non pro users .. your mom;) anyways, thanks for hints and opinion! |
Hi everyone, just to confirm, are we agree that all this solution require than a third part actually "create" the transaction? I mean as an end user with only tokens on my account can I, with the signature mecanism, make a transaction published by a smart contract (which hold some ether to pay the gas), without an action from a particular owner? |
Not sure if this is entirely appropriate but I used this design for the basis of another token on the Zilliqa Blockchain. Just wanted to give some credit and say thanks for the background work you all did here. |
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. |
Preamble
Problem
The problem of tokens spending is that token owner have to have ETH in his account to pay for gas. So it's impossible to have pure token account. Even in ERC777 (#777) where you can have operator which can manage your tokens (and paying for gas), there still is same problem of lack of gas to initiate
authorizeOperator
call.Solution
Add default operator smart contract which would accept tx with signed permission to send tokens. This tx can be made by anyone who has tokens owner signature and is willing to pay for gas.
Possible implementation:
Additioanlly
On user's (wallet) side cheque creation could look like:
Later transaction could look like:
Use case
This kind of cheques could potentially be widely used. Example use-case:
Frequent payments use case situation where shop gives discount points for client in a form of tokens. Also client downloads special app which is not only loyalty app but also is some kind of wallet and stores private key.
Later, when client will want to use such points (e.g. could be many times per day), without depositing some amount of gwei into his 'token wallet', he will not be able to transfer tokens. Meanwhile this token wallet could sign cheque and transfer it to shop back. Then shop (or some another entity) using this cheque could transfer tokens while paying for gas by himself.
Merchant use case the merchant generates a one-time address (OTA) and associates it with an invoice/order. The buyer transfers tokens to OTA as a means of paying the invoice.
Now the merchant wants to sweep the tokens out of OTA, but OTA cannot pay for the gas since it has no ether. So the merchant needs to send yet another transaction to fund OTA with just enough gas so it can sweep the tokens out.
The cheque model solves this problem by allowing the merchant to call sendByCheque and get the tokens out of OTA while paying for the gas from his own (master) account. This is possible because the merchant already controls OTA's keys and can produce the signature for the check.
The text was updated successfully, but these errors were encountered: