Solana: The Bugs You're Missing | DeFi Security Summit 2025
DeFi Security Summit at Devconnect Argentina 2025 · November 2025 · 21:17
Robert Reith, CEO of Accretion, describes a class of Solana bugs that experienced developers still write and auditors often miss. Drawing on advanced vulnerabilities found over the past year in production protocols, he covers runtime nuances, account validation edge cases and account lifecycle quirks, giving security researchers new angles beyond the standard bug lists.
Speakers
Topics
From DeFi Security Summit at Devconnect Argentina 2025, hosted by DeFi Security Summit.
Transcript
Right. Um do I wait for a minute or can I start? All right. So, uh yeah, welcome to uh my talk. My name is Robert and I think this screen is not working. Ah, there we go.
All right. Um, can I get the notes on the on one of the uh screens just real real quick? I have now to talk twice. Okay. But um I can start anyways. So um today I'm going to talk about Salana um and in particular some buck classes that might be um less known or like not not as common to find in um at least from what I've seen. Um and yeah, my name is Robert or Rob online. Um I've been like in Salana
auditing for maybe three and a half years. Um, and yeah, that's me. Maybe you recognize me by my PFP or something. Um, I'm the founder of Accretian and we're like a Salana focused auditing company. So, we only do Salana uh audits. So, if you're interested in Salana audits, uh, we're also hiring. Uh, yeah, shoot me a message. Um, yeah. So, while I was making the talk, I kind of realized that not all the bugs I wanted to talk about will fit into this. So I kind of decided to focus on one particular category which is account life cycle bugs. Um I think if you've been in Salana auditing you might know
that a lot of bugs come from account validation issues. So Salana has this um uh system where you have store uh storage is all um state is all stored on accounts. So it's all always very important to validate those accounts and most common bugs are like simply account validation bugs but actually there's another whole category I would say that um is account life cycle bugs which relates how accounts are created um how they change uh and um how they move through um state when they're active and
how they're in the and delete it again. And for this you should know the most basic background for Salana is [snorts] um every account has an address. By default uh an account um belongs to the system program. So every account belongs to some program and only that program is allowed to write data to an account. that includes reducing lamports and any account uh any program is allowed to increase lamps. It's like the only exception. So um an empty account has a owner of all zero and all zero happens to be the address of the system program on Salana which I show here as like one
one one because that's base uh 58 of all zeros. It's a bit confusing but uh once you know that 111 stands for system program. So the Salana account life cycle is simply um an account create uh belongs first to the system program. Then you can call the system program to allocate and assign that account to another program for example the token program. And um then the account belongs to the token program. And now only the token program can um change the state in this account or the data. And uh now you have to call the token program to um put data
into this account. And then you have the third um version of this account on the slide. And after that you can delete this account if the program allows to delete an account. In this case, the token program allows you to delete an account and you can reset it basically back to the original state. And that's the whole account life cycle. And that's kind of all that there is to um to this life cycle category really. But um what's interesting is on top we have the same uh life cycle that I just explained. On on the bottom we have the same life cycle again but with a different program. So you can take the same account that you uh went through this life cycle. It belonged to
the token program before and in a second cycle you could um assign it to for example a launchpad program and then the launchpad program can use it and then maybe the launchpad program can delete it again. And um now like if you think about it, oh this is like a storage piece of storage with an address that at one point in time belongs to one program and at a later point in time belongs to another program. Um that's a bit of a weird kind of thing that uh can lead potentially to issues if you think about it. Um and the normal way to delete an account on Salana go back here is that the program which is in charge of that
account would zero out every um all the data and reallocate the account's data to a length of zero. Um and it will reset or like reduce the lamps to zero and then it will change also the owner of the account to all zeros which is the system uh system program. So that's the main way and the uh correct way to close an account. But there's also a second way to close accounts which is simply to reduce all the lampers to zero. And when the lampers are zero then um the at the end of the transaction the garbage collector of the runtime will go and see oh this is an account that does not have a rent
deposit. So, we're going to delete this account, which means the garbage collector in this um scenario would reset all the other fields all to zero. Um, and that's just something you should keep in your mind. So, yeah. Now, I would like you uh to just show you uh six different bugs that can arise from this um life cycle um idea. The first one is um honestly it's quite simple. Um here we have like a AMM program uh account and this pro AM program account wants to pay some fees to the creator of a AMM pool and for
this this um program saves the creator's token account address in the pool's uh account. So it's just a reference to the token account directly. And uh in this case it would be not required to be an ATA. It could be any token account any kind of token account. So if you think about the life cycle the obvious issue here is that it's a token account and token accounts can be closed. So if the uh if the owner of this token account decides to close this token account at this address then any uh swap on that AMM in this mm pool which then tries to transfer tokens to
this token account will just fail because the account doesn't exist. Um and this is a very simple denial service because of this normally you should never save like a token account address in an account in in Salana. And the the pattern to remember here is um if you close an account, is there some other um instruction maybe that would require that account to be open? That was like a very simple issue and very similar to that issue is um this version here. We have like a launchpad program and in this launchpad users commit USDC and maybe they will later
get tokens from that launchpad or maybe they will get a refund and for like a user experience uh reason this launchpad wants to crank the refund payment or the token payment back to the user so the user doesn't have to go to the website and claim those tokens. And because of that um they save obviously the depositor for each deposit into a launch and at the end of the launch the crank will run and look at the depositor and then send tokens to the ATA address of that depositor and that's a normal very common pattern but simply the distinction of um the crank being run by the project itself
means that the project will have to pay any rent if that account does not exist yet. So if the uh owner of the token account closes all their token accounts and then this crank is run, they will um the crank uh the cranker will have to deposit rent to create that account and run this instruction and then uh the account will belong to the owner like who who receives the tokens and they can close this account and they will receive this rent that was paid for them. And on Salana, this rent deposit is actually quite significant, like 30 cents. So if you create like a civil network with a,000 accounts, you can make $300.
If you make 10,000, you can make $3,000. And it's really very easy way to like get free money. And this has happened in the past. And uh yeah, the recommendation here is if you're going to pay rent for people, then make maybe when they deposit in the beginning, make them upfront uh pay you like a a rent deposit on top. Um yeah, so here explain it as well. So um yeah, the third case I think um is [snorts] here is getting a bit more interesting.
Here we have like a very regular token 22 kind of relation. So on the left side we have a token 22 token account. On the right side we have a token mint. And um on token 22 you can have those extensions. And here we have a closable mint extension. That's the very last item for the um mint on the right side. And um the closable mint extension says that if an mint account has a supply of zero, which in this case is the case, uh then we can close this mint and reassign it to the system program. And this can be done by the mint close authority.
Um obviously the token account for this mint will also have an amount of zero because otherwise the supply wouldn't be zero. But nonetheless, in this situation with both amount and supply being zero, we can close the mint account. Um, and then once it's closed, we could go ahead and assign like recreate a new account at this address. Go to this life cycle again and assign it to a completely random program. For example, an attacker program that we created, which just mimics a token program and creates an account that looks like a mint but isn't a mint. And then maybe we can come up with some kind of bug where um
uh some program checks the token account and then just assumes the mint is checked as well um even though it is not a mint that belongs to the token program. So I don't think it's really uh it's really uh often exploitable issue this specific one but I think we can just agree that um it's kind of questionable that there can be a token account on Salana which points to a mint which is not belonging to the token program. That's like a very weird scenario which like just intuitively can lead to issues. And I think um this next case here this is my favorite of of them all and uh for this I have an example with a audit contest
platform. So uh we have two types of accounts again as always uh the first type is an audit contest. an admin can initialize this audit contest and say, um, okay, this audit runs from today until like next week, and anyone who wants to participate has to deposit $100 if they want to report a bug so that we don't have spam. Um, and then if someone wants to report a bug, they have to call this program. they have to deposit $100 and this creates like an account which counts as a bug report and they can get their money back if they withdraw their report or maybe if they um if the report
gets validated. So, so far so good. And now the interesting thing is what happens if maybe the contest configuration is closable because after the contest um they might want to reclaim the rent of that account because if that would be the case then we could do a very interesting attack. Um the the admin would basically start by creating a contest and create a contest with a zero deposit.
um and basically an expiry date that's like immediately in 5 minutes or whatever. And in those five minutes, they would create a bunch of uh bug reports for this contest. Let's say 100 bug reports. Um for each of those bug reports, they have to deposit $0 because that's what confi what is configured here. Um and next they just wait for this cont contest to expire like 5 minutes later. They could also make it like a couple seconds if they want. um and they close this contest configuration account. And now the crucial part is they can recreate a new contest at the same address. And that's surprisingly quite often the case that you can rec close an account and
recreate a new account at the same address. But when they recreate this account, um they also don't resolve any of the bug reports. And they recreate this account but now with $100 of deposit. And um now you just wait for a couple bug reports by real bug hunters. Maybe you make an interesting contest with very shitty code. Um so a lot of hunters will report bugs and everyone will deposit their $100. Um, but as soon as you have a couple hundred in the contract, you can take those old bug reports that you deposited $0 for, and because they now point to a um to a contest that's valid with $100 of deposit, you can cancel those um um you
can cancel those bug reports and say, "Oh, I think it's invalid, so I want my money back." Um and you can reclaim those $100 that you never paid. So this would be a loss of funds basically. Um and I call this category like a reopen sames because you close an account and reopen a new account with a different configuration. And this can happen when you can close an account and there are references to this account um from other accounts. Right? Uh for the next case um I call it over constraint. Um uh we have like a public order book and an order book um that has to be cranked and this crank should always work because otherwise the order
book would stop working. Um, so it cranks to like match orders or whatever. And if you call it correctly, it should work, right? And the interesting part here is this uh order book PDA uses uh the creator as a seed, which is actually also common to use the account as a seed. But for some reason, they defined in these constraints that this creator has to be a system account. Um and a system account as we know um can change the state to not being a system account.
So we can reassign it to a different account. And because the developer here was very cautious and they thought oh I have to put a strict constraint on every account um they kind of shot themselves in the foot because this constraint can be easily invalidated by reassigning this account. So that's another case very simple and uh actually happens more often to like experienced devs who try to put a lot of constraints and are very careful. Um okay and this is the last one. Uh in this case um we have like a program that creates some pool and it wants to assign um a mint to that pool. So it wants to
save in that pool some mint that belongs to that like kind of relates to that pool in some way. And for that we take the mint account and um we check for that mint account that this mint account belongs to the token program and then we check that this mint account deserializes. Um, and normally, okay, those are interesting checks, correct checks that you can do, but it turns out that it might not be enough to correctly validate this mint because um what what happens if the mint actually has zero lamps? Um, if we remember what happens with zero lamp accounts is that the garbage collector at the end of the transaction will look
at this account and reset everything to zero. So the the program ownership the data will all get reset to zero if it has zero lamperts. So if we could maybe create a mint account or not not really create a mint account, it's actually only has to be an account that belongs to the mint program. So we could maybe create an account, assign it to the mint program, allocate 82 bytes, which is the exact length that you need for a mint account to des serialize.
then those uh checks would pass and this assignment would uh just complete correctly. So the path here would be have a random account assign and allocate to the token program. It's all zero bytes at this point because we never call the token program. Um and then we call this weird instruction and this instruction will check oh yeah this account belongs to the token program. Okay, 82 bytes. It des serializes all the values are zero, but we didn't check that. Um, and now at the end of this transaction, um, the garbage collector will go and see, oh, this account has zero lamp ports. We're going to reset it to zero.
So now it belongs to the system program. And now what we have is a dangling pointer. So we have a um we have again a case where we have a account which stores this pool. But this mint is not a mint, but actually just a random account that belongs to the system program. and we could assign it anywhere and yeah I mean that's that's it that's like this category of life cycle bugs um which is quite an interesting category you get mostly denial of service but of sometimes you get loss of funds bugs this way um and to spot it it's actually quite easy you just look at how accounts are closed uh which accounts are closed um and maybe there exist references to accounts when they are closed uh can we
create maybe a dangling pointer that means like a reference to an account that doesn't exist anymore. And um can we recreate an account at an address um that didn't exist uh wait at the same address where this account existed before? Yeah. And to fix it basically the main tip don't close accounts or if you have to close accounts um only delete all the bites except for one and that bite can act as a tombstone. Other ways is to use a PDA derivation instead of uh key accounts and use monot monotously monotonously increasing um number in the
PDA derivation path. So you can never recreate the same PDA or recent slot um or use reference counters. So you always count up or down when accounts are created. So no dangling pointers can be created. Um yeah, that's it from me. Um I'm not sure if we have time probably not for for for questions. >> Um I don't think we do unfortunately but thank you so much Robert. Thank you. >> But you can find me outside and uh ask me directly. Thank you.