0

I know that x.transfer(amount) reverts if either one of the following two scenarios takes place:

  1. There is not enough ether in the calling contract, i.e., amount > address(this).balance
  2. Variable x is the address of a contract with a payable fallback function which reverts

I know that x.send(amount) fails silently if the first scenario takes place.

But will it fail silently also if the second scenario takes place, or will it revert?

My overall purpose is to iterate a queue of (address wallet, uint256 amount) tuples, and for each tuple pay the given amount to the given wallet, until the queue is empty or my balance is too low.

So during this process, I am not concerned about failures due to insufficient funds.

I am, however, concerned about the possibility of a wallet being a contract with a payable fallback function.

If this function reverts, then this wallet will be stuck at the beginning of my queue forever, denying payment from the rest of my debt-owners.

So the only solution that I can think of is replacing transfer with send, but even this solution is very partial IMO, so I would be happy to hear alternative suggestions.

Thank you!

1

OK, the answer to my initial question is that x.send fails silently even if the fallback function of x reverts.

Here is the Truffle test (web3.js v0.20.6 if I remember correctly) that I conducted in order to conclude this:

On-chain code:

pragma solidity 0.4.25;

contract MyContract1 {
    uint256 public successCount;
    uint256 public failureCount;

    function() external payable {}

    function test(address wallet, uint256 amount) external {
        if (wallet.send(amount))
            successCount++;
        else
            failureCount++;
    }
}

contract MyContract2 {
    function() external payable {}
}

contract MyContract3 {
    function() external payable {revert();}
}

Off-chain code:

contract("Test", function(accounts) {
    it("Test", async function() {
        hMyContract1 = await artifacts.require("MyContract1").new();
        hMyContract2 = await artifacts.require("MyContract2").new();
        hMyContract3 = await artifacts.require("MyContract3").new();
        await printState(hMyContract1);
        await web3.eth.sendTransaction({from: accounts[0], to: hMyContract1.address, value: 1000000000});
        await printState(hMyContract1);
        await hMyContract1.test(hMyContract2.address, 1000000);
        await printState(hMyContract1);
        await hMyContract1.test(hMyContract3.address, 1000000);
        await printState(hMyContract1);
    });

    async function printState(hMyContract1) {
        let balance = await web3.eth.getBalance(hMyContract1.address);
        let successCount = await hMyContract1.successCount();
        let failureCount = await hMyContract1.failureCount();
        console.log();
        console.log(`balance = ${balance}`);
        console.log(`successCount = ${successCount}`);
        console.log(`failureCount = ${failureCount}`);
    }
});

Printout:

balance = 0 successCount = 0 failureCount = 0

balance = 1000000000
successCount = 0
failureCount = 0

balance = 999000000
successCount = 1
failureCount = 0

balance = 999000000
successCount = 1
failureCount = 1

So replacing transfer with send is an optional solution, but I still think that it's not a very good one, so I would be happy to hear alternative suggestions.

1

It's tricky.

You say you have debt owners and you have to repay quite a few. Ideally, this uses a withdrawal pattern and you avoid iteration altogether. I realize this is not always feasible. In cases like airdrops there can be a strong desire to use a use "push" process to send a batch of transactions. I'll assume there is a good reason.

On the contract side

You can iterate over a list of distributions and log success/fail as you go. You will want to use .send() so the whole process isn't held up by a single failed transaction. Since the sender has control of batch size, this is a case where you can use arrays for input and unbounded iteration. The sender can determine the optimal batch size. This is more gas-efficient than a purist approach insisting on one .send() per signed transaction.

The client is likely a server, but could be a browser, etc. Let's assume server. I mentioned earlier that the contract should emit success/fail while it thunders ahead undeterred by send failures. What to do with the send failures is a client-side concern, perhaps retry, perhaps a withdrawal pattern, perhaps give up. I merely assume the server needs to be informed.

On the sender side

A less obvious concern is nonce management. Whether batches or single transactions, to get significant volume through, the server and blockchain will probably use an asynchronous interface. That is, the server won't wait for confirmation before sending the next transaction. Consider deliberate throttling.

The server won't know the transactions were successful until sufficient confirmations are received. So there's the success/fail event and waiting for (10?) confirmations before declaring victory. I have found it's also important to have the ability to recall transactions that are overdue (maybe the gas was too low). You need to "recall" to have positive confirmation that the transaction is indeed failed and not merely indeterminant. This, so the server can confidently try again and be certain the end result won't be two payments to the same recipient.

In practice, this leads to a server that is tracking its pending transactions and their nonce perchance to emit a cancellation with the same nonce.

With so many pending transactions floating about, the client-side (say, geth) nonce is not reliable. Same for Infura. Pending transactions are subjective from the node perspective and a miss by even one will result in various errors and jam up the process.

In the end, you probably set up a singleton nonce counter of your own based on the idea that the sender is most qualified to know what was sent. More on the technical issues of infrastructure for this over here: Concurrency patterns for account nonce

Hope it helps.

  • Thank you very much for the extremely detailed answer. I have several questions, so I will ask them below in separate comments. – goodvibration Jan 20 at 8:51
  • I'll assume there is a good reason - the only reason that we do iterated payment instead of a single payment is gas cost, as calling the function once per debt-owner is going to cost a lot more (and we may have a lot of debt owners). – goodvibration Jan 20 at 8:53
  • You will want to use .send() so the whole process isn't held up by a single failed transaction - that was indeed my initial thought (which I have also explained in the question). However, I've just realized that a single transaction can, in fact, impact the rest of the process (thus denying the remaining debt-owners from receiving their funds). What if... a malicious user just loops forever and spends all the gas? send will not revert of course, just return false, but then, the iteration itself will be reverted due to out of gas. Am I wrong? – goodvibration Jan 20 at 8:56
  • To make it worse, even switching from iteration of many debt-owners, to handling a single debt-owner at a time, isn't going to help me solve the problem. This is because we maintain a FIFO of all debt-owners. So a "rogue user" could potentially deny service from all the other users (any debt-owner who happens to be in the queue after this owner). Once I have such user in my queue, it is stuck there forever. I could add an onlyOwner function to clear malicious users from the queue, but that would reduce the transparency of my system, as I could technically remove any user that I want. – goodvibration Jan 20 at 8:59
  • We're in danger of the chatty comments warning. Q1: Yes. Gas is the usual reason. Q2: Try to make the sender pay for their own gas so they only hurt themselves. Sender control of batch size to make it sender-managed. Yes, all reverts when out of gas. Q3: DoS is a concern. FIFO may not be ideal, especially when there is no minimum so trivially cheap to spam the process. Need to make it expensive to attack and pointless to try. Economic concept of "moral hazard" applies: users shouldn't be able to transfer costs/risks to others. – Rob Hitchens - B9lab Jan 20 at 16:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.