15

I would like to call a contract and deal with the return values manually using EVM assembly in Solidity. For example, this should simply add two numbers together.

contract Test1 {
    function add(int a, int b) returns(int){  //Simply add the two arguments and return
        return a+b;
    }
    function() returns (int){  //If the function signature doesn't check out, return -1
        return -1;
    }
}

contract Test2 {
    Test1 test1;

    function Test2(){  //Constructor function
        test1 = new Test1();  //Create new "Test1" function
    }

    function test(int a, int b) constant returns (int c){
        address addr = address(test1);  //Place the test1 address on the stack
         bytes4 sig = bytes4(sha3("add(int256,int256)")); //Function signature

        assembly {
            let x := mload(0x40)   //Find empty storage location using "free memory pointer"
            mstore(x,sig) //Place signature at begining of empty storage 
            mstore(add(x,0x04),a) //Place first argument directly next to signature
            mstore(add(x,0x24),b) //Place second argument next to first, padded to 32 bytes

            call(5000, addr, 0, //Issue call, providing 5k gas and 0 value to "addr"
            x, 0x44, add(x,0x80), 0x20) //Inputs start at location "x" and are 68 bytes long, outputs start 128 bytes after x, and are 32 bytes long
            c := mload(add(x,0x80)) //Assign output value to c
            mstore(0x40,add(x,0x100)) // Set storage pointer to empty space
        }
    }

    function test2(int a, int b) constant returns(int c){ //Make sure the Test1 function works properly
        return test1.add(a,b); // (It does)
    }
}

The issue is that this is returning an Out-of-Gas error, originating from the call... line.

How can I fix this issue?

Tjaden Hess
  • 37,046
  • 10
  • 91
  • 118
  • Not a comment on your problem but just curious what putting assembly in Solidity is for and why not coding in Solidity except for mind challenge? – Nicolas Massart Jun 20 '16 at 21:43
  • 1
    There's some reasons here, and in this case it's because I need to retrieve a dynamically sized byte array from another contract, which can't be done automatically since the EVM needs to allocate memory for return data beforehand – Tjaden Hess Jun 20 '16 at 21:56
  • Thanks, very interesting even if it's out of reach for me to write such low level code – Nicolas Massart Jun 20 '16 at 22:06
  • 1
    You really should try, it's really useful and even just the simple things like copying memory into new variables makes dealing with arrays and strings way easier – Tjaden Hess Jun 20 '16 at 22:38
  • 1
    Perhaps, someday, who knows, when I will be very confident... – Nicolas Massart Jun 20 '16 at 22:40

1 Answers1

17

The error was due to an unhandled item on the stack that was left by the call opcode. The working and optimized relevant code is here:

    assembly {
        let x := mload(0x40)   //Find empty storage location using "free memory pointer"
        mstore(x,sig) //Place signature at begining of empty storage 
        mstore(add(x,0x04),a) //Place first argument directly next to signature
        mstore(add(x,0x24),b) //Place second argument next to first, padded to 32 bytes

        let success := call(      //This is the critical change (Pop the top stack value)
                            5000, //5k gas
                            addr, //To addr
                            0,    //No value
                            x,    /Inputs are stored at location x
                            0x44, //Inputs are 68 bytes long
                            x,    //Store output over input (saves space)
                            0x20) //Outputs are 32 bytes long

        c := mload(x) //Assign output value to c
        mstore(0x40,add(x,0x44)) // Set storage pointer to empty space
    }

Thanks to @chriseth for pointing out my error

EDIT:

As @Ilan pointed out, the final mstore is not strictly necessary, since we don't care about keeping that memory allocated. If the returned data is a heap object like an array, then you need to make sure that the memory stays allocated.

Tjaden Hess
  • 37,046
  • 10
  • 91
  • 118
  • The 5th parameter to call is measured in bytes, not bits. SE won't let me edit to correct because it isn't "a big enough change" despite it having massive impact on the accuracy of the answer. :/ – Micah Zoltu Dec 08 '17 at 06:16
  • 1
    Good catch, edited – Tjaden Hess Dec 08 '17 at 16:59
  • @TjadenHess

    question: when setting storage pointer to empty space. since input not needed anymore. can u use x + 0x20 like so: mstore(0x40,add(x,0x20));

    – Ilan Dshare Mar 05 '19 at 16:30
  • Actually, you don't need to set it at all, since you've already loaded the value onto the stack – Tjaden Hess Mar 06 '19 at 01:05
  • @TjadenHess after playing more with the code there are 3 issues here.
    1. setting output over input will not work in some functions, where some output value might be set before using input for the last time.
    2. free memory pointer should be set before calling other function which might use it as well.
    3. after function call was completed. the free memory pointer should be set back to x, which is the starting position it was before the function call happened.
    – Ilan Dshare Mar 06 '19 at 18:18
  • True, but that would be a pretty unusual pattern. This way saves gas and is pretty common in practice. 2) Memory is call-local, not contract-local so this can never happen. 3) True, as addressed above
  • – Tjaden Hess Mar 06 '19 at 22:00
  • @TjadenHess can you give a little more insight into the error? I'm getting involved with inline assembly. Without assigning the call output to success, what exactly happens? Does the following output load to c take the success boolean as an input argument (along with x) and error out? Many thanks. – Garen Vartanian Jun 10 '19 at 17:58
  • Exactly what happens depends on the compiler version. Anything after ~4.0 simply won't compile with this error. Basically, assembly variables correspond to locations on the stack. When we use the variable, the compiler generates a swap n operation where n is how far down the stack the variable is. In sol ~3.5 when we leave an extra item on the stack, the addresses are off by 1 and so the argument to the next function is whatever was 1 above x on the stack, giving an invalid memory address. – Tjaden Hess Jun 10 '19 at 22:42