# Transient storage - The future roadblock of the Ethereum's AA landscape EIP-1153 is scheduled to be included in the next Ethereum fork (Cancun). It is meant to create a new type of storage for smart contract wallets, a sort of middle ground between memory and storage, "transient storage". Transient storage works just like regular storage, but with two key differences: - It is cheaper to use. - It is automatically cleared at the end of the transaction. Most of the use cases for transient storage can be implemented using regular storage and callbacks; the main goal of the EIP is to reduce their gas costs. Patterns like KYC contracts, allowlisted tokens, and similar become cheaper and straightforward using transient storage. However, in this article, we present the case that transient storage, in its current form, is not compatible with the AA vision of Ethereum; and if not patched, could set back all smart contract wallet efforts. ### But What Really Is Transient Storage? Apart from being an alternative to contract storage in cases where persistence is not required (like reentrancy locks), transient storage allows developers to easily create global singletons on-chain. These singletons are useful since information about a transaction can be accessed by any contract, independent of the stack depth. These sorts of singletons are possible using regular storage, but they are prohibitively expensive to use. This allows a smart contract to enforce something like KYC on the whole transaction, without disrupting the integration with other smart contracts. ![The image illustrates a transaction workflow where a DEX handles KYC validation and shares this data with a lending dapp without requiring the dapp to manage the KYC data directly. After the initial validation, the KYC data is stored transiently, and a subsequent DEX re-validates the KYC before the transaction concludes, with the data being cleared at the end.](https://hackmd.io/_uploads/rkw9-CN9a.png) As can be seen in the diagram, the DEX is using transient storage as a global singleton for the KYC information. In this way, the lending dapp does not need to be aware that any KYC is being enforced, and no additional payloads need to be passed around in the internal calls. Additionally, and most importantly, the KYC information is **automatically** cleared at the end; the DEX doesn't need to do it manually. This is important, since the DEX does not know when it will be accessed for the last time and thus **cannot know** when to clear the transient storage. This pattern is useful in a myriad of ways; sharing information among different components without these components sharing information directly simplifies many design patterns, such as fee-locked ERC20 tokens, KYC, allowlists, etc. ### But Isn't This Good? Apart from the specific use cases of transient storage (that may range from purely technical to more controversial patterns), we argue that having global singletons in the context of an Ethereum transaction is a bad idea, an anti-pattern that will heavily limit future developments. Global singletons are widely recognized in the development world as an anti-pattern in most, if not all, programming languages. One of the biggest problems with singletons is that they break encapsulation. Breaking encapsulation has serious consequences for regular programs, but it is even more dangerous in a permissionless execution environment, where anyone can deploy code, and programs regularly interact without trusting each other. ## Transient storage and Smart contract wallets Smart contract wallets are widely agreed upon to be the future of blockchain UX. They enable many quality-of-life upgrades that one day we will take for granted: - Key rotation - Proper batched transactions - Better cryptographic schemes - Gasless transactions - Batching and calldata compression - Social recovery - Multisignatures These features are currently not available with "normal" wallets (Externally Owned Accounts, or EOAs), and it is likely that some of them will never be. Many EIPs are being developed to improve compatibility and enhance the capabilities of smart contract wallets, but as I will demonstrate now, EIP-1153 puts the very idea of smart contract wallets in jeopardy. ### EIP-1153 is only for EOAs The above-shown diagram appears correct and self-contained, but this is because the entire diagram represents a single user. The global state has a clear, well-defined owner, and at the end of the transaction, it's safe to clear the data. This is something that can only be assumed when using EOAs. In contrast, smart contract wallet operations can be batched together and executed in a single transaction. --- ```sol function handleOps( UserOperation[] calldata ops, address payable beneficiary ); function handleAggregatedOps( UserOpsPerAggregator[] calldata opsPerAggregator, address payable beneficiary ); ``` *EIP-4337 for account abstraction is heavily geared towards operation aggregation.* --- When many operations are aggregated in a single transaction, EIP-1153 doesn't work as intended. This happens because the transaction only ends once, transient storage is only cleared once, and user operations contaminate each other's transient storage. ![It starts with "Transaction start," where a bundler executes two smart contract wallet operations. This progresses to a DEX for "User A - Operation 1," which validates KYC and interacts with a lending dapp. The KYC information is stored in transient storage. The process then loops back through another DEX, which re-validates KYC using tload. The flow continues to "User B - Operation 2," which also goes through a DEX, and mistakenly thinks the transient storage still belongs to "User A," indicating a potential error as the storage was not cleared. The process ends with "Transaction end," and a note that storage is cleared at the end, but it's too late for "Operation 2."](https://hackmd.io/_uploads/rkHgjREcT.png) In this example, it can be seen how EIP-1153 is unaware of the different user operations, leading to the DEX contaminating `Operation 2` with state that belongs to `Operation 1`. And worse still, the DEX has no reliable way of telling if the user has changed, so it can't directly avoid the contamination. ### Failure Modes This contamination of state can manifest in many different ways, but in almost all of them, either the DEX becomes at risk, smart contract wallets fail to operate, or the DEX must block smart contract wallets from interacting with it. Let's go one by one through the use cases listed on the EIP-1153 page: ``` - Reentrancy locks - On-chain computable CREATE2 addresses: constructor arguments are read from the factory contract instead of passed as part of init code hash - Single transaction ERC-20 approvals, e.g. #temporaryApprove(address spender, uint256 amount) - Fee-on-transfer contracts: pay a fee to a token contract to unlock transfers for the duration of a transaction - “Till” pattern: allowing users to perform all actions as part of a callback, and checking the “till” is balanced at the end - Proxy call metadata: pass additional metadata to an implementation contract without using calldata, e.g. values of immutable proxy constructor arguments ``` #### Reentrancy Lock Example One of the use cases that has been proposed for EIP-1153 is using it for reentrancy locks. However, there is one glaring footgun for developers: ```sol contract BadContract { mapping(address => bool) sentGifts; modifier brokenNonreentrant { assembly { if tload(0) { revert(0, 0) } tstore(0, 1) } _; // No need to clear the tstore // the EVM clears it at the end of the transaction } function claimGift() brokenNonreentrant public { require(address(this).balance >= 1 ether); require(!sentGifts[msg.sender]); (bool success, ) = msg.sender.call{value: 1 ether}(""); require(success); // In a reentrant function, doing this last would open up the vulnerability sentGifts[msg.sender] = false; } } ``` Building a reentrancy lock will work in 90% of cases, but it will hinder composability with this contract (as only one call can be done per transaction), and it will also put all smart contract wallets that interact with it at DOS risk. If two AA operations are aggregated in a single transaction, and both interact with this contract, the second one will find itself reverting (due to the reentrancy lock). In most AA environments, the smart contract wallet must pay transaction fees even for reverted transactions, so this error will lead to the AA user having to pay for the reverted transaction. Worst of all, in most AA schemas (including EIP-4337), the owner of the wallet has no way of avoiding a bundler from executing the operation in aggregation with other operations. This means that any bundler can just purposely try to bundle these operations together, and the owners of the wallets need to pay every time they fail. Developers may decide to pay attention to this edge case and clear their reentrancy locks, but it is likely that less popular protocols will decide to be less careful, the problem will lie dormant until smart contract wallets start to become popular (or the dapp becomes popular). In both cases it becomes too late to patch it. ### Single transaction ERC-20 approvals This is meant to allow ERC20 developers to implement `approve` schemes that only last during the scope of one transaction. This means that the user can approve another contract to pull ERC20s from their address, without worrying that this approval will "leak" and later be used by an attacker. This pattern will probably work great for EOA accounts, as, by definition, the whole transaction is controlled by a single user. However, this leads to a false sense of security, as an attacker can still attack from within the transaction itself, either by using a callback (similar to the reentrancy lock) or by using an operation (if the user is using a smart contract wallet). Smart contract risks are the worst when they are still there but hidden. In this case, most of the time the contract will be protected by this single layer of defense, but as explained, this single layer of defense is rather weak. This pattern can also be implemented today, without transient storage. The current implementation would be slightly more expensive, but it could be made cheaper by clearing the approval after the transaction and by improving the storage refund structure. ### Fee-on-transfer contracts > Fee-on-transfer contracts: pay a fee to a token contract to unlock transfers for the duration of a transaction This feature is meant to allow ERC20s to charge a fixed "tax" when interacting with them. This would allow a user to pay the tax once, and then interact with the contract (on a single transaction) as many times as they want. This construct is useful if a token wants to charge a tax but does not want to be opinionated on the number of "hops" that the tokens may need to take during the transaction. ![Flowchart showing a transaction process with two operations by different users. The transaction starts with a bundler executing two smart contract wallet operations. It moves through stages labeled 'Gated Token' for User A's Operation 1, where a fee is paid, and this action is verified in transient storage. The same gated token process is then mistakenly applied to User B's Operation 2, with the token incorrectly assuming the fee was already paid by User A. The transaction succeeds but the fee's payment status is misattributed between users.](https://hackmd.io/_uploads/Bke84xBcT.png) It should be glaringly obvious how this schema breaks when smart contract wallets interact with them. The first user that pays the tax automatically causes the token contract to assume that all subsequent callers must be related to the user, but in reality, some of the transfers may be originated by other users. The only option for the token is to filter by `msg.sender`, but this defeats the purpose of transient storage, as now any `transferFrom` operations will not be detected as belonging to User 1. User 1 could do one final call to the token, informing it that he has finished, **but the token can't guarantee it**, and if User 1 does not call the gated token to clear the storage, then the token contract has no other ways of knowing if the user has changed or not. This example doesn't seem too critical; after all, the only problem is that the token lost some revenue. But consider that this schema is in reality enforcing access control to the gated token. If the EIP examples themselves get it wrong and leave it open to attack, it is expected that developers trying to implement these patterns will fall for similar vulnerabilities. ### “Till” Pattern As with the CREATE2 and temporaryApprove patterns, this pattern is not directly affected by these vulnerabilities; however, this is not a blanket statement. It also has to be noted that the till pattern is possible today using regular storage. The Uniswap v3 contracts, currently in production, use the till pattern. It is true that transient storage may reduce the cost of this pattern, but these pricing optimizations can also be implemented directly in regular storage. ### Proxy Call Metadata This describes using transient storage as a generic carrier for any sort of metadata (like the KYC example from the beginning). This pattern has the exact same problem as the "fee-on-transfer" pattern: the data leaks across operations, and the contracts involved have no reliable way of controlling for it. ## Okay, the devs need to remember to clear the transient storage, isn't that enough? This is technically correct; if all developers, of all contracts, always clear their transient storage after using it, then transient storage should be as safe as regular storage. However, transient storage by itself does not require developers to perform this step, and failing to perform it leads to vulnerabilities that aren't obvious from the start—vulnerabilities that will probably lay dormant in production. Additionally, some of the issues don't directly affect the developers or their dapps, so there is an even higher chance that developers will purposely ignore these recommendations. Sometimes, leaving transient storage uncleared may be a design decision. Consider the following contract: ```sol // 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 is a simple example of the issue being manifested. This contract does the following: - Allows a user to start collecting rewards. - Once the address initiates the contract, all future rewards belong to it - Rewards are accumulated using transient storage, then the user can pull them - The user can pull rewards many times In this case, the developer may decide that clearing the transient storage is a bad idea, since other contracts could clear it and re-point the rewards to themselves. This example will work perfectly in EOAs; it will be fully composable with other smart contracts, but **it will fail if two smart contract wallets use it** (and are aggregated together). If this only breaks with smart contract wallets, and to the developer it feels "safer", what incentive does the developer have not to build this? Smart contract wallets today are a small portion of the user base. But **if these patterns start to proliferate, smart contract wallets will always be a small portion of the user base**, as they will be less compatible every day. ### Let's assume all developers are perfect It still does not help, even if Solidity itself were to refuse to compile, it would still not be enough to enforce it as a rule. The reason is that in many scenarios it is not the developer itself who has to clear the transient storage, but the user. Imagine that the developer decides to be a good citizen, so they add this method: ```sol function leaveRewards() external { address prev; assembly { prev := tload(0) } require(prev == msg.sender); assembly { tstore(0, 0) } } ``` Now the user can fully clear the transient storage, and all the properties of the contract are maintained, but why would a user bother to spend gas calling the cleanup? It does not affect them; failing to clear up means that subsequent transactions fail, but their own always remains unaffected. --- These are only some examples I could come up with. I suspect that as time goes on, people will find more and more use cases for transient storage. Use cases that work 90% of the time (with EOAs) but fail with Smart Contract Wallets. This can lead to a self-reinforcing cycle in which AA wallets are abandoned, leading to more developers ignoring these cases, and so on. ## Possible solutions The root of the issue is not intrinsic to transient storage, but to the fact that EOAs and AA transactions behave differently when interacting with transient storage. This is further aggravated by the fact that the behavior is likely to be correct on EOAs and not on AA wallets, pushing back on the adoption and increasing the compatibility issues of Smart Contract Wallets. ### Limit Effect of Transient Storage to Downstream Calls One possible solution for this EIP to limit the effect of any transient storage to the current call, and the downstream calls. This would normalize the behavior across AA wallets and EOA wallets, while still permitting most use cases and gas optimizations described by the EIP. Some consideration and further analysis need to be done, since this change would also require specifying the behavior of transient storage when accessed at different call levels. ### Toxicity of EIP-1153 It is important to mention that EIP-1153, in its current form, may become sticky, leading to an environment in which patching it becomes not possible. If contracts start using transient storage with the assumption that the transaction frame only belongs to a single user, the only possible solution to reintegrate AA wallets would be enshrining AA wallets natively in the protocol, something that has proven very difficult for many years. Changing the behavior of the EIP after it has been released will likely not be possible, since such a change will break a sizable amount of the contracts that integrate transient storage. Similarly to how hard it was to deprecate `SELFDESTRUCT`, modifying transient storage may take years of work. Years in which smart contract wallets become buggy and less compatible with existing dapps. **For that reason, we advise the Ethereum community to postpone the integration of EIP-1153 until it can be patched to avoid distinguishing between EOA and AA wallets.**