EIP-1153: Transient storage opcodes

Currently, the rules indicate that reading or tload is view and writing or tstore is the default, i.e., neither pure, nor view.

The write cannot be made view unless the interaction with staticcall is changed. Currently staticcall + tstore reverts according to the spec.

I see. I am not deep into the implementation to understand the implications of changing the interaction with staticcall. But has there been a discussion elsewhere about this? Would there be significant downsides to changing this behavior, such that a tstore at least could be considered a view, assuming the compiler could guarantee that the slot is returned to zero at the end?

As mentioned in EIP-1153: Transient storage opcodes Specification

TLOAD pops one 32-byte word from the top of the stack, treats this value as the address, fetches 32-byte word from the transient storage at that address, and pops the value on top of the stack.

In the context of stack, the use of word pop may be ambiguous. It would be better to replace it with the word put.

1 Like

Fwiw, this is a EVM feature we would surely benefit from in Superfluid protocol.

Currently, in order to keep some information between several external calls, we pass a bytes memory ctx that is secured by a “bytes32 stamp” around.

Hi everyone, I hope that this is the right place to post this.

We would like to propose two update options for EIP-1153.

Motivation for the changes

The current version’s description, motivation, and reference implementation section state that “transient storage must behave identically to storage”. However, the specification lacks a "not allowed in low gasleft state” requirement as in EIP-2200 which breaks the equivalence of TSTORE and SSTORE semantics.

That allows for reentrancies with 2300 gas left, e.g. Solidity’s transfer and vyper’s send functions are no longer reentrancy-safe. That can also affect contracts with transient storage but without the usage of such functions. Namely, by breaking the assumed trust model of contracts it interacts with (e.g. an exchange). Existing contracts, however, are not affected, but need to be careful when interacting with new contracts.

Option 1: Enforce a gas left requirement

  • Suggested Change: Add a sentence similar as in EIP-2200. “TSTORE is not allowed in low gasleft state to keep the equivalence with SSTORE.” Additionally, update the reference implementation.
  • Rationale: Prevent breaking assumptions on reentrancy in low gasleft state - which is implemented mainly by Solidity’s transfer() and Vyper’s send().
  • Implementation changes: Expected to be minor.

Option 2: Clarify that it is not identical

  • Suggested Change: Replace “identically” with “similarly”. Clarify the differences in (at least) the specification section. Further, add a paragraph dedicated to the application layer in the security considerations section.
  • Rationale: Move away from these assumptions. Raising awareness of the differences and letting developers decide on how to handle them.
  • Implementation changes: None.

Please let us know if we should further clarify, and if we should write a PR after the discussion.
Feel free to read the related blog post for more details and see our repository with the examples. Or reach out to me or @hritzdorf.

The stipend gas bikeshedding change to SSTORE in EIP 2200 was a mistake (and it would be better to eliminate the callvalue stipend entirely). It was not important to prevent SSTORE within stipend gas. The threat was entirely imagined; nobody was using SSTORE as a mechanism to revert call-with-value. Therefore I do not want to extend that mistake into TSTORE. We did not add the same complexity to CALL, STATICCALL, and DELEGATECALL in EIP-2929 when it reduced their mingas to 100.

EIP-1153 says the storage mechanism is identical, not the gas. EIP-2200 is clear that the stipend gas check only applies to the out-of-gas calculation of SSTORE. EIP-1153 is not ambiguous about the operation of TSTORE; there is no reason to believe the stipend gas check is applicable.

Hello. On the EIP page, it mentions these 6 use cases:

  1. Reentrancy locks
  2. On-chain computable CREATE2 addresses
  3. Single transaction ERC-20 approvals
  4. Fee-on-transfer contracts
  5. “Till” pattern
  6. Proxy call metadata

Are there other (already known) important use cases not mentioned here?
Among these 6, is there a somewhat clear order on which ones will be the most used (I suspect reentrancy locks is the most common and straightforward use case of the above)?

I think this has been mentioned already, but I wanted to share some concerns that I have with regards to EIP-1153. I think it will enshrine EOA transactions in a way that will become very hard to patch later on, rendering smart contract wallets unusable or outright unsafe under some scenarios.

The main problem is that EIP-1153 assumes that a “transaction” only encapsulates the lifecycle of a single user, but in reality, this assumption only holds for EOAs. Smart contract wallet transactions can be batched, and when batched, a single transaction may represent N users.

Reentrancy locks

It has been discussed already about “not clearing reentrancy locks”, any dapp that doesn’t properly clear the reentrancy locks will render batching impossible across transactions interacting with them, this primarily hurts the users but in an indirect way, thus probably pushing developers to follow the anti-pattern.

This will cause chaos on any EIP4337 bundler trying to execute multiple operations at once.

Batching is not optional

The main problem is that it’s not the user who decides if a transaction will be batched or not, it is the bundler. The user may sign a transaction and without any control of it, this transaction may end up being executed batched alongside conflicting ones.

This costs nothing for the bundler or the dapp, since the bundler is paid and the frame reverts on the dapp. But the user has to pay for a “compatibility problem” (due to the reverted user operation) that is totally outside of their control, developers of smart contract wallets don’t have means to patch this either, the attack can be carried over by a malicious bundler.

Smart contract wallets become attack vectors

Any dapp assuming that the transaction only represents a single user will likely end up being vulnerable to an attacker using a smart contract wallet to circumvent any limitations put in place using transient storage opcodes.

Imagine a dapp that uses transient storage opcodes to implement the “fee on transfer” idea, if the transient storage isn’t cleared, then other smart contract wallets interacting with the contract can benefit from the fee paid by the first transaction, and the token contract has no way of knowing that the “first transaction” is now out of scope.

This can happen in any number of ways, KYC contracts, fee on transfer, access control, etc.

Smart contract wallets become vulnerable

I already mention the batching attack vector, it can be used to DOS a wallet that is trying to interact with a protocol that uses transient storage. But depending on the implementation, other, more critical, attack vectors may open.

The EIP mentions temporaryApprove as a use-case, but relying only on transient storage for temporaryApprove is dangerous, given that smart contract wallets may “leak” some leftover approve amount. If the protocol pulling the tokens doesn’t enforce access control on some other layer, then any subsequent batched smart contract wallet transaction could access the leftover approve.


I think these cases are enough to showcase that EIP-1153 further enshrines EOA transactions, making it difficult (or next to impossible) for smart contract wallet developers to provide solutions that won’t put users’ funds at risk.

Smart contract wallets are a core pillar of the Ethereum roadmap, and I don’t think the benefits of this EIP are worth the damage that they cause to them. Other efforts like EIP-4337 may be at serious risk if dapps implementing this transient storage model don’t implement it with extreme care.

1 Like

Thanks for increasing awareness to this important topic. While I don’t see it as enshrining EOA, it does require the attention of both AA developers and dapp developers as they start using TSTORE.

Transient storage is equivalent to static variables in C and Java. It enables similar use cases as well as similar bugs. It has many benefits and I don’t think we should be against it just because it’s possible to write buggy code. Over the years we’ve developed patterns and anti-patterns when using static. We should do the same here.

I don’t think it enshrines EOAs, as the bugs can also affect EOAs when using any form of batching (e.g. multicall or EIP-5806), CoW swaps, intents, etc. As you pointed out, it also affects ERC-4337 UserOps just like any other form of batching. But on the other hand it doesn’t affect native AA (RIP-7560), so the issue is not with AA but with batching.

Locking and then failing to release the lock, is a bug. The context that locked it, is guaranteed to have a chance to unlock before it ends. A dapp that fails to do so will break in batching, but also in other cases where it’s called more than once in the same EOA transaction. E.g. suppose a DEX would do it. An EOA would be able to execute a simple swap, if the DEX is used by another protocol (any form of DeFi composability), it may be called multiple times within the same transaction. Composability is currently more common than batching or AA, so a protocol that doesn’t free reentrancy locks has a bigger problem.

The same applies for intents, CoW swaps, etc. ERC-4337 accounts are just one thing that is affected by such buggy contracts. And since it doesn’t apply to RIP-7560, I wouldn’t say that it enshrines AA.

All the examples (KYC contracts, fee on transfer, access control) could be implemented in a way that doesn’t introduce such bugs. They just need to be aware that batching is possible. Ones that don’t will be incompatible with batching, composability, intents, etc. It’s the same kind of bugs that beginner C programmers tend to introduce when using global/static variables. The solution is not to ban these variables, but to educate devs on proper patterns.

Any reasonable ERC-4337 account probably supports batching, e.g. executeBatch. There’s no reason to leave an approval hanging when the operation is complete. Either the wallet should send a batch that sets the approval, makes the call, then cancels the approval, or the account itself (or an ERC-6900 plugin) could implement an approveAndExec which takes care of post-execution cleanup. So here, again, it’s just a matter of awareness. Account devs and dapp devs shouldn’t make an assumption about what happens after the operation ends, and should cleanup before exiting.

I think such bugs will also break EOAs very soon. On last week’s ACD call, it was argued that it is critical to add native batching capabilities to EOA, whether using EIP-3074, EIP-5806, or any other form of batching. As soon as a batching EIP exists, batching will become the norm for EOAs as well. Devs will just have to learn how to properly use transient storage.

The risk (not just to 4337 but to any form of batching) is caused by contracts that use TSTORE without batching in mind. Luckily we have the luxury of zero-legacy since TSTORE is only coming in the next fork. Now’s the time to educate devs on considering the batching use case (including calls from different accounts, as with ERC-4337, intents, and CoW swaps) when starting to use TSTORE.

By the way, a new ERC could use transient storage to introduce a context, with which ERC-4337 accounts, batching solutions, intents, etc. would be able to inform dapps about the “owner” of the current call. A singleton that manages a stack in transient storage and offers four functions:

setContext() - push msg.sender to the stack
getContext() - get the head of the stack
exitContext() - if (msg.sender == getContext()) pop the head of the stack
getFullContext() return the whole stack - would be helpful for things like KYC’ed CoW swaps where an operation is associated with multiple accounts that all need to be KYC’ed.

Let’s use the opportunity to develop good patterns to get the most out of EIP-1153 without introducing bugs.

1 Like

With EOAs, you can always be sure that (even if the user is making multiple calls) the whole transaction still represents one single user. In smart contract wallets, this doesn’t hold true, and EIP-1153 assumes that one transaction equals one user.

It is true that EOAs may be affected on something like Cowswap, but I don’t think the impact will be as high, since these sorts of “batched EOAs transactions” are usually very scope-limited.

It affects any sort of non-enshrined AA; if this becomes a problem, the only way to fix it will be by creating a top-level transaction that represents an AA operation.

If this is always a bad pattern, then tstore/tload (in its current form) is a footgun; it should be modified so developers MUST clear the transient storage. Maybe if they don’t, the transaction should revert.

I think in many contexts, contracts won’t be able to guarantee that their transient storage will be cleared unless they only use it in the context of a callback. But in the latter scenario, then regular storage + better refunds make more sense as a protocol feature.

I see it this way:

  • Any non-cleared transient storage usage will conflict with smart contract wallets.
  • Any “always cleared” transient storage can be implemented using regular storage.

So I don’t see how this EIP is anything but a footgun.

Today we already have a problem with smart contract developers littering their dapps with require(msg.sender == tx.origin), this feature further incentivizes them to lock out smart contract wallets.

I understand smart contract developers may use the feature correctly, but the reality is that they don’t have an incentive for doing so. Today, smart contract wallet users aren’t a critical mass, and it is likely that they may not even notice if their dapp breaks in only “some” wallets.

Not really (or at least not in the same way) as EOAs with batching still satisfy the assumption that one transaction represents a single user. I would go as far as saying that batched EOA transactions will aggravate the issue, as now developers will start using it for enabling composability, passing state using transient storage and thus further breaking smart contract wallets.

In reality, we are on time to modify this proposal in such a way that we can make sure developers can’t make these mistakes. Remember that many of these mistakes won’t directly hurt the developer’s app, but only some users instead.

I think intents are trying to change that.

Not necessarily. For example, ERC-4337 bundles of 1 UserOp are not affected. But that’s besides the point, I think the same applies to other forms of batching. Buggy contracts that make this one-user-per-transaction assumption will be vulnerable to an EOA that uses batching to aggregate operations of multiple users.

For example, if a contract wants to collect a transfer-fee only once per transaction and implements it incorrectly, I’m sure someone will run a relay that submits a multi-transfers batch using permit from multiple users, so any number of EOAs will be able to transfer in a single transaction. The bug will affect the contract itself, not just some users the dev doesn’t care about.

I didn’t say that it’s always a bad pattern not to clear tstore. I said it’s a bad pattern not to release a lock. There are other use cases of tstore where it makes sense not to clear because the storage is needed across contexts. In these cases a different pattern is required.

tstore is a footgun in the same sense that C/Java static variables are a footgun. Yes, they are, but they also have enough legitimate uses to have them around. They just require some patterns to avoid shooting yourself in the foot.

I think callback will make sense in many cases.

It may have been equivalent if we could get the numbers right to avoid introducing DoS vectors, but that’s a discussion for before EIP-1153 became final.

Unless an ERC solves that by standardizing contexts, as I suggested above. Contracts that need to use tstore that they don’t clear, can require a context. And since the context is set by msg.sender, it can’t be faked.

Why does it incentivize them to write buggy code and then try to prevent the bug’s manifestation through a check that will get deprecated in the future (tx.origin is going away sooner or later), rather than writing their code correctly and not having to deal with it?

They do, if their intended functionality breaks due to not doing so. E.g. a KYC contract that lets a non-KYC’ed user do stuff, would be better off implementing things correctly. Are they more incentivized to write buggy code and deliberately try to prevent smart accounts from using it (temporarily until tx.origin goes away and then they become exploitable)?

The point is that it’ll break their intended functionality, so users would exploit it. E.g. performing actions without KYC or avoid paying transfer fees by using batching+permits. They may not care about “some” wallets not being able to use them, but they’ll care when people use it to subvert their intention.

See batching+permits from multiple users above. Or EIP-3074 on chains that choose to implement it. The same transaction can perform actions on behalf of any number of EOAs using AUTH.

Are we?

status: Last Call
last-call-deadline: 2022-12-08

This EIP was written 6 years ago, and spent a few years in review. The deadline for comments was over a year ago, the implementation already exists in all clients, and testnets already forked to include it. I’d be very surprised if ACD considers halting the oncoming dencun fork to rethink this EIP unless a critical bug is seen on testnet.

At this point the discussion is moot since this EIP is already in the current fork. The only viable option is education on using correct patterns, education of auditors to point out such bugs during audits (and I think they’ll be fairly easy to spot), and creating standards (ERCs) that make it easy to use correctly.

Speaking of ERCs, doesn’t the context manager I suggested solve the problem? It gives contracts an easy way to split the transaction and maintain tstore in the context of each account. And it enables support for things like CoW swaps, intents, AA, safe KYC contracts, etc.

3 Likes

(sorry for the deleted post, misclick)

I agree that the discussion is probably moot since it is unlikely that this EIP will be dropped, and even less likely considering that some big players are being blocked by not having this EIP.

Of course, all developers who care about and take into account smart contract wallets will be able to make them work with transient storage, and as you said, this even enables new information flows within AA itself.

My point is that the “default experience” (developer who does not care about AA, user who is using AA) becomes worse after this EIP. Before, it only happened if:

  • The developer explicitly did msg.sender == tx.origin.
  • The developer explicitly asked for EOA signatures.

Now, another option for breaking AA wallets is:

  • The developer didn’t handle transient storage correctly.

The first two cases are sadly quite common today and involve a far more deliberate decision to ignore smart contract wallets. With transient storage, there is just another way of messing it up.

I don’t disagree that education can help, and AA wallets getting more adoption can also help. Eventually, if a sizable portion of users are using AA, then this is not a problem; market forces take hold. Today, market forces for supporting AA are low.

The most likely solution (if this ever becomes a problem) is that we are going to need to enforce on-chain one user operation per transaction. This is easy to do (add msg.origin == tx.origin to the entrypoint), but it has some second-order consequences, like if you are trying to do account recovery from within the user operation.

I think this is the clearest example.

// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.24;

interface IERC20 {
  function transfer(address _to, uint256 _val) external;
}

contract RewardManager {
  IERC20 immutable token;
  address immutable owner;

  mapping (address => bool) public canReward;

  constructor (IERC20 _token) {
    token = _token;
    owner = msg.sender;
  }

  function setCanReward(address _addr, bool _b) external {
    require(msg.sender == owner);
    canReward[_addr] = _b;
  }

  function addReward(uint256 _val) external {
    require(canReward[msg.sender]);
    assembly {
      // Add pending rewards
      let prev := tload(1)
      tstore(1, add(prev, _val))
    }
  }

  function initRewards() external {
    address prev;
    assembly {
      prev := tload(0)
    }

    // Only allow the recipient to be defined
    // once per transaction. Otherwise a contract
    // may override it.
    require(prev == address(0));

    assembly {
      // Defines that from now-on only
      // this address can claim the rewards
      tstore(0, caller())
    }
  }

  function pullRewards() external {
    address recipient;
    uint256 val;

    assembly {
      // Send the rewards but don't clear
      // the recipient, or else other contract
      // can change the address
      recipient := tload(0)
      val := tload(1)
      tstore(1, 0)
    }

    token.transfer(recipient, val);
  }
}

This pattern is correct if all your users are EOAs; it only breaks for AA wallets when transactions are being aggregated.

Sure, you can modify it to support aggregated AA, but that’s not a given. A developer can be perfectly happy with this contract, and he wouldn’t notice a problem even if it goes live. What happens to AAs if the next trend of popular dapps is mostly based on a pattern like this?