From b9b018370b1caea1f97947d006640f8956efd377 Mon Sep 17 00:00:00 2001 From: Moody Salem Date: Tue, 3 May 2022 09:49:01 -0400 Subject: [PATCH] add more to the reference implementation of EIP-1153 (#5028) * add more to the reference implementation * couple nits * trigger merge --- EIPS/eip-1153.md | 136 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 125 insertions(+), 11 deletions(-) diff --git a/EIPS/eip-1153.md b/EIPS/eip-1153.md index fef56ac33a6296..02807b30ae3125 100644 --- a/EIPS/eip-1153.md +++ b/EIPS/eip-1153.md @@ -30,10 +30,9 @@ Potential use cases enabled or improved by this EIP include: 1. Reentrancy lock 2. Constructor arguments loaded from the factory contract (for on-chain-computable CREATE2 addresses as in Uniswap V3) 3. Single transaction ERC20 approvals, e.g. `#approveAndCall(address callee, uint256 amount, bytes memory data)` -4. Passing error codes and messages from the execution frames up the execution stack -5. More generic libraries that use callbacks, for example generalised sorting with functions `Less` and `Swap` defined. -6. Contracts that require control before and after method execution (e.g. via callbacks) -7. Shared memory (borrowed from early draft of similar EIP by @holiman). When implementing contract-proxies using `DELEGATECALL`, all direct arguments are relayed from the caller to the callee via the `CALLDATA`, leaving no room for meta-data between the proxy and the proxee. Also, the proxy must be careful about `storage` access to avoid collision with `target` `storage`-slots. Since `transient storage` would be shared, it would be possible to use `transient storage` to pass information between the `proxy` and the `target`. +4. More generic libraries that use callbacks, for example generalised sorting with functions `Less` and `Swap` defined. +5. Contracts that require control before and after method execution (e.g. via callbacks) +6. Shared memory (borrowed from early draft of similar EIP by @holiman). When implementing contract-proxies using `DELEGATECALL`, all direct arguments are relayed from the caller to the callee via the `CALLDATA`, leaving no room for meta-data between the proxy and the proxee. Also, the proxy must be careful about `storage` access to avoid collision with `target` `storage`-slots. Since `transient storage` would be shared, it would be possible to use `transient storage` to pass information between the `proxy` and the `target`. These opcodes are more efficient to execute than the `SSTORE` and `SLOAD` opcodes because the original value never needs to be loaded from storage (i.e. is always 0). The gas accounting rules are also simpler, since no refunds are required. @@ -82,16 +81,131 @@ Relative pros of this transient storage EIP: ## Backwards Compatibility This EIP requires a hard fork to implement. -Since this EIP does not change semantics of any existing opcodes, it does not pose a backwards incompatibility risk for already-deployed contracts. +Since this EIP does not change behavior of any existing opcodes, it is backwards compatible with all existing smart contracts. ## Reference Implementation -Because the transient storage must behave identically to storage within the context of a single transaction with regards to revert behavior, it is necessary to be able to revert to a previous state of transient storage within a transaction. At the same time reverts are exceptional cases and loads and stores should be cheap. +Because the transient storage must behave identically to storage within the context of a single transaction with regards to revert behavior, it is necessary to be able to revert to a previous state of transient storage within a transaction. At the same time reverts are exceptional cases and loads, stores and returns should be cheap. + +A map of current state plus a journal of all changes and a list of checkpoints is recommended. This has the following time complexities: +- On entry to a call frame, a call marker is added to the list - `O(1)` +- New values are written to the current state, and the previous value is written to the journal - `O(1)` +- When a call exits successfully, the marker to the journal index of when that call was entered is discarded - `O(1)` +- On revert all entries are reverted up to the last checkpoint, in reverse - `O(N)` where `N` = number of journal entries since last checkpoint + +```typescript +interface JournalEntry { + addr: string + key: string + prevValue: string +} + +type Journal = JournalEntry[] + +type Checkpoints = Journal['length'][] + +interface Current { + [addr: string]: { + [key: string]: string + } +} + +const EMPTY_VALUE = '0x0000000000000000000000000000000000000000000000000000000000000000' + +class TransientStorage { + /** + * The current state of transient storage. + */ + private current: Current = {} + /** + * All changes are written to the journal. On revert, we apply the changes in reverse to the last checkpoint. + */ + private journal: Journal = [] + /** + * The length of the journal at the time of each checkpoint + */ + private checkpoints: Checkpoints = [0] + + /** + * Returns the current value of the given contract address and key + * @param addr The address of the contract + * @param key The key of transient storage for the address + */ + public get(addr: string, key: string): string { + return this.current[addr]?.[key] ?? EMPTY_VALUE + } + + /** + * Set the current value in the map + * @param addr the address of the contract for which the key is being set + * @param key the slot to set for the address + * @param value the new value of the slot to set + */ + public put(addr: string, key: string, value: string) { + this.journal.push({ + addr, + key, + prevValue: this.get(addr, key), + }) + + this.current[addr] = this.current[addr] ?? {} + this.current[addr][key] = value; + } + + /** + * Commit all the changes since the last checkpoint + */ + public commit(): void { + if (this.checkpoints.length === 0) throw new Error('Nothing to commit') + this.checkpoints.pop() // The last checkpoint is discarded. + } + + /** + * To be called whenever entering a new context. If revert is called after checkpoint, all changes made after the latest checkpoint are reverted. + */ + public checkpoint(): void { + this.checkpoints.push(this.journal.length) + } + + /** + * Revert transient storage to the state from the last call to checkpoint + */ + public revert() { + const lastCheckpoint = this.checkpoints.pop() + if (typeof lastCheckpoint === 'undefined') throw new Error('Nothing to revert') + + for (let i = this.journal.length - 1; i >= lastCheckpoint; i--) { + const {addr, key, prevValue} = this.journal[i] + // we can assume it exists, since it was written in the journal + this.current[addr][key] = prevValue + } + this.journal.splice(lastCheckpoint, this.journal.length - lastCheckpoint) + } +} +``` + +The worst case time complexity can be produced by writing the maximum number of keys that can fit in one block, and then reverting. In this case, the client is required to do twice as many writes to apply all the entries in the journal. However, the same case applies to the state journaling implementation of existing clients, and cannot be DOS'd with the following code. + +```solidity +pragma solidity =0.8.13; -A map plus an auxiliary writeahead list is one implementation: -- On entry to a call frame, a call marker is added to the list -- All writes are written to the list with the old data value -- When a call exits successfully no change is made -- On revert a search is made for the matching call marker on the list. All entries on the list are reverted and the list cleared to that marker. +contract TryDOS { + uint256 slot; + + constructor() { + slot = 1; + } + + function tryDOS() external { + uint256 i = 1; + while (gasleft() > 5000) { + unchecked { + slot = i++; + } + } + revert(); + } +} +``` ## Security Considerations `TSTORE` presents a new way to allocate memory on a node with linear cost. In other words, each TSTORE allows the developer to store 32 bytes for 100 gas, excluding any other required operations to prepare the stack. Given 30 million gas, the maximum amount of memory that can be allocated using TSTORE is: