“Stack Too Deep”- Error in Solidity

Are they placed in the stack before the data as well?And what is the impact of more event arguments, are they PUSHed after or before the topic?To test that, I’ll change the contract again.

Notice that events can have any number of arguments, and up to 3 of them can be indexed.

Indexed arguments become topics, while the others are lumped in the data section.

My hypothesis, at this stage, is that each topic (indexed argument) will be placed in the stack before the data, and so will prevent the access to more of the early variables.

In my tests, I covered several scenarios, but they all lead to the same conclusion so I will save you the minute details.

I will just illustrate with another interesting and counter-intuitive case, and then draw final conclusions.

First, let’s try this version of the contract, where the event has one indexed value and two non-indexed ones.

pragma solidity ^0.

4.

24;contract TestStackError { event LogValue(uint indexed a1, uint a2, uint a3); function logArg(uint a1, uint a2, uint a3, uint a4, uint a5, uint a6, uint a7, uint a8, uint a9, uint a10, uint a11, uint a12, uint a13, uint a14, uint a15, uint a16 ) public { emit LogValue(a2, a3, a4); }}The bytecode for this function (after the function dispatch) until the event is emitted is this:265 JUMPDEST266 DUP15267 PUSH32 a5397a5faa0ec7cfb89428503b91a13bbd737592f7561e6773fa3e1458c8735c300 DUP16301 DUP16302 PUSH1 40304 MLOAD305 DUP1306 DUP4307 DUP2308 MSTORE309 PUSH1 20311 ADD312 DUP3313 DUP2314 MSTORE315 PUSH1 20317 ADD318 SWAP3319 POP320 POP321 POP322 PUSH1 40324 MLOAD325 DUP1326 SWAP2327 SUB328 SWAP1329 LOG2The opcode that emits the event is LOG2.

This means we have two topics, one the default topic0 (ie the event signature) and the other the only indexed argument in the event signature.

The remaining two values are grouped in memory.

If we check Ethervm for this opcode, we see that the last value read from stack, and the first to be pushed onto it, is topic1, that is, the indexed argument — a2.

Initially, this is placed at position 15 of the stack.

The opcode DUP15 places a copy of the value at the top of the stack, and consequently pushes all the other arguments down.

From now on, for example, a2 is in position 16, and a1 is in position 17.

The next instruction pushes a 32-bit value to the stack, that simply corresponds to topic 0.

This value is hardcoded.

This also has the effect of pushing again the arguments down.

Now, a2 is in position 17.

The following instructions are two DUP16 opcodes.

The first one copies the value at position 16, which is currently the third argument, a3.

But since this pushes a new element onto the stack, when the next opcode is called DUP16 will copy the fourth argument to the function, a4.

At this stage, at the top of the stack we have the data for the event (two words), the indexed argument and the event unique identifier.

The following lines copy the first two values to memory:(302–305): places the contents of memory 0x40 at the top of the stack, twice.

This is the position in memory where the event data will be located (and is 0x80 in my execution).

(306–308): places the first data word at the first free position in memory (ie places a3 in position 0x80)(309–311):places the next free position in memory at the top of the stack(312–314): places the second data word at the next free position in memory (ie places a4in position 0xa0)(315–321): calculates the next free position in memory and leaves it at the top of the stack, after eliminating values that are no longer needed.

(322–327): finds the length of the data submitted to the event, by subtracting the initial address of next free position in memory from the current value of that position (held at the top of the stack).

(328): reorders the first two elements of the stack, making the first element the beginning of the event data, and the second address the length of this data.

(329): finally calls the logging opcode.

I gave this detailed explanation so that you can understand how this process works, if you wish.

In that case, perhaps you can now explain the next apparent oddity.

Change only the signature of the event to:event LogValue(uint a1, uint indexed a2, uint a3);Yep, another Stack Too Deep error.

Can you see what is causing it?………………The bytecode does not change much.

We still have the same number of topics, so the opcode at the end will still be LOG2.

And it still expects to receive its arguments in the same order, that is, the topics first, then the data.

Now, the second topic must be loaded first, so a3 would be the first value to be pushed to the stack with a DUP14.

Then topic0 would be pushed.

Now, the EVM would place at the top of the stack the two arguments it needs to store in memory, a2 and a4.

These were originally at positions 15 and 13.

However, the EVM has made two pushes already, which makes these positions 17 and 15.

It is impossible to place the first value in the stack (DUP17 does not exist) and so the compilation errors.

So now that we understand this, I try changing just one more thing, the log function to:emit LogValue(a3, a2, a4);This code works, since it corresponds very closely to the last block before I changed the order of the indexed arguments.

In that code, the indexed value of the event was called with a2.

In this version, it is still a2 that is passed to that position, and the others remain the same.

The bytecode explanation is virtually the same.

ConclusionThis has been a long post.

If you have arrived this far, it is worth leaving you with an organised view of what is happening, so that you can go back to your programs and think if your “Stack Too Deep” errors could have been caused by a similar behaviour.

Although this post covers only the case of emitting events, other functions will use other opcodes, but will still have the same logic, in copying the function arguments (or intermediate values) to the stack when some computation is needed.

So here are some streamlined notes to keep in mind:When a function is called, a stack frame is created.

This includes, from bottom to top:the function selectorthe return addressthe leftmost value-type argument of the function…the rightmost value-type argument of the function“Stack Too Deep” errors depend on the central opcode of an action (eg arithmetic, hashing, calling another function, emitting events, etc.

)If these central operations are performed on pure function arguments, the order in which they are passed to the function may decide the occurrence of a “Stack Too Deep” error.

(Stack slots can also be used for intermediate calculations and local variables, but I intend to study those in a later post.

)It is crucial to know the number and order of the arguments for the opcode.

These arguments are typically read from the stack (the only exception is the PUSH opcode).

Opcode arguments have to be pushed to the stack before executing the opcode.

Each PUSH moves the function arguments down at least one slot.

The function arguments deeper in the stack are the ones that were processed first, that is, the leftmost ones in the function signature.

If some of the function arguments are not used in that opcode operation, then they should come first in the function signature, to reduce the chances that opcode arguments will be off-reach when they need to be stacked.

Opcodes use arguments at different levels in the stack.

Deeper levels are pushed first.

If an argument is pushed after another, it should appear in the function signature after the former as well, otherwise it would push the other one down the stack before it could be used.

Example:Consider an event with two indexed arguments t1 and t2 in this order, that is called inside a function with several arguments, among which a1 coming before a2If the event is emitted with t1 = a1 and t2 = a2, the opcode LOG3 will be called.

Before calling this opcode, t2 = a2 will be pushed first into the stack.

This will push a1 down and put it at risk of being unreachable when the time comes to push the value of t1 = a1.

This would be avoided if a1 came after a2 in the function signature, since it would be higher in the stack than a2.

Assuming a2 was reachable when it was pushed, so would be a1 afterwards.

The above post concentrated on LOGn opcodes only, in particular on versions requiring 3 or 4 arguments in the stack.

A more difficult case will be calling functions in other contracts or libraries, since the opcodes CALL and DELEGATECALL take 7 or 6 input arguments each, with a lot more possibilities of interaction between the opcode and function arguments.

I hope this gives you some clues on how to debug and handle “Stack Too Deep” errors.

There is a lot more to say, but that will have to wait for other opportunities.

Until next time.

Alexandre Pinto — Blockchain developer at Artos (Aventus Ecosystem Party)Alex is a software engineer at Artos, our ecosystem partner, working on the blockchain engineering team.

He has 20 years of experience working in technology, completing a PhD in Computer Science as well as a post-doctorate in Cryptography.

As part of his research, Alex has published papers on Kolmogorov Complexity, Cryptography, Database Anonymization and Code Obfuscation.

Pinto also spent seven years lecturing at the University Institute of Maia, including directing the degree programmes for BSc Computer Science and Information Systems and Software.

This article was originally posted on his blog.

Since you are here, we would love if you connected with us on Telegram,Reddit, Twitter, Facebook, Youtube, Instagram and LinkedIn.

Also, we have started a LinkedIn Group for ticketing developers/other developers to engage, join us and start a conversation.

.. More details

Leave a Reply