RIP-7614: Expose call stack to contracts

Some thoughts about how this will improve security:

  1. 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 )
  2. 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 )
  3. 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.

2 Likes

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.

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?

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!

1 Like

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.

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.

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!

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

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?

@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.

3 Likes

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++;
}