Update
Before Sep 2016 send
and transfer
pass all available gas to CALL op by default. This was changed in the Solidity compiler in this commit https://github.com/ethereum/solidity/commit/9ca7472089a9f4d8bfec20e9e55c4f7ed2fb502e.
// Provide the gas stipend manually at first because we may send zero ether.
// Will be zeroed if we send more than zero ether.
The above comments mean that if you send 0 ether the compiler will explicitly set the gas to 2300 for the CALL op. Otherwise the compiler sets 0 gas for the CALL op in which case the EVM will add 2300 gas stipend as explained on the Subtleties page.
The code that adds all available gas to the external function call is in ExpressionCompiler::appendExternalFunctionCall():
if (_functionType.gasSet())
m_context << dupInstruction(m_context.baseToCurrentStackOffset(gasStackPos));
else if (m_context.experimentalFeatureActive(ExperimentalFeature::V050))
// Send all gas (requires tangerine whistle EVM)
m_context << Instruction::GAS;
else
{
// send all gas except the amount needed to execute "SUB" and "CALL"
// @todo this retains too much gas for now, needs to be fine-tuned.
u256 gasNeededByCaller = eth::GasCosts::callGas + 10;
if (_functionType.valueSet())
gasNeededByCaller += eth::GasCosts::callValueTransferGas;
if (!existenceChecked)
gasNeededByCaller += eth::GasCosts::callNewAccountGas; // we never know
m_context << gasNeededByCaller << Instruction::GAS << Instruction::SUB;
}
The documentation was written in June 2016 https://github.com/ethereum/solidity/commit/2df142c49618138ba7f38f32a76022caecc68abb. Here is the pull request to fix it https://github.com/ethereum/solidity/pull/3197
Previous answer
Here is the transaction trace that demonstrates that it's possible to make a reentrant call: https://rinkeby.etherscan.io/vmtrace?txhash=0x2e77009bda0fc9c07a01a4589d9b426382521c6e04b84008ffda637a4268824f
On step 183 CALL operation is performed which costs 700 gas:
Step PC Operation Gas GasCost Depth
[183] 118 CALL 1189 1182 3
[184] 0 PUSH1 482 3 4
The reason it costs only 700 gas is because no value is passed with the call.
As explained on the Subtleties page in Ethereum's wiki:
CALL has a multi-part gas cost:
- 700 base
- 9000 additional if the value is nonzero
- 25000 additional if the destination account does not yet exist (note: there is a difference between zero-balance and nonexistent!)
The corresponding contracts code:
pragma solidity ^0.4.8;
contract Fund {
bool mutex = false;
function withdraw() {
if (!mutex) {
mutex = true;
msg.sender.send(1);
mutex = false;
}
}
// deposit some funds for testing
function deposit() payable {}
}
contract Attacker {
Fund f;
function Attacker(address fund) payable {
f = Fund(fund);
}
function attack() {
f.withdraw();
}
function () payable {
f.withdraw();
}
}
Note that while the original contract in Solidity docs is vulnerable to reentrancy attack it can't be exploited to send value more than once because the 2nd nested send
call would deplete the 2300 gas stipend. That's why in my example I had to use the mutex.
Note also that in the DAO hack call
method was used which passes all available gas to the nested call unlike send
which only passes the 2300 stipend by default.