🥇 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
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:
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.