A current balance on an address or prior activity proves nothing. Please don't ever rely on such data.
Use the Withdraw Pattern
The withdraw pattern is a two-step process for sending Ether to an address:
Record a balance for the receiving address. For example, assuming you have a mapping of balances:
mapping (address => uint) balances;
Send Ether by recording a balance for that address:
balances[address] += amount;
In a separate transaction, the receiving address withdraws Ether up to the balance of its address using msg.sender.transfer()
. msg.sender
is crucial here.
The withdraw pattern prevents ever sending to 0x0 or an address with a typo. Instead, a balance is recorded, and the receiver must withdraw the balance. Since the contract only ever sends to msg.sender
, Ether can never be sent to an inactive address.
You're probably wondering, though, what happens if a balance is recorded for an inactive address? Doesn't that lock the Ether in the contract, which is essentially the same problem as sending to an inactive address? This leads us to...
Include an Undo Mechanism
If Ether is held in a contract balance for an inactive address, but not actually sent, there are a few ways you can deal with it. Since sending Ether with the withdraw pattern is a two-step process, if the second step hasn't occurred yet, oftentimes reverting the first step is exactly what you want.
Of course, whether or not reverting the first step is acceptable is application specific. But you can (and should) include the mechanism if it doesn't reduce the value-proposition of the contract itself. Your mileage may vary, but it's highly recommended that you include one, if not both, of the following.
- An explicit undo mechanism in the contract that the sender can call. Since the first step entails incrementing a balance, you can undo that by simply decrementing the balance and moving it wherever it should go. If the receiving address tries to withdraw now, it simply fails.
A timeout. Whenever you can, include a hard time limit, beyond which, funds cannot be withdrawn or they're returned to the sender. Since you sent by simply incrementing a balance, you can also record the time using block.timestamp
(aliased to now
in Solidity). Then, a revert-after-timeout function can be called by the sender to undo the operation.
For example, for payment channels, the timeout may be on the order of a day or a week. (Credit card authorizations usually lock funds for a week. If the funds aren't captured by the merchant within that time, they're released back to the payer.) Other things may be on the order of months or years. But it's rare that things need to be held indefinitely. As long as all parties are aware of the timeout, it prevents losing funds to inactive addresses due to typos or other mistakes.
See Ethereum Payment Channel in 50 Lines of Code for an example of using a timeout.
For more defensive programming techniques, see Solidity's security considerations and common patterns.