🥇 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}("")
Reverts on failure | Returns false on failure | |
---|---|---|
Sends all gas | .sendValue() |
.call() |
Sends 2300 gas | .transfer() |
.send() |
-
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
- low-level counterpart to
-
addr.call{value: amount}("")
- check with
(bool success, ) = addr.call{value: amount}("")
- check with
-
OpenZeppelin's
Address.sendValue(addr, amount)
[code]- replacement for Solidity's
transfer
- replacement for Solidity's
-
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);
}
- 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
andcallback
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
How to send ETH from a smart contract using Solidity (Oct 2022) by middlemarch
Just for clarify, the
.call()
method sends 63/64th of current gas according to EIP-150, not all gas