 Hello, everyone. Welcome to my workshop. Today, we will be building an NFT bridge between Ethereum and Arbitrum. If you would like to code along, you can clone this repo here. I don't know if the URL is visible enough, but you can get it from the Off-Chain Labs GitHub. It's the most recently updated thing. Or you can grab it from my Twitter at dzac23. It's the last thing I tweeted. You're welcome to code along. You're also welcome to listen. Both work. Most of the coding will be so we have the skeleton of the smart contracts and we'll build out some of the solidity directly. Then at the end, maybe we'll deploy it live on the Goerley testnet. Wi-Fi is a bit suspicious, it seems, but we'll see. Let me talk a bit about Arbitrum first. Arbitrum is an Ethereum layer, too. For our purposes, we can think of Arbitrum as its own blockchain because it is a blockchain. It runs on top of Ethereum and inherits security from Ethereum. The important thing for building this bridge is Arbitrum allows arbitrary message passing between layers. We can send arbitrary messages from an L1 contract directly to layer 2. I shouldn't say directly, maybe asynchronously. We can send messages to layer 2, but arbitrarily. Likewise, we can make arbitrary contract calls from layer 2 down to layer 1. Just using those tools that Arbitrum gives, we can build an NFT bridge around them. That's what we'll be doing. To talk a bit about the architecture of the bridge itself, there's obviously many ways to build an ERC721 bridge or something like it. This will be very similar to the architecture we use currently on Mainnet. Speaking of Mainnet, just to clarify this disclaimer here, this is not tested code, not audited. Don't deploy this on Mainnet. Not liable, whatever. Code that actually is on Mainnet, we use similar architecture for what we call our Custom ERC20 bridge. You can read more about that in our docs. This will be a similar version of that. By custom, what we mean is the token contracts themselves on both L1 and L2 are deployed directly by a developer or user. There'll be an ERC721 contract on L1, another on L2. As long as they have certain affordances, they'll be able to use this bridge, kind of interact with this bridge. The basic functionality that we want from a bridge, we should just say clearly in case it's not obvious, is basically we're going from L1. The L1 contract, our 721 will be the source of truth. We want to be able to essentially use this 721 on L1 and get a representation of a token on L2 that effectively gives us rights to the token back down on L1. You can deposit it and you're guaranteed rights to withdraw it. The contracts involved will be what we call our gateway contract. One contract on L1, one contract on L2. These are the sort of bridge contracts that are in charge of managing the mapping between the L1 token address and the L2 token address, as well as handling the deposit and withdraw functionality. I'll just walk through the flow real quick and then we'll get started filling out the code. Again, the flow is we have our token contracts deployed on layer one and layer two. We'll see what affordances they need as we go. There's three cross-chain messages that are going to have to happen here. One is the L1 token contract kind of has to register itself to the gateway, which basically just means it kind of opts in to what its L2 address will be or what its corresponding L2 token address will be. We'll do that. We'll just sort of store a mapping on the L1 side and the L2 side that maps L1 token to L2 token. That's this kind of registration. It needs to be able to make that call. That'll be a cross-chain message. Both layers sort of know what token maps to where. We need to be able to deposit. Deposit in this case is sending the token to L1 in escrow, sending a cross-chain message to L2, so our corresponding contract mints. Escrow and mint is a deposit and it withdraws the opposite. We're going to burn the token on L2, send a cross-chain message down to layer one, which will eventually release it from escrow. That's the flow we need. If you're coding along, you can get cloned this. When we get to deploying, you can just set these environmental variables. You'll need an L1 gorelly RPC URL and some private key funded with gorelly if you want to actually deploy it. For now, let's dive in. I'll get rid of my Twitter. To start, we can sort of go. We're going to start with, as far as the cross-chain messages go, we'll start with this registration step. Let me do this. First of all, just looking in our repo real quick. I'm just in the contracts directory. Here's our contracts that will be deployed on Ethereum. Here's the ones that will be deployed on arbitrum, L2. Basically, we have the L1 token itself, the L2 token itself, the L1 gateway, the L2 gateway. We'll talk about this at least helper later. Just ignore that for now. Yes. Is that good? Any more? A lot more? Yeah. We can do less. We'll start with ... This is the token contract itself. We're going to example of the token contract itself that we might want to use, that we might want to bridge. We can see ... This is already filled out, so there's nothing to do here. We can see this is ... Standard opens up linear C721. We're just giving ourselves the ability to minch, so we can minch stuff and play with it. The important thing here, the important affordance that it needs is it has this registration method. This will actually make an external call to our gateway, and again, this will update these mappings on L1 and L2. That's already done. The token has to be able to register itself. Let's start with that actual registration. If we go to L1 NFT gateway, again, this is the gateway contract, the bridge contract. The key thing that we want, that we're going to be doing here, is updating this mapping on the L1 side. Similarly, there's a mapping on the L2 side that we'll catch with a cross-chain message. While we're here, so basically we just have a bunch of functions we need to fill out, but we can just look at what we're initializing it with. This is counterpart L2 gateway, the L2 side of the actual NFT bridge. We're storing that, and then inbox and bridge, these are both arbitrum core protocol things that we'll use for cross-chain messaging that we'll get to in a second. This is the mapping we're actually updating. Let's just peek at the L2 side as well. The L2 gateway. Similarly, we have an L1 to L2 token mapping. That's what we're targeting as we update. We'll talk about that other stuff when it comes up. Let's start with this registration step. The token itself made this call to register itself, and we want to update the mapping, basically. Let's go for it. We're going to assume that the caller is a token. If it's not, that's fine. It won't affect anything else. We'll call this L1 token, and then just grab the address, because we're going to use this in a few places. Hopefully my hands get slightly less shaky in a second. So there's the L1 token address that made this call, and now we just update our mapping. It'll be a check we want to do, but let's just get this code in first. L1 to L2 token. Okay. The L1 token mapped to the L2 address provided. Cool. That was easy. Now, one question we should ask here is, should you be able, should the token be able to sort of re-register to a new address? And we can decide, you know, for the sake of simplicity, let's say no. This is kind of, by the way, this whole architecture is meant to be as simple as possible, kind of minimum viable token bridge, so we can kind of focus on the cross-chain messaging stuff and understand what's going on. So if the tokens allow it to sort of change its L2 address, that could make our assumptions kind of weird. But another thing we might want to ask is, can it re-register itself to the same address? And we actually do want that, and I'll explain why. But first, let me write this check. So we're going to check. Actually, let's do this. Yeah. So before we register it, let's grab, we'll call it like current L2 address, and we'll grab it from the mapping. So before we registered it, here's what the address was. Yes. And now we're going to say, if it's all, we're going to check to see if it's already registered. So we're going to check to see if it's already registered. Excuse me? Yes. More high-level abstract question here, but specifically here, you require an NFT smart contract to be compatible with your bridge fundamentally because they need to self-register, right? Yes. And if I'm an NFT publisher, why do I need a bridge? I can just click create a button on my website that burns the token and then give a token to a new guy, no bridge needed. You could, in that case, it would be trusted, right? So this we wanted, yeah. But NFT fundamentally are tokenization of rights. So fundamentally NFT are trusted. Well, if we want the bridging process itself to be trusted, let's say, right? Correct. Let's put it that way, right? So the bridging, so yes, whatever trust assumptions are within. Well, that's as trustless as the cross-messaging. Yes, exactly. So again, we're treating the L1 contract as the source of truth. Yeah, assuming it's trustless, yes. Yeah, it's a good question. So L1 contract is source of truth. Whatever trust assumptions it has, we're sticking with, but the bridge itself, the bridging process itself. But it's not entirely this season compatible with legacy contract. It's only for upgradable contracts or new contracts specifically built from the bridge. That's right. So if you have an old NFT, for example, what you could do is deploy a new contract. That's kind of a wrapper and use that on L1. Yeah, exactly. Okay, so in terms of re-registering, so if it's already registered, we're going to actually allow it to register again as long as it's registering to the same address, as strange as that might sound. So we're going to acquire that the current address is equal to the new one. And the reason we want to do this, as we'll see, is we're going to be sending a cross-chain message. I'll just keep chatting. Okay, let me know. So we need to know the address at which we want to trigger a message on the L2 side, which is going to be our L2 NFT gateway, which we have stored as counterpart gateway. And then we also need to encode the actual message we're calling. So on the L2 side, just peeking ahead, the message that we're going to be calling, just because we have the name here, finalized registration from L2. We need to encode that and specify that that's the method we want to call. So let's do that and encode that data. And we're going to actually just do that in a separate method here, get register L2 message call data. So here we're going to, we're encoding the method of the call that's going to be made on L2. So we'll use encode with selector. So this is just, this is what the arbitration cross-chain protocol expects. We encode the actual, the function name itself. We've already imported this thing. So it should be, yeah, L2 NFT gateway. What was it called? Finalized registration from L1. That's the method we want to call. So there's our function. And we pass in its parameters, which are just going to be these. So basically, there's the things that we need at L2 to do the second half of the operation, which is updating the mapping at L2. Cool. So that gets us the call data. There's a reason I'm putting this in its own function. This is going to help us estimate gas later, basically. But for now, if that's not satisfying, we can just say it looks nice in its own function. Easier to read. Okay. So that will be the call data for our cross-chain message call. Let's grab that in our registration method. Yeah. And we pass in, what did we call it? L1 token, L2 address. Let's call this something. L2 message call data. Cool. Okay. Let's just see if this compiles. Just to make sure we don't go off the rails. Cool. Okay. Okay. So now we have our call data. Now with that, we can actually send a cross-chain message. So I'll just briefly show you the interface for sending L1 to L2 messages. So this isn't in the RIPA. This is in the Arbitrum Core Protocol, Nitro RIPA, but just to show you the interface here. In fact, let's do this. Hopefully that's like reasonably visible, kind of. Here's all the things we need. Create retryable ticket is how we send an L1 to L2 message. Let's just, let's start filling out the fields, and then we'll talk about all these new ones. But for now, okay. So we have our inbox address saved. Again, this is just core protocol. Create. Ah, it's auto-filling everything. I don't know why it's doing that. Okay. Okay. So 2 is the address at L2, at which we want to execute the L2 call. So this is our other gateway, counterpart L2 gateway. Maybe I can do something, whatever. Just as there's more, it should become horizontal. Or vertical, rather. Okay. So we have that. Again, this key thing down here, this is the actual message that we encoded. So we have that. Now everything else has to do with kind of paying for gas at L2. So the way these retryable tickets work is we not only specify a contract call, we also fund it. So we literally, you know, we're effectively depositing ether along with this message. We're going to use that ether at L2 to do the execution. So a lot of these other, basically all these other fields have to do with what's going on in that L2 execution. So L2 call value, that's easy because that's just zero. In this case, we're just updating our mapping. We don't need call value on the L2 side. And now we have all this stuff that has to do with gas. So I've just included those in this L2 gas program struct. Let's just look up at that. But basically L2 execution, so max gas and gas price bid, this is very similar, basically the same idea as gas price and gas limit on L1, right? Works the same way on arbitram. The one additional thing, because this is a cross chain message, is there's the special max emission cost thing, an additional fee for processing cross chain messages because there's just a bit, it's a bit more resource intensive. So that's like proportional to the amount of call data that you use. But for our intents and purposes here, we're just asking the client to provide all these values so we don't have to worry about where they came from. But those are the fields we'll need to create a cross chain message. And I should just do this now as well. Like I said, we're sort of depositing the ether along with it. So we're going to send that along. Is that right? That looked weird to me. So we send this along. This will pay for the L2 execution. The L2 will sort of automatically try to execute it. If it fails, on the L2 side, it can be retried for a week. That's why it's called a retryable ticket. And this gives us kind of an atomicity guarantee. Anyway, let's keep filling these out. So we have the L2 counterpart gateway, no call value. And now we get to these L2 gas params. So that was just passed in. So max emission cost is next. Okay, now we see over here, we have these refund addresses. And this is basically on the L2 side. If you provide excess gas in your deposit, you specify which address you want that gas to end up on. And this is necessary, basically, because it's a cross chain message. We need to sort of do something with this excess gas. Yeah, question. The L2 call value. Yeah. Yeah. So think of L2 call value when this cross chain message, we're creating this effect, something that's going to look a lot like an L2 transaction. It's just going to appear at L2 unsigned, basically. Okay. So for that L2 transaction, we need to specify kind of all the usual fields. Okay. So we'll sort of build out the contract call in a second and we'll see what the L2 transaction is doing. But here we're specifying all the necessary things. So we have like the contract call, the address, and then, yeah, the actual call value just on the L2 side. Yeah. Hopefully that makes sense. If not, hopefully it'll become clear in a second when we see the full flow. Yeah. Okay. So the refund address, we can just, we're asking the client to provide. So in this cross chain message, sort of where your excess ether gets refunded at L2. And then there's a second refund address, which is if there is call value, you specify if the L2 message never succeeds, you specify where that call value gets refunded at L2. In this case, it doesn't really matter because there's no call value. So we'll just use the same, the same refund address. And now we have L2 gas per ams. That was it called gas, max gas. So that's kind of the gas limit. And then L2 gas per ams. It should be something like, what do we call it? Max fee per gas, I think? For gas price. There we go. Gas price, bid. Okay. Let's get rid of this. I think this will just horizontalize. Okay. And then finally, the last parameter was the actual encoded, the encoded call data of the L2 message. There we go. Okay. We are complaining. Yes, I did mean that. Okay. Looks good. So there's our question and message. This gets published and this will sort of automatically execute this method that we encoded at L2. So let's go to that now, which, if we remember, was this thing, finalize registration from L1. So this gets automatically executed. Now we want to finalize the registration, which means basically the same thing it did before we're just updating this other mapping. Okay. So that both sides of our gateway know what token goes to what. So we'll update the mapping. L1 to L2 token, the L1 address that it got from the cross chain message gets updated with this L2 address. And that's basically all we need to do there. Now, other than one other important thing, which is you want to make sure that this method at L2 can only be called as a cross chain message. Otherwise, this whole thing breaks. So we'll add this only counterpart gateway modifier. And now we have to fill that in. I think I still have do here. No more to do. So only counterpart gateway, this is going to be a simple required check just with a slight gotcha. So here we're going to ensure that the message.sender equals this thing that we're calling counterpart gateway L1 or counterpart L1 gateway aliased. So notice that we initialize the contract with the L1 gateway and then we do this thing where we alias it. I'll just peek at this real quick. The actual aliasing step you'll see is just like adding to the address itself some constant offset. So what's going on here is when it comes to cross chain messages, the way that the sender address appears is not the sender of the L1 contract. And this is basically so that on the contract level, we can easily differentiate between messages coming from L1 versus messages coming directly from L2. We need to do this because contract addresses at Arbitrum are computed the same way they are on Ethereum. So we can and often do have two different contracts, one at L1 and one at L2 that share the same address. So if you can't differentiate between them, you can get all sorts of weird spoofing attacks. In this case, every cross chain message kind of lives in its own address space, this alias space. So we know this is definitely coming from the L1 gateway. Cool. There's our registration step. Any questions there? If aliasing wasn't part of the Arbitrum protocol, for example, if the message.sender was just the address from L1, then the deployers of this contract, let's say, if they were malicious, they could deploy something else at L2 at the same address. It becomes very hard to reason about the risks there. So this way it's protecting you from malicious developers, which is a good thing to protect you from. Anything else? Cool. Okay. So there's our first cross chain message, just simply doing the registration. Let's do the deposit. So this will also be an L1 to L2 message. And it'll look fairly similar, actually. Let me just make sure this compiles again, because I'm not using remix and I'm paranoid. Okay. I'm just running, just compiling with hard hat. Cool. Okay. So our L1 token registered. So now we're going to assume, or we're not going to assume anything, but if this token is indeed registered, it can do a deposit. Let me just make sure I didn't forget anything. Cool. Okay. So now we're going to deposit. So you can see it as, once again, the L1 contract is the source of truth. That's what we use. So that's what's passed in as a parameter. So we'll use that to look up the L2 address first. So let's say L2 address equals, we'll look it up in the mapping, but we just saw the registration step go down in. Yes. And now we're just going to make sure it's actually registered. So just require that this thing is really an address. L2 address registered. Okay. So cool. If our token is registered, our L1 token opted. We inherit whatever assumptions L1 token is making again. Now we can proceed. So we can actually do our deposit. So deposit again entails these two things. We're going to ask for the token here, send the token to this contract. Yeah. We have our 721 interface imported. So we'll connect to it at the L1 token address and do safe transfer from to token ID. Yeah. So from the sender, the caller of deposit to this contract, this is the escrow. So here, here. And the token ID. Yes. Cool. So there's the first half of our deposit. And now very similarly, we're going to send a cross-train message to L2. Again, we're going to need, so we have similar parameters here, the gas params, the refund address, all the sort of gas specific stuff. Once again, we need to encode the call that we're going to make on L2, which we will do in a function here. So this is basically, again, just think of this as like the information that we need to send to L2 and the actual function we're going to be triggering on L2 is called finalized deposit. And there it is. Okay. So let's encode the call to that. Now, again, as a separate function, encode the selector, awkward autofill. So just like before, awkward autofill. And it's the same parameters in the same order. So yeah, function selector here. And it's just, yeah, all these. So this gets encoded. Yes. Now let's grab that. So we have our, what did I call up before, L2 message call data or something? In fact, I want to call it the same thing. So I can be lazy later. L2 message call data. Yes. And we pass in the parameters. So this was L1 token, L2 address, token ID. And two is the sort of the address at which we'll be depositing the token, the sort of receiver of the token at L2. Okay. Cool. So we have our call data. And now, once again, we're going to send a, send a, a tribal ticket cross-chain message. In fact, in the interest of time and being lazy, I'm just going to copy and paste this exact thing because it should be all the same parameters, I believe. I don't see why not. So we create a tribal ticket. Yeah. Same parameters. So we still have the same L2 gas business. Once again, the L2 call value. We're not sending any ether. Or rather, we're not, you know, using any ether in the, in the L2 side of the message. We could, if we wanted to have the option of like deposit your token and some ether, we could add a parameter there, but we're not. And then yeah, the only difference from before is we're just calling a different method, which we did here. Cool. So now we will finalize our deposit and finalizing deposit means minting at L2. So let's do that. So now we're back on the L2 side. And cool. We want to check here that it's properly registered. There is something that looks like an edge case, which I can talk about in a second. Why it's not really a scary edge case in terms of the timing of registration and things like that. What am I doing? Yeah. So we're going to look up in our mapping. Yes. And see that that matches with the token provided. So this ensures that it's registered on the L2 side. Yep. And cool. That should be it. Once again, we'll add our only counterpart, L1 Gateway. It's kind of the important thing. So it's going to only be called via that cross-chain message. And okay, now we can do the L2 part. The L2 part of a deposit is minting. So now we want to actually look at our L2 token, which we haven't even looked at yet. What is going to be deployed on the L2 side? So here, this was deployed directly. And basically, this is the assumption. This is the assumed interface. It has to have these methods here. It has to have a method that allows the bridge to mint and allows the bridge to burn. And we can just fill that out now. Pretty simple. Mint. Okay. So we exposed the ability for only the gateway or the L2 bridge to mint. I think it's in that same order. I'm not mistaken. I think that's how it works. And let's add this only gateway. That should probably be clear if it was called like only L2 gateway or something. But when we initialize our L2 token, we give it the address of the L2 gateway. Cool. So now that's exposed. The L2 gateway can mint. So let's mint. Where were we finalized deposit? Yes. So let's connect to our L2 token. Or rather, yes, let's connect to the interface, but we'll use our assumed interface. L2 RBRC 20, which has the bridge mint and bridge burn functionality. Yes. Sorry. The L1 and L2 gateway, they deployed it. It doesn't matter. Yeah. It doesn't matter. They don't need to be. Yeah. So just as long as we, basically as long as when we, like when we initialize these contracts, we know they know each other's addresses, we're fine. So there's no need for them. It's probably better. They're not at the same address because that tends to confuse people. We've had issues actually. Yeah. So they can be if you want to be fancy with how you deploy them, but they don't need to be. Okay. What are we doing? We're minting. Yes. And my editor is smart, I think. Is that the right order? Yes. Cool. So there's our actual mint. And that's it. Right. Because again, this can only be called as a cross chain message. So there's our deposit. Yeah. Any other questions before we move on to the last time is flying. So let me, let me slightly speed around this, but let's do a withdrawal. So here we're going to need a cross chain message this time from L2 to L1. I can just show you the arbitram pre-compile we're going to use. This is called RBSIS. So every, every arbitram chain gets this pre-compile. It lives at address 100 or that hex. And this just exposes all sorts of system level functionality for our purposes. We have this send transaction to L1. Very similar idea. Right. We specify the L1 address that we want to make some contract call and we give this, this data, this call data, which is the encoded message that we're actually going to call. Notice in this case, there's none of that gas stuff. And this is because there's no such thing as automatic execution on L1. So this has to just happen in two steps. Right. We initiated on L2 and then sometime later, the client will sort of finalize it on L1 via its own transaction. So perhaps slightly more inconvenient for the user, but convenient for us now that we don't have to have those parameters. So it's a two-step process. And this is where you have to wait the optimistic roll-up delay period if you're familiar with that stuff. Okay, cool. So let's do our, let's do a withdrawal here. Yeah, let's go for it. So let's see. We have like 15 minutes. That should be fine. Let's check to see. So we're initiating this on the L2 side. Once again, we have our mapping that is the source of truth of registration. So let's check to see that the thing is registered and also get the L2 address. So again, we're specifying our token by the L1 address. That's always the source of truth. And here I think we just need to see that it is registered. Yes. Just like before. So we look for how to address, require that it exists. Close. Autofill. Okay. So now, okay, for withdrawal, we want to burn first. So let's fill that in. Back in the token, we want to expose the affordance for the gateway to burn. So let's do that. In this case, we also need to actually verify that this owner has the right to burn it, which we didn't have to do for Mint. Basically, in order to get to the Mint code path, you had to transfer a token to begin with. So we were safe that you were the owner. In this case, you just gave me a token ID. Who the hell are you? You better be the owner. So let's make sure you're the owner. So the gateway will be responsible for passing in the owner address. And now we want to, I think it's owner of, I'm blanking out. I think that's right. What's that? Is that right? Yeah. Thank you. So we want to require that it's the owner passed in. Let's call this like, whatever. It's like potential owner, should be owner, whatever. Yes. Cool. So we make sure that this, so yeah, again, the gateway is responsible for making sure that that owner address is passed in. We make sure you're the owner. And then, okay, once we are, we can, we can go ahead and burn. Yes. Maybe there's a nicer way to do that, but that works. I don't think there is. Okay. So now we've exposed bridge burn. Oh, this also has to be on the gateway. Okay. Of course it would have been critical vulnerability. Okay. So now we can burn. Yeah. So we connect to the token on the L2 side of the address we just got from the registry. We call bridge burn. We pass in the owner or claimed owner, which will just be the caller of this method and the token ID. Yeah. Yeah. Okay. Cool. So we burn on the L2 side. Now we want to send a cross-chain message down to layer one, which once it reaches layer one, we'll release the same token from escrow. Same token. We're just, notice I didn't say this explicitly, but we're just sort of tracking tokens by IDs. We're just keeping the same ID at L2 as it has on L1 because why not? Okay. So we need the destination address, which we have the L1 counterpark gateway. Let's do what we did before and encode a function call. Here we can, we'll just do it in line, not as a new method. Okay. So what's our function and what do we need to send is the question in order to sort of release it from escrow on the L1 side. We can peek. So our method here is called finalize withdrawal. We specify these things. So let's encode that on the L2 side. I forgot them already. We specify L1 token token ID in the two address. Okay. And it's called finalize withdrawal, go on token, call it something. So this would be the L1 message call data in this case. That doesn't, oh, selector. Yes. So we encode it with the function selector. Will my linter just be nice? It will not. Oh. Okay. So this is the message we're encoding to send to L1. And now, there we go. Is that compiling? Yes. Now we will, yeah, we'll send a message to L1 with the RBSIS pre-compile. Yes. Yes. Yes. Yeah. So basically, that's a good question. So the question if you didn't hear it was like, how do we ensure that this mapping is legit, essentially? Like, how do we know this isn't the actual mapping? And basically, that's the registration step ensures that. So we're inheriting whatever trust we give to the L1 contract. Only the L1 contract itself can register. So the L1 contract, the token contract itself is the only thing that can self-register. And it specifies the L2 contract. So if it lies, it lies, but that's, right, then that means the L1 contract was malicious. Or if it gives the wrong address, that's its fault. Okay. So we're assuming that this L1 contract over here, when it makes this registration call, picks the right address on the L2 side. Right. I guess, yeah. I don't know if that really answers your question, but I think maybe the key thing is only this register call, it can update the mapping and only the token itself can affect it. Hope that's all. Okay. Where are we here? Oh, yeah. Now we have to send our actual message. So let's do that. Here's our message. We have this RBSIS, by the way, I'm just, you know, in the interest of time, not showing, but all this stuff is imported. These are some, this is like our library where we publish these interfaces. But anyway. So again, it's just a pre-compiled address 100. And our method that we're using is NTX to layer one. The address is the gateway contract, counterpart gateway or whatever it's called. Counterpart L1 gateway. Not the alias version. This time we want the actual address. And we'll send the call data. Oh, address. Sorry. There we go. Address 100. And that looks right to me. Yes. I think that's all we need to do here. I think I missed a return statement or two earlier. Cool. Okay. So let's finalize that withdrawal. This part should be fairly simple. So what does it mean to finalize the withdrawal? This means just release it from escrow. Well, okay. So the first part will be simple. Let's release it from escrow. So transfer it out to the destination address or the two address, I should say. So we connect and we transfer from to token ID. So from here. Right. It should already be escrowed. Sorry. Too much coffee. Very jittery. To and token ID. So there's the actual method that gets called. Now, the last thing to do is fix this error. Oh, I did something stupid. Okay. The last thing to do is to make sure that this actually only gets executed as a cross chain message. So this is only counterpart L2 gateway. This is a bit more involved, at least to understand the pieces of it. And I see there's eight minutes left. So maybe I'd rather, if there's any other questions, feel free. I feel like maybe that's, oh, if I don't see it, just shout. Yeah. Yeah. The doodle contract itself will do the registration and it only has to kind of register once is the way to think about it, right? So once the contract itself, and it's not necessarily the owner of the contract, I don't think about that way, it's just, however the contract is set up, the contract needs to be able to register. So in this case, it's not the owner. In fact, in this case, anyone can call register token L2, which means in this case, actually, it's probably not the best idea. You should deploy this contract and register right away to the right address. But as long as the function can make the external call to register, it can register. And whether that's proper, whether that's malicious, you should look at the L1 token contract. So that's, yeah. Probably the doodle team, right? So yeah. So whoever, so somebody, basically, whoever sets this up, probably the same party that deployed the L1 side, will sort of plan this. So they deploy both. And the reason you want to deploy both is so you just know which address to set. Okay. But the L1 contract ultimately has control. No. So good question. In the happy case, in the common case, no. The question was, when we deposit, yeah, does anyone need to call a function on L2? So the way these retribal tickets work, these are original cross-chain messages, is it should, it'll execute automatically, it'll try to execute automatically. So we supply the gas for the L2 side to execute. And assuming that doesn't revert, the user doesn't have to do anything else. So from a user perspective, they publish an L1 transaction, they wait a few minutes, and then it just, it'll get minted on L2. Now we can't guarantee the execution will succeed at L2. Like maybe there's a gas spike in the time between when you deposited and when there were triable tickets created at L2. If it fails, then someone's going to have to kind of poke it again to re, to re, to retry it, to re-execute it. We have a week to do that. But normally, yeah, in the happy case, that shouldn't happen. Correct. Correct. We supply it, supply it here. So actually, so when we do this create retribal, what this is doing, create retribal ticket behind the scenes, is it's actually depositing this message.value either, okay? So this is, so that's, that's, that's happening behind the scenes. So this, so this, this value that we send is actually getting deposited to L2, and that has to cover all of this gas cost that we're supplying. So that has to cover, you know, uh, uh, you know, gas price times gas limit plus max emission cost plus call value. Yes. Correct. The L1, yes. The L1 call supplies the gas for L2. Now, by the way, as like a side note, if you already do have gas at your L2 address, there's ways you can, you can use that. But this is, this is the path that we encourage for doing cross-chain messages is just supply the ether directly for various reasons. Yeah. Yeah. Cool. Then you can read, if you look at our docs, you know, I brushed over some of the details of retribal tickets because yeah, there's a lot going on there, but that's, that hopefully gives you the gist. Any other questions? I'll just start writing out how we do this in the meantime. Okay. I'll just finish this because it's, so here's how we're actually guaranteeing that the finalization of the withdrawal is really a cross-chain message. And here, this gets a bit into the weeds of how the sort of cross-chain messaging contracts work in arbitrum. But basically cross-chain messages all come from this bridge contract, which is a protocol contract. I'd like to show it to you if there's time. I'll just show you the first part. That's, that's the ultimate caller. Let me show you this one. So cross-chain messages just real quick. If we look at how they're executed, so they're initiated on L2 as we saw in the case of, you know, you're going to have to wait for the dispute period because it's an optimistic rollup. And then anyone can call this execute transaction thing. So in this case, it's, that's not automatic the way retribal tickets are. There actually is the second L1 step. The thing I want to show you in this execution, here, here, what we do is we, as we're executing this cross-chain message, we save this context thing so we can expose all sorts of data about this, what's happening, you know, what happened from L2. Notably, this sender data is what we're going to need. So we expose that in something like L1 to L2 sender. So during execution, it's a little hard to show I guess, but during execution, the point is as we're executing a cross-chain message, we can access this context so we can do the checks that we need. So that's going to be important. That's how we'll ensure that it's really coming from the L2 gateway. And then finally, the thing that actually makes the call is going to be this other contract, that's the bridge. So that's always how L2 to L1 messages work. So we ensure that it's really coming from the bridge and then we would also ensure it's really coming from the bridge. That's half of the work. The other half is making sure that we grab that context and make sure it's really coming from the address that we expect. Let me just throw that in here, I think there's time, so that this is at least complete. So we'll grab that outbox contract and this is, we can just do this in one line, I guess, slightly ugly, but we can instantiate the bridge and then the bridge gives us access to the active outbox. This is just so we don't have to store another address. Yes. That looks like something might be off. Instantiate, this is hard to do live. Oh, I didn't call it anything. Yes. Thank you. And then I think I also messed it up. Outer. Oh, at the end, yeah, that should be, yeah, we're instantiating after we call, there we go. That's the address. Okay. So we're just grabbing the outbox address that just stored in our protocol contracts. And now we can get the L2 sender via this neat little context trick. What is L2 to L1 sender? So that updates during the context of execution. And now we want to make sure that that thing is the counterpart gateway address, basically. So because there's no automatic execution, because this doesn't work the way you know, tribal tickets work, checking that the sender is where we expect it to be is slightly different. But this tells us, yeah, it's from the address we expect and it really is a cross-chain message. Cool. Since we're almost out of time, you know, so there's, if you're coding along or if you want to grab the repo, there's a branch that has like a completed version. I can also push this one. But there's scripts to deploy the contracts. I can even just really quickly run them. I don't think there's anything much to show there. But then there's another script that'll actually do a registration and deposit so you can mess with it. I'm getting to wrap it up. Okay. But yeah, cool. If we have to stop there, I guess we'll stop there. Yeah. Cool. Thank you all for listening.