Check out a free preview of the full A Tour of Web 3: Ethereum & Smart Contracts with Solidity course
The "Diamond Pattern App Storage" Lesson is part of the full, A Tour of Web 3: Ethereum & Smart Contracts with Solidity course featured in this preview video. Here's what you'd learn in this lesson:
ThePrimeagen creates an AppStorage struct to act as a common location for storing application data. The only issue with this storage solution is it has the potential to write into an already allocated storage block. The solution is to use assembly to select a random storage location.
Transcript from the "Diamond Pattern App Storage" Lesson
[00:00:00]
>> So I did figure out why I totally forgot. If you call an address that does not exist, the transaction does successfully execute. It's just that there's no contracts there. So of course, it returns true, but nothing would be stored. So I think I can prove why that works by simply overriding my address, and then putting in a value that's higher so it won't set my value again, it won't set up correctly to what to expect.
[00:00:25]
But we'll get to that, that was from the previous problem where I couldn't understand why my address was being called. Forgot about that. All right, so let's keep going with the storage thing for a little bit. So now we understand why this one could be called a. This one could be called c and we overwrite these two, right?
[00:00:41]
Cuz even though c and d are stored in slot 1, if we overwrote with a, it would totally destroy it, correct? Correct, so a lot of ways people get around this is by making something called storage. So we're gonna go like this, Storage.sol. We're gonna do our standard pragma statement.
[00:01:00]
And then I'm gonna go like this, struct AppStorage. And I'm gonna go like this, uint a, uint8 b, right, do a c and then I could even have address, oopsies, of A in here, right? We could have all these things right here, and then what we could do is we could jump back to say this and import "'/storage.
[00:01:26]
And now just go, AppStorage s, right? I yank that, put that in here, go, Oopsies, oop, there we go, have all those, s.b, s.b, there we go. And so now when we do this, and I call A in perspective of B, we won't clobber over anything. Because what has just happened here.
[00:01:58]
We have this struct called AppStorage. Now, let's just do a quick little review of what happened to our memory, cuz obviously I just rearranged things around. The first thing that's gonna happen is it's gonna tell me I've failed because I've kinda have. So I'm gonna s.ContractA, and then inside of storage, I have to go ContractA, correct?
[00:02:19]
And so now when I do this, it should hopefully work a little bit better. So I'm just kind of making sure you guys are paying attention, which you weren't, clearly, you forgot that there was a d involved in this. And, of course, you can't explicitly convert a big uint into a uint b, cuz b is a uint8, right?
[00:02:42]
Based on our storage, it's a uint8. So, let's just delete, actually let's just make it a uint, close enough, right? There we go. We got through all these things, classic, explicit conversion required. So there we go, so now you can see, there is c, there is d, there's the address of a, here's the value of a.
[00:03:08]
So if I were to call and set in stuff, it would happen in the order we would expect, or this is the value of b, correct? If I'm not mistaken, is that what's happening? Yeah, b is the value of 4, a hasn't been set yet, cuz that's set in A.
[00:03:22]
So let's go back to our deploy script. And let's go await b.setB and let's go 45, right? Oopsies, there take this, print this out, let's reprint those three storages, but of course let's go console.log, put a nice big ol line right here, right? Here we go, and if we've done this correctly, there you go.
[00:03:48]
Now notice that we have something very, very nice right here. But obviously something didn't happen out right here. So what just happened here? We got 2d in one of them, which makes sense. 45 should convert something maybe to 2d. Does it convert to 2d? I'd have to work it out in my head, but I just assume, so here, let's go like this.
[00:04:05]
0x45, that tends to be a little bit easier in my head to be able to do those things. All right, so we see 0x45, we're still seeing 0 here, so what is happening? Let's go into here, we can see that we're delegate calling over to here. We see a uint 256, we see a uint 256.
[00:04:23]
So everything should be executing how I would expect it to be executing, yet we're not seeing a, so what is actually going on here? Actually, I have no idea what is going on here. This should just work, let's just make sure we actually have a success, do it really quickly, cuz perhaps we're not successifizing it or even better is when I forget to do a little.
[00:04:44]
All right, success is true, and of course ContractA is in order. So this, there it is. So how do I not see this last step? I feel like when I put the word console true, it always seem to work. I must have messed something up but anyways, there you go.
[00:04:59]
There's 46, we added 1 to it, I know the moment you, yeah. I had a bug in and starts working it again, I know, it's the classic. I don't know if that's ever happened to you, but have you ever put in a single print statement and it works and then you delete, set single print statements that doesn't work.
[00:05:12]
Those are the single most painful things that have ever existed. All right, so there we go, you can see we now are no longer clobbering our data cuz we're sharing a space. So what's happening? Well, what's really happening is this, is that when we create a struct, it actually goes and it starts in whatever position it's set in, say its position 0, which is what we're doing.
[00:05:35]
And it will tightly align all of its data to be fitting in slot after slot. That's why I can just say, hey, give me the first three slots and we're actually getting out a, b, c, d, contract of address A or ContractA's address. That's why we're getting out all that information but it's nicely held in a struct this time, which just makes it a lot simpler, right?
[00:05:58]
And so if you have a bunch of contracts that are all sharing data, if everybody puts their state storage at the very first slot, if you're using the diamond pattern, it works every single time, right? You're not gonna get clobbered. The one problem that happens is if you're using, say, OpenZeppelin's ERC721, which is the NFT token, they specify their own storage.
[00:06:20]
So we can't do that because then you'd actually right into their storage, right, you'd ruin the NFT storage. And so there is a way around that, and that has to do with how storage effectively works. So let me show you something that's a little bit more exciting, I would say.
[00:06:37]
So I'm gonna go into storage here. And this is how I've gotten around all my storage issues. Is I actually create a library called Storage. It has one single function up here, which I'm gonna say it's gonna be, what is it, bytes32. And this will be key, and got KEY with big capital letters cuz it feels more real.
[00:06:57]
And we're gonna keccak, and we're gonna like this, my-storage-location, all right? So if I keccak it with that string exactly, it will always produce the same 256 bytes or 32bytes worth of data, it'll always be this step. Then I'm gonna go like this, function get() internal pure, meaning I'm not touching anything, this is where it's gonna get real weird.
[00:07:21]
Returns (AppStorage storage s), wait, I thought you can't touch storage if you're pure, you cannot read state if you're pure. So this is what I'm gonna do, bytes32 k = KEY. Assembly, so we're breaking into the assembly layer. This is where I told you weird things happen in the assembly layer.
[00:07:40]
And we're gonna do this, s which is the name of the return variable, I'm gonna go s.slot = k. I've just said, hey, the slot in which you exist is somewhere randomly within this gigantic, infinitely-sized memory pool that we have available. And so now our AppStorage will always be placed in the exact same spot as long as we make this storage call.
[00:08:10]
And if you have any inherited contracts that specify their own data, you won't stop over them, because you're no longer using slot zero, instead you're using a consistent slot. So you don't even have to worry about everybody else following your pattern, which is almost in some ways, way worse, right?
[00:08:26]
You don't wanna do that. If you have to make an extension, you could theoretically make another slot in which you're storing a different set of data that you now want to refer to. And so that way you never have to worry about stomping over yourself. And I find that this is kind of the best way to do this.
[00:08:42]
I actually even brought this version up to the person who made the diamond pattern. And he was surprised, he's like, yeah, I guess if everyone shares it, then you don't have to worry, he was surprised that you could actually get around OpenZeppelin's implementation of ERC 721 by just doing this.
[00:08:59]
Does this make sense? Is anyone a bit confused as to how this works?
>> Why do we assign k to KEY and not just call KEY directly?
>> Reason why you do that is that assembly cannot read off of here. Assembly reads off of only locally scoped items.
[00:09:17]
So you just do this, so I'm taking key, which is up here, assigning it right here, setting it slot right here, and then we can do that. Now obviously what I can't do now, is in here I can't simply print my storage cuz it's not at slot zero, one, two.
[00:09:31]
It's at some place somewhere else. So we could create some helper functions, read out that data. But now, so it's just not that interesting, right? So really what I could do is just go like this. AppStorage s = Storage.get(), right? And so now, we have effectively the same thing, except for we're always safe against somebody else actually writing into our storage.
[00:09:57]
Now there is that one possible chance that you could get a mapping that overrides or lands into your storage. But again, the likelihood of that is probably slightly less than the creation of the universe. So I think we're probably good at least for the next few billion years before you have to worry about one collision.
[00:10:13]
So we'll just keep it there. So there you go, that's just me wanting to make sure I take the time and properly explain storage. And then on top of that, to really show you how to manipulate storage or to play around with it. So effectively, all we did right here is create our own version of mapping, right?
[00:10:30]
Cuz mapping just uses its slot plus a little bit of magic, a little bit of a key, keccak it all together, boom, it has its own position. We did our own. All right, Mark, question?
>> What happens if the storage is overwritten accidentally, multiple times?
>> I would have to understand what it means by accidentally, since we're specifying a spot, you can't really accidentally override this.
[00:10:54]
Like I said, there's 10 to the 79 available slots. This randomly select one of those, nothing else has the potential chance of ever colliding with it, just as the reality of randomness, right? And so we just never have to worry about that happening. But, if you were to upgrade your storage and do something like this, you would definitely clobber yourself.
[00:11:21]
If you redeployed your contract and moved your internal storage around, you would definitely destroy yourself cuz now you've just changed the layout. Before it went slot one, slot two, slot three, right, now it go slot one, slot two, slot three, slot four. So you can never change your internal representation of memory around.
[00:11:44]
But you can certainly never worry about it colliding with something else. It's just not theoretically possible. If you wanted to recover, I mean, theoretically you can read storage. Here we go, if we go back here, if we jump to getStorageAt, it also has a blockTag. So, theoretically, we could actually go to a specific block, read our storage, and then reput that back into our storage.
[00:12:14]
So if you wanted to recover because you screwed things up, you could go back back in time, read out things and put it back in. Now, would this be easy? This actually sounds extremely hard. And I think it would take a couple days of working out all the different, especially if you had mappings inside your struct.
[00:12:30]
Because you can have mappings inside your struct, cuz now you're like, okay, it's in this position, which is here and I gotta apply the mapping formula and go over here. And I have to know which keys I need to be able to grab out. If you don't know your keys, you're pretty much screwed, right?
[00:12:42]
So it'd be very, very hard to actually work everything out. So, good lesson, don't destroy your storage.
Learn Straight from the Experts Who Shape the Modern Web
- In-depth Courses
- Industry Leading Experts
- Learning Paths
- Live Interactive Workshops