Skip to content

Instantly share code, notes, and snippets.

@karmacoma-eth
Last active August 22, 2024 17:07
Show Gist options
  • Save karmacoma-eth/4f206a46dedc6da6808c1ccdef3262d0 to your computer and use it in GitHub Desktop.
Save karmacoma-eth/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

@karmacoma-eth
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

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