
SmartNFT Drop 01 — Hack Postmortem
Ladies and gentlemen we have our very first Stellar smart contract hack postmortem!

📌 tl;dr
Everything is patched, zero funds were lost and a valuable lesson was learned.
The SmartNFT
series is a public experiment of the Stellar Turrets protocol. Its intention is to provide a safe yet exposed place to build and test production level smart contracts in the hopes that design patterns can arise, common gotchas can be addressed (like this one!) and confidence in the entire protocol can grow.
We’ve been making steady progress on the Turrets protocol and reference implementation thanks to massive efforts led primarily by the Script3 team as they press on with their YieldBlox project. YieldBlox is a huge protocol with a lot of complex market mechanics. Thus it’s likely the wrong project to pave the way into a production Stellar Turrets world. For that we need something less serious, more fun, simpler and modular. Something exposed and vulnerable yet also innocuous and experimental. Enter SmartNFTs
!
NFTs by their nature are a bit experimental to begin with. They’re weird, unexpected and expand beyond the bounds of a simple profit market model. There’s something game-like, and community centered about them, making them the perfect avenue for experimentation. SmartNFT
is the tokenized frontlines of DeFi built on Stellar. They are the Stellar community's collection of "I was here, and it got weird" badges of honor.
I am not a smart contract protocol designer — I don’t even have a CS degree — and I while I do pride myself on my ability to produce hard to kill code, these smart contracts are an entirely new beast. Which leads us to the topic of today’s postmortem. I wrote some really poor contract code. More specifically I didn’t write some simple code which allowed for a basic vulnerability to be exploited.
⚠️ Disclaimer
This post will be somewhat technical but if you’re planning on writing smart contracts for the Stellar Turrets network it would serve you well to grok it as best you can.
The Background
The Stellar Turrets protocol operates on the basic fundamental capability of the Stellar protocol to enable multisig on Stellar accounts. The concept is simple, by default each Stellar account is an asymmetric keypair. So you have the account’s public key (also know as the source or master key) which can be used by signing transactions or operations it is the source of by signing with the account’s private key (also known as the secret key). So if I want account ABC
to make a payment I'll need to sign that payment operation with the secret key counterpart of account ABC
. Multisig is added to an account by attaching the permissions of another public:secret keypair to the account in question. So if ABC
wanted account DEF
to be able to sign for transactions and operations on its behalf it would simply add account DEF
as a signer on its account.
Using this feature of Stellar we can give signing power of accounts (known as ctrlAccounts
in the Turrets protocol) over to the Turrets by adding Turret smart contract signers to the accounts we want them to control through those smart contracts.
Take the original SmartNFT00
issuing account for example GCRHEEBJQ5FLJPHIGIQWJ7YLBT64MK7TS7W4K7PDIZQC5HCFN7KVKOWF
. You'll notice two additional w:1
signers GAUPT4VNDXOSXVRGADHI2GYNAFKNLLWWWF4ON43GXXTCBH3AFW2EI4RV
and GBSOHYMDNL4DL2J62DMTFXRIFU7KU4G6SRTGTZD2KVFPUF5LMTNISZHX
which if you look up the contract hash for SmartNFT00
on the two Turrets I uploaded the contract to those are the signing keys I was given.
turret1-b97ea3f0ca317fc75a6b428a71b6c232e302d377f90e5e41fa8f8f8a145c5566
turret2-b97ea3f0ca317fc75a6b428a71b6c232e302d377f90e5e41fa8f8f8a145c5566
As mentioned both of those keys have been added to the G...KOWF
ctrlAccount
with a weight of 1. The operations thresholds for that account have all been set to 2 meaning that for the Turrets to be able to sign for any operations they both need to coordinate together to gain sufficient signing power to use the account. How do they coordinate? Through the smart contract!
This is the fundamental backbone of the Stellar Turrets protocol. It works well, so where is the issue?
The Attack
Any ctrlAccount
under signing control of the Turrets can generate a sufficiently signed transaction such that no other signatures are needed. Duh, yeah we just covered that. Okay so riddle me this smarty pants, what if the contract accepts a source
account intended to be a user's Stellar account public key but rather than inputing their own key they input one of contract's ctrlAccount
keys? Oof, the contract would execute its logic on the ctrlAccount
as if it were a user account wreaking potential havoc into the dependencies and flow of the contract.
This is exactly the vulnerability Nebolsin exploited on SmartNFT01
. The mitigation is incredibly simple, on the contract side just don't allow inputs to be ctrlAccount
addresses. Those address in my case are known and baked into the contract so I can just create if
statements to disallow those address as inputs.
Oddly enough this vulnerability is exposed on the SmartNFT00
contract as well but cannot be exploited as the transaction that contract builds includes adding a trustline for an asset issued by the ctrlAccount
and you cannot add trustlines for assets issued by the issuer. So the submission of the transaction would fail. Built in derived protection! It's worth calling that out as there's more than one way to close off a vulnerability, sometimes with simple if
statements and other times with protocol level side affects and features. What used to be a gotcha just saved SmartNFT00
from buying itself its own asset and escalating payments back to the most recent 95 buyers which would not at all have been my intent to allow for.
The Fallout
Thanks to the design of the Turrets protocol nothing unacceptable happened. The contract behaved exactly as it was designed to, just on an account which shouldn’t have been able to be operated on as a source of the contract. You can look through the official drop docs to see what the dig
command of the contract actually produces but the tl;dr is that a SmartPlotNFT
was minted, issued and de-authorized on both of the PixelAsset
distributor accounts.
GDKYN7QARRKKOITW5JAN35LTZVKWD52MJDTTKOJIBXVFX7IHKMA7UKCW
GAHRTMNCDU2T3BV4KA4LKEYQW6UIFTE7F3T6YPRMIBFIMFCJNSE76FTT
De-authorized is the key word here. When a > 0 balance asset is de-authorized on an account it’s effectively stuck in the account. This means 2 things, the obvious first is that you can’t send your asset anywhere. No sales, trades, payments or even burns back to the issuing account. The second is that because of this you won’t be able to delete the account by merging it away. This is the crux of the issue with this hack, the mint
command (which must be run before the issue
command can be run) includes operations which merge (delete) both distributor accounts back into the main issuer account. This won't be able to run as long as there are SmartPlotNFT
assets in either of the distributor accounts which will be true as long as those assets are de-authorized, which, will be like, forever, unless I have access to the issuing account of SmartPlotNFT
and can manually remediate the issue by authorizing the trustlines and sending those assets elsewhere. Thankfully Nebolsin was gracious enough to provide me with these signing keys and I was able to perform exactly that remediation.
The vulnerability of course was still open until I created and transitioned to the new smart contract with the appropriate if
statements to disable the dig
arguments from containing ctrlAccount
addresses.
You can observe the fallout from this by inspecting the primary issuer ctrlAccount GDJ2TPZFWEWXYIR27YMCUUR3KEDM37PUY7KY2MEFGB344EMTIRA7PXXJ
which now has two SmartPlotNFT
assets which were issued by the two hacked accounts.
GCV7X75NDTRCHLB6DT4HMWGYBZEDUSTDONXLC7CLGHJFSVVASZ4M2IDK
GDIKLCUDJSHLO5QLRIZPD5NHI3JSFDK5C5T7ULTYU3Q6CWUGST7JJJVU
These are the derived child addresses of the above two distributor ctrlAccount
accounts which were previously locked by receiving the SmartPlotNFT
from these two issuers. If you look through the operation history you can see exactly what happened and how it was remediated.
But wait, what do you mean transitioned to a new smart contract? Good question. In the case of the SmartNFT
contracts I’ve left the master ctrlAccount
keys as primary signers allowing me to swap out old Turret signers with new ones from new upgraded contracts. This is an intentional design decision intended to protect the SmartNFT
ecosystem during this experimental phase. It is not a built in feature when using Turrets. If I had chosen to remove myself as a signer there would have been no remediation, no transitioning, the contract would have died the good, though unfortunate, death. Leaving yourself as a primary signer isn’t very decentralized depending on the goals of your protocol. The ideal scenario would likely be to build in a governance model from within your protocol such that signer swapping and contract upgrading could happen in a fully decentralized way.
The Summary
Thankfully this was an incredibly innocuous hack and thanks to Nebolsin’s cooperation and coordination with me we were able to resolve without any fundamental changes to the contract itself. If he had signer swapped and tossed or refused to gift me the secret keys for the SmartPlotNFT
issuers I would have had to remove the distributor deletion operations in the mint
command. That would have been fine, but I'm glad it didn't come to a protocol change.
This highlights a really nice fundamental feature of the way Stellar Turrets operate. Most hacks won’t be detrimental and far reaching. You definitely need to design carefully but it’s actually quite easy to sandbox effects into operational layers and account sandboxes isolating attack vectors to dead contracts or ctrlAccounts
vs wide spread lost or stolen value. User accounts and funds are rarely directly controlled. Stellar Turrets will most often act as functionality coordinators for actions performed external to the Turrets network. This makes the design very flexible and the attack surface on Turrets quite small. In this hack assets were never at risk and actions were still entirely controlled by the contract, there was no unauthorized or unacceptable access, just unexpected due to a design oversight on my part.
All that to say Stellar Turret design best practices and common gotchas are still emerging so design and participate with care. Know your contract designers and understand the risks while this whole thing grows up, matures and stabilizes. This is an exciting time to participate in DeFi and it’s neat to be able to tokenize that in some small way via the SmartNFT
experiment.
🗣 Shoutout
Sergey Nebolsin for white hat hacking me. Definitely could have caused more problems for me but he didn’t. Thanks!
Interested in learning more? Have questions for me? Ready to be the better builder and begin creating your own un-hackable contracts and protocols?
Want (aka need) to snag your own piece of SmartNFT
history?