Skip to content

Instantly share code, notes, and snippets.

@0xkarmacoma
Last active December 2, 2024 19:23
Show Gist options
  • Save 0xkarmacoma/4f206a46dedc6da6808c1ccdef3262d0 to your computer and use it in GitHub Desktop.
Save 0xkarmacoma/4f206a46dedc6da6808c1ccdef3262d0 to your computer and use it in GitHub Desktop.
Sending Ether Cheat Sheet

Sending Ether Cheat Sheet

TLDR

🥇 Instead of sending Ether, use the withdrawal pattern

🥈 If you really need to send Ether, use a safe wrapper like OpenZeppelin's Address.sendValue(addr, amount)

🥉 If you really need to send Ether without dependencies, use (bool success, ) = addr.call{value: amount}("")

Nifty Table

Reverts on failure Returns false on failure
Sends all gas .sendValue() .call()
Sends 2300 gas .transfer() .send()

More details

  • addr.transfer(amount) [docs]

    • not recommended anymore, because during hard forks the gas price of certain opcodes can change (meaning that it can break some transfers that used to work) [Consensys blog post]
  • addr.send(amount)

    • low-level counterpart to .transfer(), should be avoided for the same reason
  • addr.call{value: amount}("")

    • check with (bool success, ) = addr.call{value: amount}("")
  • OpenZeppelin's Address.sendValue(addr, amount) [code]

    • replacement for Solidity's transfer
  • there is also the "WETH fallback" technique: if the call to transfer ETH failed, one can wrap the value in WETH. For example, from ZORA v3:

(bool success, ) = _dest.call{value: _amount, gas: gas}("");
// If the ETH transfer fails (sigh), wrap the ETH and try send it as WETH.
if (!success) {
    weth.deposit{value: _amount}();
    IERC20(address(weth)).safeTransfer(_dest, _amount);
}

⚠️ Always be mindful of these:

  • Ether transfer can always cause code execution, so be careful about reentrancy
  • it is always possible to force Ether transfer into a contract (e.g. using selfdestruct(x)), see forceSafeTransferETH
  • when the recipient of a transfer (i.e. a call with empty calldata) is a contract, its receive() external payable function is called if it exists [receive-ether-function docs]
  • if it does not exist, its fallback function is called
  • in your receive and callback functions, make sure you use less than the 2300 gas stipend (but beware that the pricing of opcodes can change during hard forks)
  • the EVM considers that calls to non-existing contracts always succeed. Solidity normally includes an extcodesize(addr) != 0 check before calling other contracts, but this check is not included for low-level calls on addresses (.transfer(), .send() and .call())
  • From the sending-and-receiving-ether docs again:

Sending Ether can fail due to the call depth going above 1024. Since the caller is in total control of the call depth, they can force the transfer to fail

Further reading

How to send ETH from a smart contract using Solidity (Oct 2022) by middlemarch

@zitup
Copy link

zitup commented Jun 14, 2024

Just for clarify, the .call() method sends 63/64th of current gas according to EIP-150, not all gas

@0xkarmacoma
Copy link
Author

Just for clarify, the .call() method sends 63/64th of current gas according to EIP-150, not all gas

yes that's right, "all gas" is a shorthand

@hcheng826
Copy link

if .transfer() and .send() are not recommended, what's the correct pattern if I want to limit the gas on the destination address (also 2300 gas force EVM to be read-only), so I have less to worry about the reentrancy risk? The comment in the OZ's note says that is still subject to reentrancy risk.
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/dac63c4612df765c9d0f24d9148e912b8cd5032f/contracts/utils/Address.sol#L28-L31

@0xkarmacoma
Copy link
Author

if .transfer() and .send() are not recommended, what's the correct pattern if I want to limit the gas on the destination address (also 2300 gas force EVM to be read-only), so I have less to worry about the reentrancy risk?

if you want to guarantee that you're safe from reentrancy, you can add a nonReentrant guard (which adds cost)

if you want max control over gas you can do something like this:

(bool success, ) = addr.call{gas: g, value: v}("")
if (!success) {
   revert("transfer failed");
}

some rules to choose the gas amount g:

  • g > 2300: (we now know 2300 is too low, may not be resilient to opcode repricing)
  • g <= gas(): forwarding all available gas may be too high (recipient has wiggle room to write state, do reentrancy, gas bomb you...)

so maybe something in between? it's hard to come up with a definite answer that works for everyone. To be honest g = gas() feels fine and future-proof to me, as long as you're aware that your target may try to reenter you or exhaust all your gas.

@KrishBaidya
Copy link

KrishBaidya commented Dec 1, 2024

In the withdrawal pattern the user has to call two different functions to withdraw their money. So, here is a ChatGPT's response

mapping (address => uint) pendingWithdrawals;

function sell(uint256 eth_amount) public {
    //...

    // Update the pending withdrawals mapping
    pendingWithdrawals[msg.sender] += eth_amount;

    // Auto-withdraw in the same transaction
    _autoWithdraw(msg.sender);
    
    //...
}

function _autoWithdraw(address user) internal {
    uint256 amount = pendingWithdrawals[user];
    require(amount > 0, "No funds to withdraw");

    // Reset the pending withdrawals for the user before sending the funds to avoid reentrancy attacks
    pendingWithdrawals[user] = 0;

    // Transfer the Ether to the user
    (bool success, ) = user.call{value: amount}("");
    require(success, "ETH transfer failed");
}

Is that safe to use? Since everything in this code is still the same as in a contract without withdrawal pattern, except for a mapping pendingWithdrawals

@0xkarmacoma
Copy link
Author

@KrishBaidya no this is dumb in so many ways I don't even know where to start:

In the withdrawal pattern the user has to call two different functions to withdraw their money

that's not true, all you have to do is call withdraw() and you will get the accumulated balance

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment