Title: RIP_7614__Expose_call_stack_to_contracts
Category: RIPs

Author: canercidam
Date: 2024-02-07T13:43:21.011Z

Hi Magicians!  
We would like to propose an improvement to increase visibility in L2 EVM execution.
https://github.com/ethereum/RIPs/pull/10
What are the proposed changes?
We propose a new call stack implementation to record opcodes, addresses and function selectors and expose them through a precompiled contract interface.
If implemented, the precompile will give protocols deeper visibility into addresses involved at any point in execution. The added visibility enables more robust exploit prevention mechanisms, including transaction screening.
Why are we recommending this and how do we believe the user/developer experience will be improved?

This improvement is rooted in the authors’ common goal to prevent smart contract exploits.
Transaction screening is an emerging and promising exploit prevention approach. Here’s an example - a DeFi protocol may screen incoming transactions against a negative reputation list and/or for anomalous activity outside of normal user behavior, and revert transactions deemed malicious.
Unfortunately, transaction screening techniques are susceptible to evasion. DeFi protocols only have visibility into msg.sender and tx.origin fields of a transaction, meaning hackers can hide behind various proxies.
We are recommending this improvement to enable more robust security mechanisms that collectively deliver great protection for DeFi protocols and user funds.

Eager to hear the community’s thoughts and feedback on this improvement. Your insights will be invaluable in refining the RIP. We’re looking forward to an insightful discussion!

Author: aneurablock
Date: 2024-02-07T16:15:58.445Z

Hello Magicians community,
For the past two years, we’ve been working on machine learning models that enable the earliest detection of hacks, including zero-days, based on opcodes. Therefore, exposing the call stack to contracts opens the door to blocking malicious contract calls without affecting daily business operations. This allows for the creation of a new ecosystem of protection where different companies work with products that can detect and block malicious attacks, thereby protecting and enhancing the blockchain ecosystem. At NeuraBlock, we are fully in support with @canercidam of the need to implement opcode recording, addresses, and function signatures by exposing them through a precompiled contract interface to prevent attacks and avoid the obfuscation of malicious transactions.

Author: Idan-Levin
Date: 2024-02-07T18:37:01.423Z

This pre-compile is really important because the way the EVM is built today, exploit prevention solutions are really easy to avoid by using proxy contracts.
And if this won’t be changed, it just increases the motivation to insert a centralized middleman that can process the chain of contract calls offchain, and then to act as a firewall - which only opens the door to opaque policies and potential offchain censorship. Visibility into the contract calls history onchain is important so that threat prevention solutions will also live onchain, and not with opaque offchain logic.

Author: ernestognw
Date: 2024-02-07T20:03:22.790Z

I’m pretty sure this is the correct approach for on-chain security prevention. Right now we rely on off-chain mechanisms for security.
Thanks for putting everything together @canercidam !

Author: tkstanczak
Date: 2024-02-08T11:44:40.597Z

COI disclosure: I am a Forta Council member and Forta team has been working on preparing the proposal
I have reviewed the proposal contents, suggested some improvements to the early versions that were taken into account.
I support bringing the idea for the discussion at the core devs call as any topics on improvement of user security would be welcome.

Author: assafIronblocks
Date: 2024-02-08T13:30:18.417Z

as on-chain firewall builder i can say that having the ability to get the call trace can benefit on-chain security - which means more decentralized centric solutions (like on-chain phishing-blocker / sandwich-blocker) can use that without compromising the web3 ethos.
this ability can be used also for other purposes along with the security - account abstraction users and protocols can benefit from this especially with its first phases.

Author: hal2001
Date: 2024-02-09T10:20:39.160Z

Full disclosure: I am a Forta Council member and the Forta team preview this proposal with me.
My views: transaction screening is becoming critically important for exploit mitigation. We need these features on L2s as L2 usage grows—we won’t be able to safely scale L2 applications without these techniques.
This proposal seemingly has few downsides, and a lot of upside. Feels like an easy win for the ETH ecosystem.

Author: kaspaw
Date: 2024-02-09T14:34:24.079Z

I really like the effort and it’s definitely a step in the right direction. Without a deeper insight into the call stack, I doubt there is a way to effectively block malicious transactions on-chain.
This proposal does not compromise our ethos. It merely provides the devs with information that wouldn’t be accessible to them otherwise.
I support this idea. Thanks @canercidam for starting the discussion

Author: ArielTM
Date: 2024-02-10T18:33:44.826Z

This is a step in the right direction. On-chain prevention has proven to be the only way to effectively block malicious exploitation of smart contracts. Allowing observability to the call stack is one of the strongest tools in the protection arsenal

Author: kladkogex
Date: 2024-02-12T15:45:07.341Z

Arguments against would be
*- it can break composability and enable censorship. Basically a dapp would be able to censor other dapps along the call chain. *
- it can enable new harder-to-analyze attacks since the attackers would be able to analyze stack info
- complexity of resulting code would make security audits more expensive
Overall, modern languages that target security go into making things more simple vs making them more complex.

Author: ajbealETH
Date: 2024-02-12T21:13:35.882Z

Thanks @kladkogex. Can you expand on why security audits would be more expensive as a result of L2s adopting this precompile? The authors were envisioning this being implemented at the chain level, not the dapp level.

Author: kladkogex
Date: 2024-02-13T13:00:27.002Z

Well the price of the security audit depends on the complexity of stack.
If inside the code there are security protection that read information from outside the current function, auditing these protections will be really hard.

Author: ulerdogan
Date: 2024-02-13T14:52:15.829Z

It’s definitely a new approach for precompiles in terms of implementation and use-case, congrats on the proposal.
My questions are:

It would be really nice to see some example use-cases by dApps to understand your vision for utilizing this precompile. Then, it will be easier to imagine how this is useful. I am curious how generalized precautions can be detected by utilizing CallStack. Also, even if generating CallStack is a negligible overhead, how do you expect the gas costs of utilizing CallStack. As a dApp developer, I can’t imagine how the CallStack can be analyzed or dApps can be designed to predict security issues.
How much do you think this development appeals to all developers? I feel like it’s an additional and a bit complex security responsibility.
Do you plan to provide the exact specs of the precompile contract itself? It should be defined in the proposal without the need for reference implementation.

My recommendation is:
I expect the precompiles as pure functions to replace some other functions in a smart contract which consumes huge amount of gas or be able to execute some functionalities that cannot be achieved by Solidity etc. Also, the reference implementation itself shows that this precompile should have a divergent implementation than other precompiles.

Is there a specific reason that you have used a precompiled contract to expose the CallStack?
Can the same functionality be provided by a new Opcode?

Author: canercidam
Date: 2024-02-13T22:08:04.531Z

Thanks a lot for the feedback @ulerdogan -  great questions!


It would be really nice to see some example use-cases by dApps to understand your vision for utilizing this precompile. Then, it will be easier to imagine how this is useful. I am curious how generalized precautions can be detected by utilizing CallStack. Also, even if generating CallStack is a negligible overhead, how do you expect the gas costs of utilizing CallStack. As a dApp developer, I can’t imagine how the CallStack can be analyzed or dApps can be designed to predict security issues.
How much do you think this development appeals to all developers? I feel like it’s an additional and a bit complex security responsibility.


The screening mechanism implemented in the target/victim protocol contract (e.g. with the help of a Solidity library) would be retrieving the call stack through the precompile interface. So it could either be adopted with custom screening logic or by using a screening library that uses the precompile.
Such screening logic would need to do checks on the addresses i.e. read storage. Transactions of a regular user is simple enough that the amount of addresses and the call stack depth remains at a low level. So the gas cost of screening (precompile call + checks) remains small. If screening cost somehow scales, then it only hurts an attacker that sends a complex transaction. In any case, we consider these checks out of scope of this proposal since the only goal is providing more visibility to the protocol contracts. 


Do you plan to provide the exact specs of the precompile contract itself? It should be defined in the proposal without the need for reference implementation.


The proposal has two technical components: a call stack, and a precompile interface that exposes it. We outline in the Specification:

how the call stack should be defined and updated, without definining concrete client implementation details,
how the precompile interface should encode the call stack data and return to the caller in EVM.

We could do a little bit more explaining in the Specification if you feel that some bits are unclear.


Is there a specific reason that you have used a precompiled contract to expose the CallStack?
Can the same functionality be provided by a new Opcode?


Same question crossed my mind during the development of this proposal and, yes, the same functionality could be provided as an opcode but there are two challenges with it:

Adopting a new opcode is much harder than adopting a new precompiled contract. Testing custom opcodes are not easy and Solidity compiler would require changes to support it.
Evolving a precompiled contract is easier. If a future proposal suggests that the call data should be extended with new fields, then the encoding logic would have to change. Maintaining backwards compatibility after such changes in a precompiled contract is easier as all we have to do would be making the precompile accept a flag to turn on the new fields, and keep the old encoding if the flag is not provided. Maintaining this with an opcode looks harder.

Author: mratsim
Date: 2024-02-14T09:14:34.228Z

I don’t see how you would even start to prove this in ZK in an efficient manner.
When proving something with dynamic size, you need to prove all code paths, all sizes.
Here, everything is dynamic and as gas cost increases, you would have an exponential amount of possible states to prove.
This looks also like quite tricky to get right, test and fuzz and looks like very bug prone. In regular programming, manipulating the stack is very tricky to get right, see discussions around longjmp and setjmp.
In general, opcodes that are non-deterministic are avoided in the EVM, which is why there are proposal such as EIP-3690: EOF - JUMPDEST Table (EIP-3690: EOF - JUMPDEST Table) to ease execution analysis. This precompile goes in the complete opposite direction.
Finally all the other precompiled contracts are pure functions, with very well understood results. This one looks like even more complex than feu SELFDESTRUCT.

Author: canercidam
Date: 2024-02-14T10:37:23.527Z

Hi @mratsim, thanks a lot for the feedback! We find the adoption of this precompile by zkEVM rollups very important.
We are proposing a call stack that is:

completely separate from the stack used in every call frame,
is a singleton per EVM transaction simulation and not redefined every frame,
manipulated only by CALL, CALLCODE, DELEGATECALL and STATICCALL opcodes upon enter (push) and exit (pop),
exposed through a precompiled contract interface.


When proving something with dynamic size, you need to prove all code paths, all sizes.


Here, everything is dynamic and as gas cost increases, you would have an exponential amount of possible states to prove.

If execution of CALL, CALLCODE, DELEGATECALL and STATICCALL can be proven, I wonder why the call stack could not be, given that the call stack state at any point could be deriven by looking at the opcodes that appear in the trace.

This looks also like quite tricky to get right, test and fuzz and looks like very bug prone. In regular programming, manipulating the stack is very tricky to get right, see discussions around longjmp and setjmp.

We propose that it is only the opcodes which I listed above manipulate the call stack - nothing else. The variety of pushed and popped data is very limited and it is done by the opcodes. Would you still agree that it is still hard to test and bug prone?

Finally all the other precompiled contracts are pure functions, with very well understood results. This one looks like even more complex than feu SELFDESTRUCT.

The “precompile” is an interface that merely reflects the current state of the call stack. It only encodes an array of elements, which is a very predictable process. The reason why we choose a precompile interface is because it looks easier to adopt and evolve.
We are very curious to know if there is anything we can do to make the call stack and the precompile more ZK friendly. Looking forward to hearing more suggestions about this, thanks!

Author: rpolysec
Date: 2024-02-14T15:19:42.919Z

I don’t understand the implementation enough to comment, but after reading the dialogue here I’m also interested in seeing several concrete examples of how widely useful this would be. I’ve spent a lot of time in detection engineering for traditional cyber security and often we look for the OS to expose features that we think will make detection easier and after much effort we find there are actually very few use cases. I would like to see half a dozen or more concrete use cases with teams of developers outside of OpenZeppelin and Forta demonstrating how this would be a game changer vs. what is currently possible.
I’m also quite concerned about potential abuses. I would need a whole lot more detail before being comfortable supporting this.

Author: shemnon
Date: 2024-02-14T15:31:10.712Z

Some specific feedback on the RIP

The scope of the operations needs to be expanded, at least to CREATE and CREATE2, as those also create message frames.  Perhaps make it a general statement like “All operations that create a new message frame MUST push a Call to the CallStack. In the Cancun fork specification this list is CALL, CALLCODE, DELEGATECALL, STATICCALL, CREATE, CREATE2, and the initial call frame of a transaction.”
There needs to be a way to distinguish create transactions from call transactions.  Perhaps a pseudo opcode?
What about contracts that are hand written, or Fe, or other languages that do not follow the Solidity ABI? Such as MoveVM cross compiled to EVM, for example? Will signature checks be validated against new languages?
How are function signatures identified?  There is a large amount of Solidity ABI explicitly required by this spec, which is not good. A section describing how to retrieve that independent of anything outside the EVM needs to be included.  such as “The signature is the first four bytes of the call data for the message frame for CALL style opcodes, zero extended if necessary.  For Create style opcodes it is <pick a constant>”.  But per the prior point, are these reliable function signatures?
The function signatures can be faked, or collisions found. Access to the entire input arguments may be needed.
Java used to rely on stack checking extensively in it’s Applet sandbox defenses. The string of Applet exploits in the early 2010s usually involved a previously unknown and clever way to overcome the stack checking. Considering how composable Ethereum is I expect a lot of the techniques are portable.

Author: 0xalpharush
Date: 2024-02-14T16:46:43.204Z

I would like to see several specific examples of how this would be used in practice perhaps with past hacks as an example. It’s not really clear what class of bugs this is meant to prevent and why catching them at runtime is preferable to during development using static and/or dynamic analysis. Potentially, simple mechanisms like the one used by Arbitrum Stylus to detect reentrancy at runtime could be used depending on the motivation.
As it stands, aside from preventing interactions with specific addresses based on off-chain heuristics, I would argue that any security property that is violated is incorrect in it of itself and would warrant fixing instead of relying on this functionality. Adding precompiles that are only enabled on rollups involves rollups e.g. Arbitrum and Optimism each configuring their forks of go-ethereum to activate in hard forks and I haven’t seen any discussion of alternatives that would be more straightforward to maintain and not risk consensus issues e.g. activating the precompile on mainnet or prematurely on the rollups.
Additionally, I think that the added complex of reasoning about call stacks for developers, tooling (fuzzers and formal verification, for instance), and ZK proving (as others already noted) is under appreciated. For instance, the dynamic cost of calling the precompile is just one more consideration that must be examined carefully on top of the already complex gas accounting in the EVM and how it interfaces with Solidity. Now, every tool must implement support for this standard and have the ability to toggle it on/off based off the rollup and EVM version. This has already been error prone coordinating across solc, hardhat/foundry, and rollups for PUSH0; however, this could be said for any RIP and maybe is a separate discussion.

Author: ajbealETH
Date: 2024-02-14T17:06:42.393Z

Thanks for the feedback @rpolysec. Just a reminder that the precompile by itself is neutral. It is simply exposing call stack data to protocols that is already available to the caller, block builders, etc.
One exciting use case for the precompile is supporting exploit prevention; however, it must be paired with a screening solution (of which there are several that benefit from this add’l visibility) before it can have an impact. The authors aren’t suggesting a specific approach - there are pros and cons to negative reputation, positive reputation and anomaly-based screening. Screening is possible today, but it’s less robust than it could be due to the limited visibility dapps have into the call stack.
We’ll pull together some material on the impact on exploits. In the meantime, would like to understand some of the abuse patterns you are thinking about. Can you share more?

Author: ArielTM
Date: 2024-02-14T19:09:32.869Z

Some thoughts about how this will improve security:

Efficient Reentrancy guard - Today, reentrancy guard is implemented using modifiers and a dedicated storage slot. Using the precompile will enable a more gas-friendly reentrancy check and will also reduce the problems caused by bad integration of the current reentrancy guard solutions (we’ve summarized some of the less effective reentrancy guard integration here - https://www.spherex.xyz/post/reentrancy-guard-2-0 )
Static Reentrancy Guard - Utilizing the precompile will introduce, for the first time, a countermeasure to combat static reentrancy attacks (which wasn’t possible since reentrancy guard uses a storage slot) - see https://www.youtube.com/watch?v=8D5ZJyU-dX0 )
In the context of SphereX - We have a unique approach of analyzing a given protocol history and checking during execution of future transactions that the transaction effect on the protocol doesn’t differ from how the protocol used to operate (in a nutshell). Let’s take the Poly hack as example. Our on-chain engine would have been able to notice, using the precompile, that this is the first time putCurEpochConPubKeyBytes was called from EthCrossChainManager (of course, this would have been detectable using msg.sender, but think of the generalized case) and actually revert this transaction

We’ve analyzed numerous of previous hacks and proved our solution could have prevented them. Using the precompile increases the number of ways we can check transactions against the protocol history.

Author: rpolysec
Date: 2024-02-14T20:18:30.260Z

Had a chance to listen to the call and Mamy Ratsimbazafy seemed to voice the concerns I’d want to look into more. The potential unintended consequences of introducing additional complexity at this layer. This seems like early days though so I’ll hold further comment. There’s really aren’t enough details here for me to really understand it deeply enough.

Author: mds1
Date: 2024-02-14T23:37:05.406Z

I largely agree with the questions and comments raised above. This feels like a complex feature and I’d like to see concrete examples of how it’s expected to improve security. My understanding of the proposal’s intention is that the precompile is used in conjunction with off-chain early detection services as follows:

Bot detects newly deployed contract that looks suspicious.
Service provider adds that address as an entry in some SuspiciousAccounts contract they maintain.
Smart contracts can call the precompile for the accounts in the call stack, and compare them against SuspiciousAccounts entries to determine if they should revert.

If that’s correct, this precompile is based on the assumption that attackers will deploy suspicious looking contracts with enough time for service providers to react and update their SuspiciousAccounts contract. In which case, wouldn’t attackers just execute their exploits in initcode (which is already being done, for example this exploit) rendering this precompile ineffective for it’s primary use case?

Author: canercidam
Date: 2024-02-16T22:30:11.471Z

Hey all, thank you so much for your feedback! We made some updates to the proposal to improve and clarify some points based on the recent discussions. Please see this this diff.

Added CREATE and CREATE2 opcode frames to the call stack as per @shemnon’s suggestion
Mentioned the encoder compatibility with different languages that compile to EVM
Improved the gas cost explanation
Extended the Rationale to address frequently asked evasion cases

Let us know what you think!

Author: canercidam
Date: 2024-02-16T23:10:40.309Z

Thanks @shemnon, super useful suggestions!


The scope of the operations needs to be expanded, at least to CREATE and CREATE2, as those also create message frames. Perhaps make it a general statement like “All operations that create a new message frame MUST push a Call to the CallStack. In the Cancun fork specification this list is CALL, CALLCODE, DELEGATECALL, STATICCALL, CREATE, CREATE2, and the initial call frame of a transaction.”


I updated the CallStack description with your feedback, in this diff. I’m curious what you think.


There needs to be a way to distinguish create transactions from call transactions. Perhaps a pseudo opcode?


Now that we have added CREATE and CREATE2 frames to the call stack (along with the initial frame, regardless of what kind), the screening mechanism could reason about that based on the oldest call frames in the call stack. If a transaction is checked, that would be the opcode of the first frame in the list. If an ERC-4337 UserOperation is checked, then it would be another intermediary frame.


What about contracts that are hand written, or Fe, or other languages that do not follow the Solidity ABI? Such as MoveVM cross compiled to EVM, for example? Will signature checks be validated against new languages?


I put some thought in to this. It seems like Fe would support this encoding. And it doesn’t seem to be problematic for any other language which somehow wouldn’t because the raw bytes returned from the precompiled contract could easily be parsed to form a CallStack array - the encoding is simple.


How are function signatures identified? There is a large amount of Solidity ABI explicitly required by this spec, which is not good. A section describing how to retrieve that independent of anything outside the EVM needs to be included. such as “The signature is the first four bytes of the call data for the message frame for CALL style opcodes, zero extended if necessary. For Create style opcodes it is ”. But per the prior point, are these reliable function signatures?


I realized one mistake in naming so renamed “signature” to “selector”. Fe seems to use first four bytes as the selector. Using any other compiler would result in seeing the first four bytes of the call input and I currently don’t have a better idea than defaulting to that.
I updated the explanation with your suggestion regarding the CALL-style and CREATE-style opcodes.


The function signatures can be faked, or collisions found. Access to the entire input arguments may be needed.


We could add this but might require some flag in the precompile arguments to turn on and off full call data and might complicate encoding. Let’s wait and see if there is more demand for this.


Java used to rely on stack checking extensively in it’s Applet sandbox defenses. The string of Applet exploits in the early 2010s usually involved a previously unknown and clever way to overcome the stack checking. Considering how composable Ethereum is I expect a lot of the techniques are portable.


It’s hard to disagree to this and foresee what exploit patterns will be discovered, but we expect that the attacks will become much harder to design and execute as prevention solutions advance. We believe that having this kind of on-chain visibility improvement is the first step.

Author: canercidam
Date: 2024-02-16T23:41:16.971Z

Hi @0xalpharush, thanks for the insightful comment. We will provide more explanations but meanwhile, does our Motivation and new Rationale (“Evasion concerns”) in the proposal make sense? We are mainly trying to increase visibility on the contracts that can hide behind proxies. The main problem is that once a contract uses DELEGATECALL to launch the exploit, there is no way of knowing from the victim contract’s perspective that the logic contract could be a flagged one: msg.sender remains as the proxy.

As it stands, aside from preventing interactions with specific addresses based on off-chain heuristics, I would argue that any security property that is violated is incorrect in it of itself and would warrant fixing instead of relying on this functionality.

Totally agreed on fixing the problem at the source - getting a high quality audit is crucial in mitigating exploits. Unfortunately, the real world examples from the industry show that the exploits may happen despite the popularity of pre-chain checks. This is making real time attack detection and prevention more essential everyday.

This has already been error prone coordinating across solc, hardhat/foundry, and rollups for PUSH0; however, this could be said for any RIP and maybe is a separate discussion.

Great point. This is one of the reasons why we believe that a precompiled contract interface is easier to support, adopt and evolve.

Author: canercidam
Date: 2024-02-16T23:45:31.050Z

Great question. Since this has been asked several times in different forms, I felt the need to update the proposal Rationale to address this concern. Could you please take a look at the new “Evasion concerns” subsection in this diff and let us know what you think about it? Thanks!

Author: dror
Date: 2024-02-18T11:55:45.126Z

canercidam:

We propose that it is only the opcodes which I listed above manipulate the call stack - nothing else


Unfortunately, stack manipulation can happen on every opcode - in case of out-of-gas.

Author: canercidam
Date: 2024-02-19T13:44:19.173Z

The new “call stack” is separate from the machine stack and is only manipulated (push/pop) by certain opcodes. It is expanded when a frame enters and shrunk when a frame exits. Could you elaborate on how opcodes other than the listed in the proposal could affect this?

Author: ajbealETH
Date: 2024-02-21T16:16:41.809Z

@mds1 @rpolysec @0xalpharush In response to your comments, the authors put together this supplemental presentation to explain how exploits are detected in advance, how exploit transactions can be screened today, and the value the precompile provides in this context. If you still have outstanding questions after reviewing this material, let’s keep the discussion going.


docs.google.com



RIP-7614 - Use Case Material
This Slide is private

Author: dror
Date: 2024-03-17T11:33:22.030Z

canercidam:

Could you elaborate on how opcodes other than the ones listed in the proposal could affect this?


Any context exit modifies the call stack regardless of which opcode caused it.
In the following (contrived) example, the innerFunc will always abort on “OOG”, and thus will always exit its frame on “simple” opcode (push/add/jumpi)
function outerFunc() external {
  address(this).call{gas:1000}(encodeCall(innerFunc,()));
}

function innerFunc() external returns (uint count) {
  while (true) count++;
}