Recently, the cryptosphere has seen a surge in a particular type of “hack” where a user thinks they are safely interacting with a dApp, but later finds their account drained of their tokens.
While word is getting around that wallet users need to be careful when doing certain things, this class of hack exposes one of the deep flaws in the way that virtually every smart contract platform works today. In short, it’s highlighting the fact that “holding tokens in your wallet” doesn’t work like most users think.
In fact I wager that most crypto users don’t actually know where their tokens are.
So where are my tokens?
In short, they’re not actually in your wallet - your wallet just knows where to look.
On Ethereum and virtually every other smart contract platform, tokens are a creation of smart contracts. Most people have heard of an ERC-20 contract (other types like ERC-721, etc.) that is deployed to create a token. How does this work? Well, the contract doesn’t say “hey Ethereum, I would like to create some tokens, please”. What it does is create and maintain a private list of balances for account addresses that represent ownership of a new “token” – that’s all a token is on these networks.
This private list is really as simple as Alice’s address has 50 USDC, Bob’s address has 190,334,025 USDC, and so on. If you’re Alice, your “ownership” of 50 USDC just means that your wallet knows to ask the USDC smart contract for your account’s entry in the list. If you want to “send” 3 USDC, what your wallet is actually doing is sending a message to the USDC smart contract that says “hey, I’d like to send 3 USDC to Bob” and hopefully the smart contract reduces your number by 3 and increases Bob’s by 3 - you have no guarantees of what happens inside the contract.
If you own 8 different tokens, this system looks a lot less like “there are 8 tokens in my own private wallet” and a lot more like the traditional world of “I have balances in 8 different bank accounts”. You don’t directly hold or control those tokens; you’re relying on the creator of the smart contract to manage a list of balances correctly and give you the ability to “send” and “receive” tokens by changing those balances.
Note: Platform native tokens (or “coins” if you prefer) like BTC or ETH are special. These tokens actually are directly assigned to an account address, and you can say pretty confidently that they are held in your wallet and you can do whatever you want with them. But, using Ethereum as an example, every other token, stablecoin, NFT, or other asset on Ethereum other than ETH doesn’t work like that. Each one is just a balance held within a smart contract somewhere, created by somebody.
This is kind of shocking if you’re used to popping open Metamask and assuming that the tokens you see there are all safely there in your wallet. Not only that, this way of representing assets creates all sorts of opportunities for your tokens to get stolen.
The spend approval hack
This way of creating tokens on a blockchain is the source of a class of hacks that I’ll put under the category of “spend approval”, which all end in tokens being stolen from accounts.
These hacks all rely on something that happens all the time in DeFi: If you’ve ever used a dApp where you had to send it some tokens (even for something as simple as sending tokens to Uniswap for a trade), you’ve probably noticed that first you had to “give approval” for the token you wanted to send. Maybe you didn’t even think about what this means – it’s just something you have to do before you can do your trade.
Here’s what’s happening.
Remember that all tokens (other than the special cases like ETH) are just balance entries in multiple smart contracts, so “sending” tokens is a message to the smart contract requesting the change of balances. You would expect that only the owner of an account is allowed to request that their balance be decreased (at least if the smart contract is implemented correctly!). But this poses a problem for using tokens with smart contracts.
Taking a Uniswap trade as an example, you don’t just want to “send” tokens to the Uniswap smart contract; you also want to ask Uniswap to do a single-step process of accepting the 50 USDC and returning, say, 1621 SHIB to you. Unfortunately, there are two problems. One, you can’t directly call multiple smart contracts in one transaction (Uniswap and USDC). Two, there’s no way of telling the USDC contract “hey, I only want to send these USDC tokens if I also get SHIB out of the Uniswap contract”.
Well that means you can’t do a whole lot of DeFi without a different solution. So token smart contracts had to add a feature: spend approval for other addresses. Basically it has become the standard for token smart contracts to let the account owner send a special transaction to say, for example, “if Uniswap asks for some USDC tokens from my account, that’s fine”.
That’s what’s going on when Uniswap asks you for that approval step before you swap; you’re giving Uniswap the ability to spend your USDC on your behalf so that you can make a single transaction to Uniswap that only says “take 50 USDC from my account and give me some SHIB back” (once again, you must trust that Uniswap will do what you expect). This spend approval standard makes the majority of DeFi possible, so users are constantly being asked to give approvals like this for the dApps they connect to.
Now, smart contract developers aren’t dumb. This spend approval function on token contracts also includes the ability to set a limit on the amount of the token the smart contract is approved to take. The problem is that each one of these approvals means this extra approval step (with another network fee to pay). So if you’re Uniswap, do you want to ask users to approve only the amount they want to swap every single time? No, you ask them for approval to spend an infinite amount of the token so you never have to bother them again. And that’s what virtually every dApp does by default.
This is crazy. It’s as if paying your phone bill meant T-Mobile asking you for the login to your bank account.
But this is what people are used to today: approving smart contracts to take any amount of a given token and trusting that the smart contract will only take what is expected. Users could adjust the limit down to be more safe - but most don’t bother.
So you can already probably see where the hack comes in: A dApp promises some cool functionality or an investment opportunity, you connect your wallet, the dApp requests an approval to spend your tokens, and since you’re so used to this process, you approve. Maybe you thought you were only going to send a small amount of USDC rather than all of it. Maybe you thought you were approving a token that was masquerading as another. Regardless, at some future point the rug is pulled and your tokens vanish when the smart contract you approved takes them.
Or worse, maybe you bought a hot new token and found out later that the token smart contract itself didn’t even need your approval for somebody else to take your bag (and then go sell it on the market before you noticed the rug was pulled).
There are many variations, but the problem is the same: It is insane that a “decentralized finance” platform would have its assets of value represented in a way where this sort of thing is possible in the first place. But this model of token interaction is used not just on Ethereum but virtually every other smart contract platform, and so we see this “hack” across multiple networks.
Radix tokens work how you think they do
This is one of the many reasons that Radix went back to the drawing board and made assets a safe, built-in feature of the platform – not something implemented thousands of times in separate smart contracts. This applies to simple tokens issued on the current Radix Olympia release network, and is extended further with Radix’s coming smart contract and token/NFT-creation capability.
On Radix, you actually hold your tokens in your account – just how you imagine it. Tokens on Radix can’t include any hidden behavior that you’d only see if you read some smart contract code. And there’s no such thing as approving dApps to spend your tokens – you actually pass the smart contract only the tokens that you want to spend for that transaction.
Radix does this while still allowing creation of the wide variety of assets and dApps that people want. Radix has introduced its unique asset-oriented smart contract language, Scrypto, that allows creation of powerful logic for finance systems, games and more to be written around platform-managed “resources” that are easily configured to have the behavior they need as tokens, stablecoins, NFTs, and more. You can start writing Scrypto-based smart contracts now – and deployment to the Radix Public Network will be possible at its coming “Babylon” release.
This is one of the reasons why we believe Radix is the only platform truly built to serve as the basis of decentralized finance that can go mainstream.
To learn more about Scrypto and Radix Engine, read our 4-part series starting here.
If you’re a developer and want to start building with Scrypto, get started on our developer’s hub.