3

I was inspired by another question to learn more about stack depth attacks.

When I try to run attack(1023) in Remix on this contract:

pragma solidity ^0.4.14;

contract Attacker {
    uint x;

    function attack(uint y) {
        if (y > x) {
            this.delegatecall(bytes4(sha3('attack(uint256)')), --y);
        }
        else {
            throw ; // doesn't matter, we never get here
        }
    }
}

Using 3M gas, I run out of gas 228 calls deep. Toward the end the DELEGATECALL instructions seem to use about 1.5k gas. But on the first DELEGATECALL, I'm seeing 94k. Here are the steps I use to generate that number:

  1. Create above contract in Remix using Javascript VM
  2. Call attack(1023)
  3. Click into the debugger after it runs out of gas
  4. Step through with the "Instructions" bar open until just before DELEGATECALL
  5. Under "Step detail" note "remaining gas" of 5977987
  6. Click "Step into forward"
  7. Under "Step detail" note "remaining gas" of 5883892
6

Update. I'm leaving the answer below in place as it has some interesting points, but I think I've finally got to the bottom of this.

The key point is buried in the Subtleties page:

A CALL or CREATE can consume at most 63/64 of the gas remaining at the time the CALL is made; if a CALL asks for more than this prescribed maximum, then the inner call will only have the prescribed maximum gas regardless of how much gas was asked for.

This is explained quite clearly in the YP, so I don't know how I missed it :-) enter image description here

So, your CALL is not using up that much gas, but rather you are not able to pass all the gas you want to down to the next CALL in the recursion. If your initial gas is around 6.4M, then your first CALLed contract will receive around 100k gas less than you thought it would. I think this is what you are seeing. The surplus gas that doesn't get passed on to the CALL remains unused at contract termination, it is not used up.

So - not a Remix glitch (sorry, Remix!). If anything, it looks like testrpc is not enforcing this.


I think this is a reporting issue with Remix. I've run a minor variation of your contract against testrpc (i.e. not Remix - in my own environment) and don't see the same.

pragma solidity ^0.4.13;

contract Attacker {
    uint256 x;

    function attack(uint256 y) payable returns(uint256) {
        if (y > 0) {
            this.delegatecall(bytes4(sha3('attack(uint256)')), --y);
        }
        else {
             // Save the remaining gas in storage so that we can access it later
             x = msg.gas;
             return;             
        }
    }
}

The starting y is then the number of times delegatecall is used. It leaves the remaining gas amount in storage which can be accessed with eth_getStorageAt.

Results (y is the number of delegate calls, the value is gas remaining, diff is the difference in gas between this and the previous).

  • y=0, 2978464
  • y=1, 2977288, diff = 1176
  • y=2, 2976176, diff = 1112
  • y=3, 2975064, diff = 1112
  • y=4, 2973952, diff = 1112

In each case the difference is 1112 gas, including the difference between zero delegatecalls and one delegatecall (except for an extra 64 gas which I think is due to the extra zero byte in the initial calldata when y=0; this is charged at 4 gas rather than 68 for a non-zero byte).

This is essentially what I'd expect, which is why I think Remix must be a bit wonky here.

Edit: Just to confirm Remix wonkiness. With this same program: from the internal measure of gas (msg.gas placed in storage), Remix reports that each extra time round delegatecall a further about 45000 gas is used; however contract execution cost reported by Remix on the contract tab increases by only 1339 gas per delegatecall.

Note that the gas is calculated before the sstore of x, so we don't see the (large) cost of this. But it doesn't matter since it's a one-off.

  • 1
    Great, I opened an issue on remix: github.com/ethereum/remix/issues/539 – carver Aug 3 '17 at 19:40
  • 1
    @carver - I think I've got to the bottom of this. Please see the update I made to the answer. Quite subtle. I now think Remix is in the clear on this. Apologies for intial misdirection. – benjaminion Aug 5 '17 at 7:39
  • Subtle, indeed. That explains why "gas used" totals are different from "gas remaining" reports in the debugger tab, also. I've closed the issue on remix. Wow, that yellow paper section is obtuse. It took me a while, but found the relevant definition above it: L(n) = n - floor(n/64). Opened this issue on testrpc instead: github.com/ethereumjs/testrpc/issues/367 @benjaminion Can you tell me your testrpc version/environment to add to the issue description? – carver Aug 6 '17 at 17:57
  • 1
    @carver - I've added version info on your report for testrpc; you are a good citizen! As far as I can tell, this behaviour was introduced in the EIP150 fork precisely to counter stack-depth attacks. – benjaminion Aug 6 '17 at 18:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.