Pietro Carta - Reentrancy in Cancun hardfork: the curious case of EIP1153 (transient storage)

EthereumZuri.ch 2024 · April 2024 · 23:35

Pietro Carta shows how EIP-1153, the transient storage feature introduced in Ethereum's Cancun hardfork, breaks some security assumptions made by Vyper and Solidity smart contracts and opens the door to reentrancy attacks. Although transient storage is a useful primitive for dapp developers, he examines this curious reentrancy case in detail and discusses mitigations.

Speakers

Topics

From EthereumZuri.ch 2024, hosted by EthereumZuri.ch.

Transcript

hello so uh I will talk about this re-entrance in Cancun hard Fork the Curious cage case of EIP 1153 so transient storage chain security is is a smart contract audits firm uh we represent many of the main protocols on ethereum I'm a blockchain security engineer at chain security I have many years of experience in cyber security I've been auditing uh curve the Viper compiler conic year and I've found critical live bugs on multiple protocols so uh I will give a quick recap about what canun does uh I will uh

present an overview over execution context messaging so how different uh execution context of the same contract talk to each other I will present how transient storage works and then I will talk about this new re-entrancy Vector which is introduced by transient ve transient storage so the canun hard Fork was activated on the 13 of March 2024 exactly one month ago and it contains a bunch of new IP eips that change the protocol uh for once um we've got uh the beacon block

uh route in the evm that means we can access the consensus chain from the execution chain we've got uh blobs so uh this one and this one uh are for blobs we've got memory copy op code we finally get rid of self-destruct and finally we have TR transient storage op code so uh I will talk quickly about how the data layout was before Cancun So within uh the an instance of the ethereum virtual machine you had

um uh the stack the memory uh which you can write to the code is immutable you've got the account storage which has um uh which has the data storage of a contractor uh program counter gas available uh not represented here but also present are call data and return data uh these are all the data locations that are present within asmart contract execution uh so let's say we have two contracts c and a uh and C calls a which calls C again so now uh we've got a

first execution context C1 and a second execution contest C2 C2 all within the C contract uh how can we communicate information from this execution context to this execution context so it's the same contract but we cannot use memory because uh when you call uh a contract even if it's already executing within the call stack the memory will be new it will be a new instance so we cannot uh write data in memory to communicate between the two execution contexts uh that would have been convenient because memory is very cheap to access and to write to uh we can use

C data Maybe So C1 can pass a message to a which passes this message to C2 uh this can work if a is a trusted contract so we know what it does and we trust it but if a is controlled by the user it can do it can do whatever it wants and it can lie about the message it received from C1 and back to C2 so C data can work but not always and finally we can use storage uh so basically C1 writes to its own storage and C2 reads from its own storage and since they have

the same contract they share it and uh they can message this way this is what happened until Cancun but it's expensive so the cost of one s store is between 2,900 gas and 20,000 gas depending on whether the slot is warm or if it's been written to already um most of this can actually be refunded if we reset the slot to its original value at the end of the execution but the refunds are limited by the total amount of gas you are spending in one transaction so a refund can only be up to 20% of the gas

spent uh so this is why transan storage has been introduced uh EIP 1153 introduces these two new OP codes T load and T store uh and they behave they claim to behave exactly like storage except that they are reset at the end of a transaction so now C1 can t store and C2 can tload and uh the value that's been stored in C1 is retrieved in C2 and this happens within one transaction at the end of the transaction the value that's

been stored is deleted it's reset back to zero and to load and T store cost both 100 gas so much less than uh s store one example of how we can use these uh train and storage op codes is to implement reeny guards in this case the execution context messaging would be that C1 says to lock while calling out and C2 reads the current state which is locked so re-entry guards are implemented like this in uh using traditional storage so lock is a storage variable it's

initialized to not entered which is one and uh the non-reentrant modifier will simply check that lock is not entered and then uh it will set it to entered it will execute the body of a function and then set it back to not entered uh now why we use one and two is a specific of how uh s store costs uh so if you write a a a positive value in a in a slot that's zero that cost 20,000 gas if you write a value in a slot that's non zero that costs 5,000 gas because the slot is already initialized

so that's why we use one and not zero for not entered anyway this was the current state of re-entrancy blocks and we've uh so how much does this cost we've got one uh s load uh which is cold it's taken from storage and it's not been accessed yet so this is 2,100 gas then we've got a cold a store which cost 2,900 gas and then we've got uh when we reset back the lock we've got a warm s store which is 100 gas so in total uh we've got

5,100 gas spent but we get 2,800 gas in refund from the resetting of the lock so if we are using the full refund it's only 2,300 gas so this is the minimum cost of our re-entrancy lock uh using trains in storage we don't have it in solidity yet I think but we can use it in assembly and uh so this is pretty much the same logic we check that it's non set we check that it's uh zero uh we set we set it to one we execute the code uh

and then we set it back to zero uh and now the cost is very easy to calculate it's just 100 for first T load 100 for the second T store and 100 for the uh third T store uh this ends up being 300 gas plus the overhead of everything of course uh so this is like three cents of a dollar in today's price pretty much uh these would be like 30 cents of a dollar or something like that so it's quite Improvement now we know what transi storage does and how it works uh let's

forget about re-entrancy guard and talk about re- entrances uh I've got a contract here which uses uh normal storage not transient storage uh it's got a deposit function in which you can increase the balance of the message sender by sending some ether and then there's a withdraw function which transfer to the message sender its balance and then resets its balance to zero so can you spot the problem yeah

yeah but it was a trick question actually very is no problem because because transfer only transfer 2,300 gas and this is not enough to recall withdraw again calling withdraw will cost uh 100 for the message call and then you will not be able to set the balance of the sender to zero again in the inner call because even if it costs only 100 because it's it's a warm storage slot maybe uh when you've got less than 2,300 gas in your execution context you cannot use S store and this is an addition to the evm that was

introduced with a Constantinople heart Fork because initially um uh a storage um was always 5,000 gas and so you could um you could never uh ride to storage uh in Ascend or transfer uh re-entrancy so this was a safe operation with a Constantinople hard Fork uh the cost of storage was changed and it was lowered and it was in the least case it was 900 gas I think um which uh which was kind of an

oversight because now you could uh use send and transfer functions to re-enter uh contracts uh so this was detected actually actually by a colleague of mine uh back in 2019 the Constantinople heart Fork was delayed one month because this bug was just uh uh detected a few days before the upgrade uh so in low uh gas when you have got low gas in your context you cannot uh have a re-entrancy and this is uh to preserve the safety of transfer and send uh with uh old

uh uh with this Constantinople upgrade um so what changes with EIP 1153 well the difference is that now uh EIP 1153 so T store and T load they don't this T store doesn't implement this Safeguard if you've got less than 2,300 gas you can still T store uh so the send and transfer functions are now no longer safe uh if you use them in a contract that uses transin storage they're not inherently safe they can still be safe so I've got an example

here uh which has uh a balance field it's still a volt it has a balance field and a temporary allowance temporary allowance is a transient mapping uh we can deposit by sending some ether to this contract and we can temporary approve which means we make an approval which only lasts for this transaction and this is highlighted as a use case of EIP 1153 because usually when you make an approval you don't want it to last a long time you're just you just want to to do some operations with it so temporary approvals could be an application of transient

storage uh and then we've got withdraw all temporary approval from and this uses the send function uh and again it sets the temporary it decreases the balance by the temporary allowance so what can happen here uh is that I call withdraw all temp from uh which sends me uh my balance I I can do yeah from whatever approval I have uh and in this send function I re-enter temporary approve set it to zero and then my balance is decreased by zero but I've already sent the ether to

myself so now we can use send for this uh re-entrancy uh I've got a second example it's a bit more complicated but it uh reflect some code that was in Oiler V1 which is actual code in production uh which uh the code of Oiler uses storage it's safe this one use transient storage it's uh it's more efficient therefore but it's um it allows this re-entrancy so we're not uh we're not we can defend against re-entrance

but now functions such as send and transfer have to be double checked if you are using transient storage uh what happens here is that we've got a deferred liquidity check option this is a a vault that can do operation and uh if we enable the the third liquidity check our balance can become negative uh which is sometimes useful if you want to do we want to chain operations such that at the end of a chain of operations we have a positive balance but uh at some point the the balance might become negative this is a way to implement flash loans uh and um so just translating this code from s

store to uh T store uh we we have now the option in this send to re-enter the withdraw function function and send ourself our balance twice so uh in conclusion Transit storage implements data locations which have the lifespan of a single transaction uh it's mostly equivalent in Behavior to storage operations they both cost only 100 gas in every case uh but differently than um storage operations T store will work during

address transfer even if uh if the gas uh left is less than 2,300 um there's an additional cavat with Transit storage is that um if we go back to the example of the re-entrancy lock here we might think since this is is reset to zero anyway at the end of a transaction we might want to drop this additional t- store at the end because anyway we are locking uh we are executing our function and then since the transaction is ending uh the this thing will be reset to zero

anyway so we can save 100 gas with this uh this can be tempting but it breaks comp composability of contract so if you want to execute multiple times a cont contract sequentially not during a re-entrance this will no longer be possible so it will break multicols it will break batching operations it will break uh um account extractions such as EIP 4337 um so this is a cat of uh of T store you should always reset back to the intended value and not let the transaction uh

expiration do it uh and operations that we thought would be safe such as send and transfer or contracts that we thought to be safe because they only use S uh send and transfer might not be safe anymore if your contract that uses them uh uses trans transient storage thank you very much thank you very much Petro for this insightful presentation uh we have about 6 minutes left so uh let's open the floor for questions thank you hi uh yeah

thanks for the presentation uh really interesting so just to recap um the the smart contracts that have been previously uh not vulnerable to re-entrancy now with the dankun are still not vulnerable right unless it's like unless that contract is taken and redeployed with uh additional functionality of accessing the transient storage am I understanding it correctly yeah exactly so contracts that are currently deployed uh are safe because they don't use the transient storage option basically uh new contracts which use strent storage uh have to be careful

around transfer and send okay so like if I have a code for the smart contract that has been deployed and in the past I know it's been safe yeah if I go and uh add trans and storage functionality I I should account for the re-entrancy and go over the code to see if any of the functions are now have been exposed to that vulnerability right exactly if you optimize your existing contract by using transient storage you should re-evaluate it security okay thank you any other questions I guess I can ask another one

yeah um so with uh with dun the transient storage is there any I mean with the next update of uh I believe 64 blobs being added whatever that's called the DK sharding right yeah um do do you think do you see additional vulnerabilities as far as like transient storage when it's expanded to that many blobs or is it going to be just no I I don't think that touches the transient storage subject the the blobs and the blobs are really uh accessible from execution only through the block route uh blob hashes and I mean this is safe it's it's not

something that's touched yeah oh because in the consensus is that because in the consensus the uh the the evm execution cannot really access the the blocks that's why it's it shouldn't be an issue for the smart contract well you can kind of access it in the smart contract by getting a proof uh of what's in the blob but it it's not related to transient storage and um it's also no it's okay so the main reason it save is it's read only you will not write to it from the execution layer so the problem here is you can

write in a transfer send uh re-entrancy thanks okay one more question please yes uh I remember there was some discussion or some um people that wanted to implement the resetting of the of Transit storage at the call level rather than at the transaction level so when you what do you think about that so at the C Level it would have uh introduced a a concept of a c stack of cont tracks which

uh uh so the transient storage would only pertain to the calls under my my execution and not up up from me uh I thought it was a probably a cleaner uh implementation but probably it adds a lot to the execution nodes and I think this is also valid all right thank you very much thanks a lot